Skip to content
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

Merged
merged 8 commits into from Apr 22, 2020

Conversation

n8pease
Copy link
Contributor

@n8pease n8pease commented Apr 13, 2020

No description provided.

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()()
Copy link
Contributor Author

@n8pease n8pease Apr 13, 2020

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.

Copy link
Member

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 ?

Copy link
Member

@timj timj left a 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.

:prog: validateButlerConfiguration.py
:groups:

.. click:: lsst.daf.butler.script.butler:cli
Copy link
Member

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?

def _getPluginList():
pluginModules = os.environ.get("DAF_BUTLER_PLUGINS")
if pluginModules is None:
return None
Copy link
Member

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.



@click.group()
# this group doesn't use the verbose command but does allow (ingore) it,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: ingore

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()()
Copy link
Member

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 ?



def main():
verbose = len(sys.argv) > 1 and "-v" == sys.argv[1]
Copy link
Member

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?

Copy link
Contributor Author

@n8pease n8pease Apr 14, 2020

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:

  1. I wanted to be able to log the loading of plugins, this is before the invocation of the top-level Click object.
  2. 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 arg force 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...

Copy link
Member

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.

Copy link
Contributor Author

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



@click.command()
@repo_option() # required defaul val is False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: defaul

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)
Copy link
Member

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.

@@ -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)
Copy link
Member

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.

self.assertTrue("datastore" in result.stdout)
self.assertTrue("storageClasses" in result.stdout)

def testDumpConfig_file(self):
Copy link
Member

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?

def testValidateConfig(self):
"""Test validating a valid config."""
runner = click.testing.CliRunner()
with runner.isolated_filesystem():
Copy link
Member

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.


def _initLogging(logLevel):
log = lsst.log.Log.getLogger("lsst.daf.butler")
if "critical" == logLevel:
Copy link
Member

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.

Copy link
Member

@timj timj left a 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.

import logging
import os

from lsst.daf.butler.cli import cmd as butlerCommands
Copy link
Member

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

@n8pease n8pease force-pushed the tickets/DM-24330 branch 7 times, most recently from 98a86d4 to 2879484 Compare April 20, 2020 22:43
Copy link
Member

@timj timj left a 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.



def funcNameToCmdName(functionName):
"""Change underscores, used in fucntions, to dashes, used in commands."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: fucntions

try:
return doImport(pluginName)
except (TypeError, ModuleNotFoundError, ImportError) as err:
logging.warning("Could not import plugin from %s, skipping.", pluginName)
Copy link
Member

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).

Copy link
Contributor Author

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.

plugin = self._importPlugin(pluginName)
if plugin is None:
continue
for command in plugin.__all__:
Copy link
Member

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__])

?

try:
cmd = doImport(fullCommand)
except (TypeError, ModuleNotFoundError, ImportError) as err:
logging.debug("Command import exception: %s", err)
Copy link
Member

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:
Copy link
Member

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?

Copy link
Contributor Author

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():
Copy link
Member

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.

Copy link
Contributor Author

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"]
Copy link
Member

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,
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Contributor Author

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.
Copy link
Member

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.
Copy link
Member

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.

Copy link
Contributor Author

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.

@n8pease n8pease force-pushed the tickets/DM-24330 branch 2 times, most recently from be6a42f to ab3e4f6 Compare April 21, 2020 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants