-
-
Notifications
You must be signed in to change notification settings - Fork 185
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
WIP: First attempt to implement an interactive mode #325
Conversation
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.
Wow, it is very impressive @FlorianWilhelm. Thank you very much for putting so much energy on this, and sorry for the time it took me to review it 😢
It is a real pity that the argparse
lib is not made with extensibility in mind, and that it requires a huge amount of complexity to do anything... For example, I was wondering why we need to override the add_argument_group
method, just to realize that argparse
uses it behind the scenes to implement positional arguments (even if we don't call the method directly)... I can only imagine all the trouble you went to figure out those things. Very good job.
Some minor comments:
- I feel like
noprompt=True
is a bit over a roundtrip in the brain, would it be better to have:prompt=False
? - In the
is_included
maybe a way of making the function work is to check is theaction.option_strings
correspond to any of theext.flag
s (since not alwaysaction.const
will be available). - I suppose we can leverage the
command
value of the parsed opts to avoid conditionally checking for the interactive mode...
I was trying to analyse what are the causes of the complexity in the parser, and attempting to find ways of simplifying it... For me, it seems that the complexity boils down to the following points:
- In order to cover
argparse
's API that are currently exposed to PyScaffold's users we need to overwriteadd_argument_group
,add_mutually_exclusive_group
andadd_argument
- To keep track of the actions already processed, we need to overwrite the code that creates the actions, and also wrap them.
In the end, these 2 points lead to all the problems of dealing with mixins and having to re-implement some internal parts of argparse
.
I wonder if we can avoid the mixins if we simply refuse to support some parts of the argparse
API in interactive mode? More specifically: can we simplify the cli_parser
file, if the only commit to support add_argument
in the interactive mode? (I have already remove the mutually exclusive group between the django and cookiecutter extensions for v4 anyway, since I always find it a lacky abstraction... For pretend
and list_actions
we can just use the last one as winner, I don't think it would be a non-intuitive outcome...)
Another thought is that we might be able to also side step the need of tracking the actions that were already processed if we find a different way of checking it... What if we check the original arg list for the option_strings
? Would it be problematic?
I am putting together a proof of concept for these ideas and I hope to submit a PR to your branch soon, so you can have a better understanding of what I am trying to say. Please feel free to use just the parts you like, or even ignore.
src/pyscaffold/cli_parser.py
Outdated
|
||
An extension can include another using :obj:`~pyscaffold.extensions.include` | ||
|
||
FIXME: This still doesn't work! |
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 by checking action.options_string
and ext.flag
instead of trying to get action.const
? (the later is not guaranteed to exist)
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.
Right, I think checking ext.flag
is way smarter :-)
return False | ||
|
||
|
||
def merge_user_input(opts: ScaffoldOpts, input: Any, action: Action) -> ScaffoldOpts: |
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 suppose we need to convert the opts back to a namespace object, pipe it to the Action __call__
and then convert it back to a dict... Should we try to do this before converting the namespace object into a dictionary to avoid going back and forth?
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 have always found the namespace object of argparse
quite annoying as it creeps everywhere into your code if you are not careful and it feels not as natural as a dictionary, which is expected by many other libraries. So my first intuition would be to draw the line at the interface between argparse and Pyscaffold where the conversion should take place. So PyScaffold only deals with dictionaries everywhere and only when argparse
functionality is called, it's converted to a namespace object. Or would you have an elegant solution to keep handling the namespace object to a minimum, so that it doesn't creep everywhere into the codebase?
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, that makes sense, and we don't have to be super efficient anyway...
I suppose merge_user_input
, would be something along the lines(?) :
def merge_user_input(parser: ArgumentParser, opts: ScaffoldOpts, input: Any, action: Action) -> ScaffoldOpts:
namespace = argparse.Namespace(**opts)
action(parser, namespace, input, action.option_strings[-1]) # the last argument could also be omitted I suppose
return vars(namespace)
The action should mutate the namespace as a side effect, so I suppose this will automatically merge the existing opts with any augmentation coming from the action...
src/pyscaffold/cli.py
Outdated
@@ -169,7 +185,11 @@ def parse_args(args: List[str]) -> ScaffoldOpts: | |||
extension.augment_cli(parser) | |||
|
|||
# Parse options and transform argparse Namespace object into common dict | |||
return _process_opts(vars(parser.parse_args(args))) | |||
opts = vars(parser.parse_args(args)) | |||
if opts["interactive"]: |
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 suppose we can avoid branching here by relying on the dynamic behaviour of dest="command"
for the-i
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.
Smart, really smart!
src/pyscaffold/cli.py
Outdated
"-V", | ||
"--version", | ||
action="version", | ||
noprompt=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.
Would prompt=False
be better than noprompt=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.
Absolutely, totally agree! What was I thinking? Normally I try to avoid double negations :-)
By the way, I really liked the way the interactive mode teaches the user how to use This reminds me a bit magit (one of my favourites interactive interfaces) and how involuntarily people discovers new git switches... |
That being said, an alternative to interactive could be editable... Imagine that instead of an CLI constantly querying the user, we spawn I wonder how these ergonomics/UX would compare to the interactive mode and how much we can reduce in terms of complexity (e.g. do we even need to overload |
@@ -196,6 +197,10 @@ def __init__( | |||
self.formatter = formatter or ReportFormatter() | |||
super(ReportLogger, self).__init__(self._wrapped, self.extra) | |||
|
|||
level = environ.get("PYSCAFFOLD_LOG_LEVEL") |
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.
@abravalheri Should we document this environment variable PYSCAFFOLD_LOG_LEVEL
somewhere or is it meant for development usage only?
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 used this to debug some errors that I was seeing and decided to leave as it is, supposing it could be useful in the future (specially when debugging things that happen before the CLI processes the verbosity level)...
I suppose final users should not have a direct usage for it... We could include it in the log
module documentation but I would prefer not to commit on this being part of the public API (just an internal API, for PyScaffold developers only).
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 we should revive this PYSCAFFOLD_LOG_LEVEL env var...
# Conflicts: # setup.cfg # src/pyscaffold/extensions/__init__.py
Merge v4.x into interactive
Sorry the PR got automatically closed when I merged the v4 branch on master and deleted the branch, but it is still valid 😅 I understand that the current idea is to make the "editable" the new "interactive", because it is less complex. Once I finish testing and checking for bus in the editable, if it is still less complex we can adopt that for v4. If it turns up to be as complex, we can try again the code in the interactive branch. |
@abravalheri: This is the first attempt to add an interactive mode to PyScaffold as described in #191. It was just way more complicated as I thought and took quite a while to only get a minimal version running. Please check it out if you find some time and give me your most honest feedback ;-) There is still a lot to do and I am not 100% sure if this is the way to go as the
inquirer
has quite some rough edges and combining command-line options with user prompts is quite difficult if the number of parameters depends on an unknown number of installed extensions.That being said, a minimal version works and there are some ToDos in the source code. Let me know what you think. I am on another trip the next 3-4 days, so if you want to work on this branch, feel free to do so :-)