[WIP] Generalize command-finding to support sub-commands, incl. for extensions.#11145
[WIP] Generalize command-finding to support sub-commands, incl. for extensions.#11145greenc-FNAL wants to merge 13 commits intospack:developfrom
Conversation
|
Update: These questions have been answered in the comments below, resulting in significant evolution of this PR's implementation. Please see the primary description above for the most current explanation of the features and details of this PR Questions for reviewers:
|
alalazo
left a comment
There was a problem hiding this comment.
There are a few comments on the code below. Also, it seems that this PR breaks previously working extension commands like this one. To give an example here's develop at 263d8a8:
$ spack filter -h
usage: spack filter [-h] [--installed | --not-installed]
[--explicit | --implicit] [--output OUTPUT]
...
[...]and here's what happens with this PR:
$ spack filter -h
==> Error: Unknown command: filter
$ spack -h
Traceback (most recent call last):
File "/home/mculpo/PycharmProjects/spack/bin/spack", line 48, in <module>
sys.exit(spack.main.main())
File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/main.py", line 662, in main
sys.stdout.write(parser.format_help(level=args.help))
File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/main.py", line 303, in format_help
return self.format_help_sections(level)
File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/main.py", line 166, in format_help_sections
add_all_commands(self)
File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/main.py", line 112, in add_all_commands
parser.add_command(cmd)
File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/main.py", line 286, in add_command
module = spack.cmd.get_module(cmd_name)
File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/cmd/__init__.py", line 123, in get_module
module = spack.extensions.get_module(cmd_name)
File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/extensions.py", line 112, in get_module
module = spack.cmd.get_module_from(cmd_name, pname)
File "/home/mculpo/PycharmProjects/spack/lib/spack/spack/cmd/__init__.py", line 99, in get_module_from
level=0)
ImportError: No module named filter.cmd.filterWas that done on purpose? In any case I think it's mostly my fault as I didn't add a unit test for a simple extension in #8612. Will provide a PR soon, so that if changes are needed somewhere for extension commands they'll be documented in tests.
| pname = spack.cmd.python_name(cmd_name) | ||
| extensions = spack.config.get('config:extensions') or [] | ||
| for folder in extensions: | ||
| module = load_command_extension(cmd_name, folder) |
There was a problem hiding this comment.
The function load_command_extension is not called anymore anywhere. In a comment you say:
spack.extensions.get_module() now uses import() (via spack.cmd.get_module_from()) rather than load_command_extension(), because the latter does not deal correctly with package-level globals, which are essential when the extension command so loaded has sub-commands.
Can you provide an example of this issue (or point me to a command that wouldn't work in the current state)? Asking because I don't fully understand the logic behind the need to add get_module_from and am wondering if we could solve the issue by modifying load_command_extension in some way.
There was a problem hiding this comment.
Imagine a command "dev," implemented in dev.py, which has sub-commands which are discovered at run time. The following code will not work when loaded with load_command_extension() because the package-global variable _subcmd_functions populated via calls to add_subcommand() is not available to the dev() function, so the selected sub-command cannot be invoked. This problem is resolved if dev.py is imported rather than loaded.
There was a problem hiding this comment.
"not available" is a little sloppy: the value initialized by one function is not available to the other.
|
The current implementation does impose the convention that top level command |
4f3b189 to
1b18471
Compare
|
From the commit message for 'Address assorted comments': (1b18471)
As a side effect of the above changes, the ability to have multiple command extensions per path has been restored. Also, a module load failure during |
1b18471 to
831f829
Compare
831f829 to
fa4102f
Compare
* New function spack.cmd.find_commands(), allowing for the location of subcommands. * New function spack.cmd.get_module_from() to allow subcommand module loading from alternative namespaces. * Improve error handling on missing, misspelled or broken command, including as part of `spack help`. If --debug is active, produce a stack trace. * Extension command paths are cached, including which commands are to be found at which path. * Corrected help text for --spec option (drive-by).
|
@alalazo Ping. |
fa4102f to
730b558
Compare
* New function spack.cmd.find_commands(), allowing for the location of subcommands. * New function spack.cmd.get_module_from() to allow subcommand module loading from alternative namespaces. * Improve error handling on missing, misspelled or broken command, including as part of `spack help`. If --debug is active, produce a stack trace. * Extension command paths are cached, including which commands are to be found at which path. * Corrected help text for --spec option (drive-by).
|
@alalazo @scheibelp Since PR #11209 has been merged into develop and this PR has been rebased against same, it is no longer blocked. |
This comment has been minimized.
This comment has been minimized.
scheibelp
left a comment
There was a problem hiding this comment.
Could you address:
- Overly-broad exception catching
- _extension_command_map and _command_paths seem redundant (more details in review comments)
- Access of _extension_command_path should be done via a function (more details in review comments)
Let me know if that seems unclear or disagreeable.
|
Also: could you update the PR description? I think the following bit is stale:
And to be clear I assume that the following is the primary intent (especially now that #11209 has been merged):
|
730b558 to
0f37716
Compare
* New function spack.cmd.find_commands(), allowing for the location of subcommands. * New function spack.cmd.get_module_from() to allow subcommand module loading from alternative namespaces. * Improve error handling on missing, misspelled or broken command, including as part of `spack help`. If --debug is active, produce a stack trace. * Extension command paths are cached, including which commands are to be found at which path. * Corrected help text for --spec option (drive-by).
|
@scheibelp I think I've addressed everything: please let me know if I missed something. While rationalizing the exception handling, I remove a handling block in |
1f87782 to
662f726
Compare
|
@alalazo @scheibelp Ping. |
1 similar comment
|
@alalazo @scheibelp Ping. |
|
@alalazo @scheibelp @adamjstewart @tldahlgren @becker33 If no-one has substantive issues remaining after my last answer to Massimiliano, I would like to push to have this merged. |
|
@chissg At a first glance I'm still of the opinion expressed in #11145 (comment) regarding
and what would be required on the To be clear: I'm not asking that you actually do the refactor, I just want to understand if that refactor would be at all possible and what would be the pain points. EDIT: Looking for a simple reproducer of your blockers was the intent of the mock extension created above. |
|
@alalazo Thank you for your response. In order to re-factor SpackDev to operate within the current extension model, the top level command would require explicit knowledge of every sub-command, including both a call to configure the parser and one or more calls to implement functionality. Subsequently extending SpackDev with new sub-commands would require the addition of (at minimum) two calls and an import to the top level command implementation (over and above the implementation of the sub-command module itself), vs no change whatsoever under this PR. The ability to encapsulate sub-commands entirely and avoid the necessity for this explicit knowledge of sub-commands in the implementation of top level commands is analogous to the existing ability with top level commands (the Spack application main code has no explicit knowledge of the "install"—or any other—commands, for instance) and as a result the implementation of this feature uses much the same code. We can discuss the specifics of how that is achieved in order to satisfy technical concerns you may have, but to date I have not seen an explicit argument from you against the idea of extending command encapsulation to sub-commands. Refactoring SpackDev to fit some different idea of where sub-command and other support code should be located either physically or within Python namespaces is practically trivial and can certainly be done. I am perfectly fine with enforcing the convention that commands and sub-commands be in a I believe there is a useful discussion to be had on the relative merits of explicit per-file loading of code modules vs Finally: with this PR in its eventual final form and merged upstream, I see the scope for a further PR providing the turnkey ability to add sub-commands from a designated path and/or namespace. I look forward to your comments on the technical aspects of this PR when you have had chance to review it in its current form. |
|
Apologies but in order to meet our next targeted release https://github.com/spack/spack/projects/10 I think my next review will be pushed out to 10/21. I have added a reminder at that time and I hope to get to this sooner but currently that's my estimate. |
|
@scheibelp Looks like quite the push! Thanks for the update. Looking forward to seeing you at SC19. |
|
@scheibelp @alalazo I'd like to plan to discuss this at the telcon Thursday with a view to understanding what needs to be done to get this PR merged. |
alalazo
left a comment
There was a problem hiding this comment.
I went through which modifications are needed to make SpackDev load commands using the current code in develop. A PR with them is here for reference.
It is my impression that most of the modifications in this PR are needed to support the custom layout of SpackDev and are not necessary if the extension is written according to the layout specified in the docs.
Therefore I advise against merging, since this PR touches a core part of Spack and being able to place code in arbitrary locations does not justify in my opinion the extension of these changes.
|
@alalazo @scheibelp To pick up from the discussion at FNALssi/spack-dev#5 (comment): #11145 contains several improvements that have nothing to do with subcommand encapsulation, and in its earlier form solved a problem that was subsequently solved in a different way by @alalazo's #11209—things might have looked a little different if #11209 had already existed (see issue 1 below). Allowing subcommand (etc.) discovery—while being originally motivated by a desire for encapsulation in SpackDev—has additional use cases. Fermilab's fork of Spack includes legacy support for the Fermilab-specific "UPS" package management system (FNALssi#21, FNALssi#23) in the form of two "module" types: With respect to the complexity of this PR:
So, the issues to consider:
Assuming we can agree:
then I believe it is possible to significantly simplify extension command and subcommand location / loading while avoiding the need to add to If I'm reading the various pieces of documentation correctly with respect to Python finders, it would appear that careful definition of a finder for def get_command_module(cmd_name):
spack.cmd.require_cmd_name(cmd_name)
try:
module = get_command_module_from(cmd_name, 'spack')
except ImportError:
module = get_command_module_from(cmd_name, 'spack.extensions')
return modulewhere the implementation of One can argue over whether a Spack extension command should be allowed to implement and preempt an internal Spack command, in which case the order of the Harmonizing the loading in this way also allows for a scheme for subcommand location and invocation along the lines of: def setup_parser(subparser):
sp = subparser.add_subparsers(metavar='SUBCOMMAND', dest='dev_command')
spack.command_loading.add_subcommands(subparser)
def dev(parser, args):
spack.command_loading.subcommand(args.dev_command)(parser, args)
One final advantage of harmonizing Spack command loading and extension command loading and providing the ability to discover modules in designated places is that it lays the groundwork for configuration and use of externally-provided build systems and module generators (and other Spack features) in addition to simply commands. After that wild ride, a proposal for discussion on telcon:
While harmonization of Spack command and extension command loading is not strictly necessary for the enablement of subcommand loading, it does seem to be indicated as independently desirable (via |
Can you write a brief bullet list of requirements for achieving "subcommand encapsulation"? I feel like we are using the same term for different things (as I believe in FNALssi/spack-dev#5 commands are as "encapsulated" as they were before under this PR). |
If you can extract the part that improves error messages and error handling without touching the command loading / storing mechanism I think that would be a lot less controversial to discuss and will probably be merged faster. |
Not sure what you mean above, but the broader explanation of why the comment is there can be found at #11209 (comment) |
👍 Can you point to the various features added besides the improvement in error-handling (see comment above) and the refactor of command loading? |
I think we agree on the definition, but my plan was always to eventually merge the bulk of the work done in spack-dev's top level command into a class or pair of functions in Spack core that could be accessed by any command, either an extension command or one within Spack core. I wanted to get some eyes on everything first and have things mature before abstracting out that code for a subsequent PR. I think this is an important point, because then we can consider:
With the three points above, I believe that the reasonable changes to Spack core to that would accommodate the generalization of the subcommand handling code from FNALssi/spack-dev#5 would start to look very similar to what would be in #11145 + generalization of the subcommand handling code from FNALssi/spack-dev + simplifications allowed by point (3). #11209 was an alternative implementation of part of #11145, rather than being an improvement to same growing out of a conversation and shared understanding of what SpackDev needed from an extension command system that #8612 did not provide. In a post-#11209 world, I realize that #11145 would look somewhat different than it does now but I was not successful in starting a conversation (regarding, among other things, point (3)) that would lead to those improvements. |
After re-reading #11209 (comment), I'd like to understand what is "not recommended" about |
|
I have 3 extensions enabled, config:
extensions:
- /home/culpo/PycharmProjects/spack-container
- /home/culpo/PycharmProjects/spack-testcommand
- /home/culpo/PycharmProjects/spack-devTiming on the current $ git status
On branch develop
Your branch is up to date with 'upstream/develop'.
nothing to commit, working tree clean
$ time spack help &> /dev/null
real 0m0,607s
user 0m0,533s
sys 0m0,058s
$ time spack commands &> /dev/null
real 0m0,448s
user 0m0,395s
sys 0m0,036s
$ time spack dev -h
usage: spack dev [-h] SUBCOMMAND ...
develop multiple Spack packages simultaneously
positional arguments:
SUBCOMMAND
getdeps install missing dependencies of packages in a SpackDev area
init initialize a spackdev area
info describe a spackdev area
build_env
run a command in the build environment of a spackdev package, or start a shell in same.
stage stage packages in a spackdev area
findext search for system packages and add to packages.yaml
optional arguments:
-h, --help show this help message and exit
real 0m0,414s
user 0m0,363s
sys 0m0,033sTiming on this branch: $ git status
On branch PR/11145
nothing to commit, working tree clean
$ time spack help &> /dev/null
real 0m0,607s
user 0m0,555s
sys 0m0,036s
$ time spack commands &> /dev/null
real 0m0,459s
user 0m0,414s
sys 0m0,028s
$ time spack dev -h
usage: spack dev [-h] SUBCOMMAND ...
develop multiple Spack packages simultaneously
positional arguments:
SUBCOMMAND
getdeps install missing dependencies of packages in a SpackDev area
init initialize a spackdev area
info describe a spackdev area
build_env
run a command in the build environment of a spackdev package, or start a shell in same.
stage stage packages in a spackdev area
findext search for system packages and add to packages.yaml
optional arguments:
-h, --help show this help message and exit
real 0m0,407s
user 0m0,358s
sys 0m0,031sOn my development box there's no sensible speed-up (nor any slow-down). Can you share your numbers? |
|
@alalazo Starting with |
|
Closing as stale (trying to clean up old PRs). Feel free to reopen if you think otherwise. |
Enable the encapsulation of sub-commands to top level Spack or extension commands, removing the necessity to explicitly add sub-command parser and implementation invocations to the top level command.
Implementation details.
New function
find_commands(*command_paths)to return all the commands found under the specified directories.all_commands()now uses this to obtain and cache the list of top level commands (including extension commands).In addition, there is a new function
get_command_module_from()to import a module from a specific namespace, abstracted fromget_module()(which has been renamed toget_command_module()). The higher level function continues to attempt to get commands from the corespacknamespace first, falling back toload_command_extension()on failure.When used together,
find_commands()andget_command_module_from()allow Spack commands (internal and extensions) to implement sub-commands (and sub-sub-commands, etc.) as fully-encapsulated standalone modules. As a result sub-commands and their options may be added to the parser configuration without the parent command having to be aware of each sub-command individually.A new exception,
CommandNotFoundError, ensures that Spack behaves sensibly when a command is misspelled or otherwise cannot be found, including forspack help.Functions utilizing both
spack.cmdandspack.extensionsfunctionality have been extracted tospack.command_loadingto avoid cyclic includes.