Skip to content

Conversation

@anselor
Copy link
Contributor

@anselor anselor commented Jun 15, 2020

  • Group commands in CommandSets to be installed/uninstalled as a batch. Commands within a CommandSet can be optionally tagged with at default category. Individual commands may override the default category.
  • Install/Uninstall individual command functions at runtime
  • Tag command functions for automatic discovery and load
  • Automatic discovery of complete_ and help_ functions that are contained in the same module as command functions.
  • Automatic discovery and load of CommandSets and registered command functions that can be disabled in the constructor

@anselor anselor requested review from kmvanbrunt and tleonhardt June 15, 2020 16:52
@anselor anselor requested a review from kotfu as a code owner June 15, 2020 16:52
@codecov
Copy link

codecov bot commented Jun 15, 2020

Codecov Report

Merging #945 into master will decrease coverage by 0.05%.
The diff coverage is 96.13%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #945      +/-   ##
==========================================
- Coverage   97.71%   97.66%   -0.06%     
==========================================
  Files          17       18       +1     
  Lines        4155     4318     +163     
==========================================
+ Hits         4060     4217     +157     
- Misses         95      101       +6     
Impacted Files Coverage Δ
cmd2/cmd2.py 96.73% <94.96%> (-0.17%) ⬇️
cmd2/__init__.py 92.00% <100.00%> (+0.33%) ⬆️
cmd2/command_definition.py 100.00% <100.00%> (ø)
cmd2/utils.py 98.25% <0.00%> (+0.24%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update fa92771...25d6ed4. Read the comment docs.

@tleonhardt
Copy link
Member

On my laptop I'm getting the following error trying to build the Sphinx docs using Python 3.8.2 and Sphinx 3.1.1:

  File "/Users/toddleonhardt/src/cmd2/.venv/lib/python3.8/site-packages/sphinx/util/logging.py", line 421, in filter
    raise exc
sphinx.errors.SphinxWarning: /Users/toddleonhardt/src/cmd2/cmd2/cmd2.py:docstring of cmd2.Cmd::py:class reference target not found: cmd2.command_definition.CommandSet

Copy link
Member

@tleonhardt tleonhardt left a comment

Choose a reason for hiding this comment

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

I took a quick look, but I need to take a more in-depth look later.

This is a significant change.

@kmvanbrunt @kotfu I would appreciate feedback from both of you on this one as well.

@anselor
Copy link
Contributor Author

anselor commented Jun 16, 2020

On my laptop I'm getting the following error trying to build the Sphinx docs using Python 3.8.2 and Sphinx 3.1.1:

  File "/Users/toddleonhardt/src/cmd2/.venv/lib/python3.8/site-packages/sphinx/util/logging.py", line 421, in filter
    raise exc
sphinx.errors.SphinxWarning: /Users/toddleonhardt/src/cmd2/cmd2/cmd2.py:docstring of cmd2.Cmd::py:class reference target not found: cmd2.command_definition.CommandSet

That's weird. I ran sphinx on mine and didn't have any problems. I'm running Ubuntu 20.04 through WSL with the latest. I'll have to check when I get back on that machine what the versions were.

Copy link
Member

@tleonhardt tleonhardt left a comment

Choose a reason for hiding this comment

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

@anselor I started giving this code an in-depth review and have made a number of comments and suggestions for change. I sincerely apologize for not getting to this PR sooner - life has been busy.

Overall this adds a feature that I've thought for a long time we need a good example to show developers how to do it - namely how to define commands in separate files and/or dynamically load commands at runtime.

However, it adds a lot of complexity and does so in a way I fear may be challenging for others to maintain. A few thoughts that passed through my mind while reviewing include the following:

  1. This could probably be done more simply
  2. There is more than one way to achieve this and I'm not sure we should pick one way as the "right" way
  3. This a great excellent example for users with very nice work put into test, a good working example, and documentation ... but would it more appropriately exist as a cmd2 extension akin to cmd2-ext-test than as a core part of cmd2?

I'm not sure what the right answers are? What are your thoughts?

@kmvanbrunt and @kotfu If either of you are available to review this PR and/or weigh in with your opinion, I would very much appreciate it.

auto_load_commands: bool = True) -> None:
"""An easy but powerful framework for writing line-oriented command
interpreters. Extends Python's cmd package.

Copy link
Member

Choose a reason for hiding this comment

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

It is critical that you document these new parameters in the docstring below because that is what populates the Sphinx documentation.

shortcuts=shortcuts)

# Load modular commands
self._installed_functions = [] # type: List[str]
Copy link
Member

Choose a reason for hiding this comment

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

Is this the right data structure? More importantly does it need to exist at all?

The only place this is used is in install_command_functiion() where it is appended to and in uninstall_command() where it is checked to see if the specified command name is in this list?

If we keep it, it should be a set so that there is fast O(1) test for membership.

However, it looks like it is very weakly used and I'm not entirely sure if it is necessary.


# Load modular commands
self._installed_functions = [] # type: List[str]
self._installed_command_sets = [] # type: List[CommandSet]
Copy link
Member

Choose a reason for hiding this comment

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

This too should be a set for the fast membership test since it is only ever used to see if something is in this container.

It is also weakly used and I'm not sure it is strictly necessary.

def _autoload_commands(self) -> None:
"""
Load modular command definitions.
: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.

Specifying :return: in docstring is redundant when using type hints and should not be done unless you are providing more context on what is returned.

This comment applies throughout this PR

@@ -0,0 +1,127 @@
#!/usr/bin/env python
Copy link
Member

Choose a reason for hiding this comment

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

You forgot to make this file executable - make sure to chmod it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's weird. It shows up as executable for me.



class WithCommandSets(Cmd):
def __init__(self, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

Please don't follow this anti-pattern of just using *args, **kwargs. It makes code harder to read and maintain. Please explicitly list out the args you care about, e.g. command_sets. All arguments to the parent class __init__() are optional so it is safe to pass in the one you care about.


from cmd2 import Cmd, Cmd2ArgumentParser, CompletionItem, with_argparser
from cmd2.utils import CompletionError, basic_complete
from modular_commands.commandset_basic import BasicCompletionCommandSet # noqa: F401
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 documentation at the top of the example of how BasicCompletionCommandSet gets loaded is in order because it probably isn't intuitive to someone just look at this example.

@@ -0,0 +1,121 @@
# coding=utf-8
Copy link
Member

Choose a reason for hiding this comment

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

Creating an empty init.py file in this directory might be a good enough hack to get the examples to run but probably not to work nicely in an IDE

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, the layout of the examples doesn't really support this very well.

@@ -23,6 +23,7 @@
# Get the current value for argparse_custom.DEFAULT_ARGUMENT_PARSER
Copy link
Member

Choose a reason for hiding this comment

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

Before merging in this would need a CHANGELOG entry for version 1.2.0 or whatever.

anselor and others added 13 commits July 18, 2020 16:41
Issue #943

New class CommandSet can be used to tag a class as a command class. If
the constructor is simple, the object will automatically be instantiated
and loaded.
New register_command decorator to tag any arbitrary function as a
command.
…oad. Added unit tests. Moved installing commands into separate functions that can be called

Issue #943
…ding command functions

Adds handling of some edge cases. More thorough test coverage.
…e are scenarios where inspect.ismethod() fails for some reason
@anselor
Copy link
Contributor Author

anselor commented Aug 4, 2020

Accepting #961 instead.

@anselor anselor closed this Aug 4, 2020
@anselor anselor deleted the commandset branch August 4, 2020 17:39
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.

3 participants