Thanks for this great review Benji. > ~~ Are there any commands that don't support dry-run? If so, could they be > made to in some trivial way so that some of the dry-run logic could be > removed? The only subcommand not supporting dry run is `version`. It's not diffcult to add a version subcommand in `cli.get_parser` (in the same way `help` is already added). Doing that, we can assume all the subcommands support dry run and totally remove the `has_dry_run` logic. I am inclined to create another card for this change, do you agree? > + - help: as an empty string. > > ~~ Would None be a better "nothing" value? Fixed. > > + - prepare_namespace(namespace): > + > + This base subcommand introduces the concept of handlers. Handlers are > + callables receiving the current namespace. They can manipulate the > + namespace adding or changing names. They can also validate the > + namespace, e.g. when the validation depends on multiple args. > + If something is wrong with the arguments, a handler should raise a > + *ValidationError*. > + > + Subcommands can store handlers in the *handlers* attribute as a > + sequence, or can dynamically set up handlers overriding the > + *get_handlers* method. > > ~~ Do we really need two ways? Which gets priority? How do they interact? By default, get_handlers returns self.handlers, so get_handlers gets priority. Subcommands can define handlers or override get_handlers if required (e.g.: when handlers must be dynamically resolved). > Is "get_handlers" part of the subcommand protocol? Should it be defined > above? No, it is something introduced by BaseSubCommand. The protocol does not know anything about handlers. BaseSubCommand implements prepare_namespace (part of the protocol) to make use of handlers. I improved the docstring to better describe the relation between handlers and get_handlers. > + - needs_root(namespace): > + > + returning *subcommand.root_required*, False by default. > + This way subcommands can just set up the *root_required* attribute > + if they don't need to calculate that value dynamically. > > ~~ Defining a method that returns a constant is pretty easy too. Maybe we > should just have the method. Brad, in his review, made me realize needs_root(namespace) is a YAGNI. I've removed that function and now we just use, if defined, `root_required`. I moved this name in the optional ones in the subcommands protocol. > + - get_description(namespace): returning *subcommand.help* > > ~~ Similar to the above: should we just have the method and not an attribute > too? get_description returning help generates confusion. They have different pourposes: help is used in `lp-setup subcommand -h` (or `lp-setup help subcommand`); get_description is used to warn the user during interactive or dry run executions. I've changed the default implementation to just return an empty string: this way I hope to avoid the confusion. > It is too bad the namespace is mutated as we go, otherwise we could pass it > into the subcommand at construction time and then these could be properties > instead. The namespace should only be mutated by prepare_namespace. get_description takes the namespace to retrieve data from it, and should not modify the namespace. I've added warnings in the protocol description. We could programmatically deny namespace changes once prepare_namespaxce is executed, but I'd prefer to just mention this restriction in the contract. > + - steps: > + > + a sequence of *(callable, arg_names, filter-callable)* where: > + > + - *arg_names* are the names of namespace attributes, used to retrieve > + the corresponding values and generate a tuple of *args* to be > + passed to the step > + - callable is a callable step that will be run passing collected > *args* > + - *filter-callable* is an OPTIONAL function that takes the namespace > + and returns True if the step must be run, False otherwise. > + If not provided, the step is run. > > ~~ The elements of the record should be define in order ("callable" is listed > second, it should be listed first). Fixed. > step[2](namespace) if len(step) == 3 else True > > ~~ It may be my general aversion to Python's ternary operator, but the above > line takes some effort to parse. Perhaps aliasing step[2] with a name like > "step_guard" or "should_run_step" would help. On the other hand, the > "granted" variable is only referenced in the next line. Right. > Maybe we should make the step > tuple always contain three elements (some of the complexity comes from making > the guard optional), so maybe replacing the above and below with something > like this would be better: Done. I've used map to ensure each step_sequence has 3 items, and then modified *resolve_steps* as you suggested. Now it should be more readable. > def get_terminal_width(): > > ~~ If COLUMNS is 0 stty isn't tried. Maybe this would be nicer: Also commenting the suggestion from Gary (try stty first). I know stty is most likely to be right, and that's how I originally coded *get_terminal_width*. This is the logic that suggested me to re-implement the function as is: Current implementation:: def get_terminal_width(): """Return the terminal width (number of columns).""" default = 79 # Try using the environment variable $COLUMNS. try: width = int(os.getenv('COLUMNS')) except (TypeError, ValueError): # $COLUMNS not exported or not a number: try using `stty`. try: width = int(run('stty', 'size').split()[1]) except subprocess.CalledProcessError: # No input available for stty, return a default value. return default return width if width > 0 else default $COLUMNS is usually not exported, so it can not be found in os.environ. In this cases, a TypeError is raised, so 90% of the times stty is used (if possible). If the user explicitly wants to wrap at some size, he can explicitly pass $COLUMNS, e.g.: COLUMNS=60 lp-setup init-host ... In this case we let the user override the value calculated by stty (as is done, for example, by dpkg). If COLUMNS is not a number, a ValueError is raised and stty is used as well. if COLUMNS is 0, well, then the user deserves 79. How does it sounds?