New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DM-24330: add ability to run an obs_base command via the butler command #258
Conversation
python/lsst/daf/butler/cli/butler.py
Outdated
verbose = len(sys.argv) > 1 and "-v" == sys.argv[1] | ||
logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO) | ||
log.setLevel(lsst.log.Log.DEBUG if verbose else lsst.log.Log.INFO) | ||
return getCli()() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file doesn't have to be called butler.py; it's called by the file at bin/butler
which is the file name that causes the butler script to be named butler. This could be named e.g. butlerCli.py
if that could reduce confusion between this and the file that owns the Butler class (_butler.py
). I wasn't sure which way to go on that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it really matters given namespacing. @TallJimbo ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good. I do have a few comments though.
doc/lsst.daf.butler/index.rst
Outdated
:prog: validateButlerConfiguration.py | ||
:groups: | ||
|
||
.. click:: lsst.daf.butler.script.butler:cli |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
script.butler or cli.butler?
python/lsst/daf/butler/cli/butler.py
Outdated
def _getPluginList(): | ||
pluginModules = os.environ.get("DAF_BUTLER_PLUGINS") | ||
if pluginModules is None: | ||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this return an empty list? Otherwise _importPlugins
is going to get confused.
python/lsst/daf/butler/cli/butler.py
Outdated
|
||
|
||
@click.group() | ||
# this group doesn't use the verbose command but does allow (ingore) it, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: ingore
python/lsst/daf/butler/cli/butler.py
Outdated
verbose = len(sys.argv) > 1 and "-v" == sys.argv[1] | ||
logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO) | ||
log.setLevel(lsst.log.Log.DEBUG if verbose else lsst.log.Log.INFO) | ||
return getCli()() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it really matters given namespacing. @TallJimbo ?
python/lsst/daf/butler/cli/butler.py
Outdated
|
||
|
||
def main(): | ||
verbose = len(sys.argv) > 1 and "-v" == sys.argv[1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought click meant that sys.argv wasn't involved. Are we causing trouble by wanting every command to have a --verbose
flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a couple things here:
- I wanted to be able to log the loading of plugins, this is before the invocation of the top-level Click object.
- As far as I can tell, logging does not like to be set up more than once. It seems there are some ways to force it but I couldn't get consistent results. (in Python 3.8
logging.basicConfig
adds an argforce
which may help with this, but I think we are on 3.7?)
The best solution I could come up with was to have the verbose option handled outside of Click, and have it turned on or off globally. Not every command will have a --verbose flag. The usage is
Usage: butler [OPTIONS] COMMAND [ARGS]...
Options:
-v, --verbose Turn on debug reporting.
--help Show this message and exit.
Commands:
create Create an empty Gen3 Butler repository.
dump-config Dump either a subset or full Butler configuration to...
validate-config Validate the configuration files for a Gen3 Butler...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with every command having this. If we only want one special option though I would be happy to abandon --verbose
and replace it with --log-level
and have it default to WARN. This gives people a lot more control over DEBUG and INFO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, I'll change --verbose
to --log-level
tests/test_cliOptionRepo.py
Outdated
|
||
|
||
@click.command() | ||
@repo_option() # required defaul val is False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: defaul
tests/test_commandLine.py
Outdated
result = runner.invoke(butler.getCli(), ["dump-config", "here"]) | ||
self.assertEqual(result.exit_code, 0) | ||
# check for some expected keywords: | ||
self.assertTrue("composites" in result.stdout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Can't we read the output as YAML, parse that, and then check for composites
in the dict
?
Also, please use assertIn
.
ups/daf_butler.table
Outdated
@@ -11,3 +11,4 @@ setupRequired(afw) | |||
|
|||
envPrepend(PATH, ${PRODUCT_DIR}/bin) | |||
envPrepend(PYTHONPATH, ${PRODUCT_DIR}/python) | |||
envPrepend(DAF_BUTLER_PLUGINS, lsst.daf.butler.cli.cmd) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want this for butler itself because the butler already knows where its own scripts are. The butler standard set should always work regardless.
For testing I'd create a trivial command line plugin that echoes back the subcommand name. In the test set the environment variable to include the path to that test code and run the new subcommand and check the output.
Note that you can create a class inline in a test and use getFullTypeName on that class and doImport will work on it (since Python will realize it has already been imported). -- the trick is triggering the read of DAF_BUTLER_PLUGINS if some other test has already triggered it.
tests/test_commandLine.py
Outdated
self.assertTrue("datastore" in result.stdout) | ||
self.assertTrue("storageClasses" in result.stdout) | ||
|
||
def testDumpConfig_file(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you check that the subsetting command line option works as well please?
tests/test_commandLine.py
Outdated
def testValidateConfig(self): | ||
"""Test validating a valid config.""" | ||
runner = click.testing.CliRunner() | ||
with runner.isolated_filesystem(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also check that an unknown dataset type causes a bad exit status.
b9c0574
to
696687c
Compare
python/lsst/daf/butler/cli/butler.py
Outdated
|
||
def _initLogging(logLevel): | ||
log = lsst.log.Log.getLogger("lsst.daf.butler") | ||
if "critical" == logLevel: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought I made a comment on this but I can't find it any more. Just in case it got lost: please don't do this with if
statements. The logging module has methods for translating the text string to a logging level and lsst.log has method for converting a logging level to an lsst.log level. See astro_metadata_translator command line tool for example code.
c2c4fe7
to
509303b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t use absolute imports when you are wanting relative imports.
python/lsst/daf/butler/cli/butler.py
Outdated
import logging | ||
import os | ||
|
||
from lsst.daf.butler.cli import cmd as butlerCommands |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use relative imports in all this code
98a86d4
to
2879484
Compare
2879484
to
e2a8432
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor comments. Some really impressive test code.
python/lsst/daf/butler/cli/butler.py
Outdated
|
||
|
||
def funcNameToCmdName(functionName): | ||
"""Change underscores, used in fucntions, to dashes, used in commands.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: fucntions
python/lsst/daf/butler/cli/butler.py
Outdated
try: | ||
return doImport(pluginName) | ||
except (TypeError, ModuleNotFoundError, ImportError) as err: | ||
logging.warning("Could not import plugin from %s, skipping.", pluginName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this add the message to the right logger? I think we want
log = logging.getLogger(__name__)
at the top and then 'log.warning()
. I know that this logger is only going to be triggered from the command line tooling but we should be consistent about log hierarchies (unless you are going to tell me that it's required to use the default logger).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nope, I'm just confused about logging. Will fix.
python/lsst/daf/butler/cli/butler.py
Outdated
plugin = self._importPlugin(pluginName) | ||
if plugin is None: | ||
continue | ||
for command in plugin.__all__: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use:
commands.extend([funcNameToCmdName(command) for command in plugin.__all__])
?
python/lsst/daf/butler/cli/butler.py
Outdated
try: | ||
cmd = doImport(fullCommand) | ||
except (TypeError, ModuleNotFoundError, ImportError) as err: | ||
logging.debug("Command import exception: %s", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logging -> log (I won't mention again)
""" | ||
try: | ||
return doImport(pluginName) | ||
except (TypeError, ModuleNotFoundError, ImportError) as err: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we gain much by trying to restrict the exception handler to these three rather than always reporting the problem for all Exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, probably not. will change to all Exception.
localCmd = self._getLocalCommand(name) | ||
if localCmd is not None: | ||
return localCmd | ||
for pluginName in self._getPluginList(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this be simplified a lot of if you had a dict mapping command name to thing to import? You'd presumably then be able to catch people defining two identical subcommand names in different packages. At the moment it seems that if two identical subcommands were defined you'd get the one that was ahead in the environment variable path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch.
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
__all__ = ["create", "dump_config", "validate_config"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think @TallJimbo and I agreed that we wanted these names reversed to be config dump and config validate.
@repo_option(required=True) | ||
@click.option("--quiet", "-q", is_flag=True, help="Do not report individual failures.") | ||
@dataset_type_option(help="Specific DatasetType(s) to validate.") | ||
@click.option("--ignore", "-i", type=str, multiple=True, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where does the comma splitting happen? Doesn't it need the same callback that dataset types get?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callback splitting is in definition of the dataset-type option. If we need some dataset-type options to take multiple and some not that can be added tot he option. https://github.com/lsst/daf_butler/pull/258/files#diff-191150039ddacf9a20d7744bc23e627aR37
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I'm trying to say is that this --ignore
option previously processed commas but I don't see it doing that in this implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I was looking at the wrong line. I'll add it to ignore
#!/usr/bin/env python | ||
|
||
# This file is part of daf_butler. | ||
# This file is part of obs_base. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not obs_base
@@ -0,0 +1,49 @@ | |||
# This file is part of daf_butler. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For utility functions we don't require a separate file per function. The reason I wanted separate files per sub command was that I was worried we were going to get many sub commands and things would get out of control. We envisaged that there would be support code needed for the subcommands and that would get confusing. You seem to have mitigated a lot of that worry with clever callbacks in click definitions but we'll see what happens when more complex commands turn up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll move utils into their own file. I think it's worth keeping commands and options in separate files at least for now.
be6a42f
to
ab3e4f6
Compare
ab3e4f6
to
bd84f18
Compare
No description provided.