Skip to content

Two resolution/naming inconsistencies: hyphenate_groups ignored by add_commands(group_name=), and resolve_to_function rejects the documented 'pkg.mod:name' form #40

Description

@thorwhalen

Found while migrating thorwhalen/priv (101 commands, four groups) — the last of the wave.

1. convention.hyphenate_groups is silently ignored by add_commands(group_name=...)

A group's name is hyphenated when it is derived from a mapping key, and passed through verbatim when it is given as group_name=. cw.MODERN therefore means two different things depending on which of the two documented spellings of "a group" you used:

>>> import cw
>>> def status(): ...
>>> list(cw.commands_from({'git_ops': [status]}, convention=cw.MODERN))
['git-ops']

>>> parser = cw.mk_parser({'info': status}, prog='priv')
>>> _ = cw.add_commands(parser, [status], group_name='git_ops', convention=cw.MODERN)
>>> 'git_ops' in parser._subparsers._group_actions[0].choices
True          # not 'git-ops'

cli.py passes group_name straight to the subparser adder; it never reaches _named. commands_from runs its key through _named(key, convention=convention, group=True).

Why it matters. add_commands(group_name=...) is the recommended shape for exactly the repos that have both top-level commands and groups — which is to say the big ones. A repo that migrates to cw.MODERN on the strength of ADR/README text saying groups get hyphenated will silently keep its underscored group names, and the parity harness will report identical because the CLI genuinely did not change. The failure mode is a convention that quietly does not apply, which is worse than one that errors.

For priv this happened to be the outcome we wanted (priv git_ops is a literal string in three docs), so it cost nothing — but we only found out by flipping to MODERN as a canary and watching the group name not move while the flag names did.

Suggested fix, in rough order of preference:

  1. Run group_name through _named(..., group=True) like a mapping key, so one convention means one thing. This is a behaviour change for anyone on MODERN + group_name=, which today is nobody who noticed.
  2. If verbatim is the intended contract for an explicitly-passed name, say so in the add_commands docstring next to group_name, and add a doctest pinning it — right now hyphenate_groups's own docstring is the only place a reader learns the rule and it does not mention the exception.

Either way the two paths should be documented as agreeing or documented as differing. Silently differing is the one option that isn't fine.

2. cw.resolve_to_function rejects the 'pkg.mod:name' form that commands_from documents

commands_from's docstring (and mk_parser's obj: parameter doc) both advertise 'pkg.mod:name' as a supported spelling. So, wanting to defer four imports, I reached for the public resolver with the same string:

>>> import cw
>>> cw.resolve_to_function('priv.git_ops:dispatch_funcs')
Traceback (most recent call last):
  ...
ValueError: func_spec must contain only word characters and dots: 'priv.git_ops:dispatch_funcs'

commands_from resolves via cw.commands.import_object, which takes the colon form; cw.resolve_to_function goes through parse_spec_with_dot_path, which takes only dots. Both are public, both are "turn a string into a function", and they accept different grammars — with the one that's exported in __all__ and named most generically being the stricter of the two.

The error is at least loud rather than wrong, but it names a rule (word characters and dots) without naming the resolver that does take the string you have. Either accept the colon form in resolve_to_function (delegating to import_object when a colon is present), or have the message say "for the pkg.mod:name form, use cw.commands.import_object".


Neither of these affected priv's result — thorwhalen/priv#118 replayed 111/111 cases identical under --strict-help, the whole 101-command surface, no diff at all. Recording that here because the migration wave is what these were found in.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions