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

WIP: First attempt to implement an interactive mode #325

Closed
wants to merge 10 commits into from

Conversation

FlorianWilhelm
Copy link
Member

@FlorianWilhelm FlorianWilhelm commented Aug 23, 2020

@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 :-)

@FlorianWilhelm FlorianWilhelm added the enhancement New feature or request label Aug 23, 2020
@FlorianWilhelm FlorianWilhelm changed the title First try for an interactive mode First attempt to implement an interactive mode Aug 23, 2020
@FlorianWilhelm FlorianWilhelm changed the title First attempt to implement an interactive mode WIP: First attempt to implement an interactive mode Aug 23, 2020
Copy link
Collaborator

@abravalheri abravalheri left a 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 the action.option_strings correspond to any of the ext.flags (since not always action.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 overwrite add_argument_group, add_mutually_exclusive_group and add_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.


An extension can include another using :obj:`~pyscaffold.extensions.include`

FIXME: This still doesn't work!
Copy link
Collaborator

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)

Copy link
Member Author

@FlorianWilhelm FlorianWilhelm Dec 20, 2020

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

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?

Copy link
Member Author

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?

Copy link
Collaborator

@abravalheri abravalheri Dec 20, 2020

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

@@ -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"]:
Copy link
Collaborator

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Smart, really smart!

"-V",
"--version",
action="version",
noprompt=True,
Copy link
Collaborator

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?

Copy link
Member Author

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

@abravalheri
Copy link
Collaborator

By the way, I really liked the way the interactive mode teaches the user how to use putup... By leaving the flag strings explicitly there, the more the users that are not familiar with the CLI use --interactive, the more they end up (passively) learning the parameters in the CLI, and more likely they are to start typing the arguments directly on the command line...

This reminds me a bit magit (one of my favourites interactive interfaces) and how involuntarily people discovers new git switches...

@abravalheri
Copy link
Collaborator

abravalheri commented Nov 10, 2020

That being said, an alternative to interactive could be editable...

Imagine that instead of an CLI constantly querying the user, we spawn $EDITOR with an text file with contents similar to putup -h, where users can uncomment flags / review the parameters that they might have already typed directly as arguments. When the file is closed putup runs with it's contents (a kind of 2 passes system, first we preprocess things with cli.parse_args, then we compose the file based on [(a.option_strings, a.help) for a in parser._actions] and leverage argparse's fromfile_prefix_chars and convert_arg_line_to_args to parse the tempfile a second time?).

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 ArgumentParser this way?)

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

@FlorianWilhelm FlorianWilhelm Dec 20, 2020

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?

Copy link
Collaborator

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

Copy link
Collaborator

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

@abravalheri abravalheri deleted the branch v4.0.x December 29, 2020 20:46
@abravalheri
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants