diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 240b669..f610a72 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - click-version: ["4.1", "5.1", "6.7", "7.1.2", "8.1.6"] + click-version: ["8.1.3", "8.1.7", "7.1.2", "6.7", "5.1"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: @@ -20,10 +20,15 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install '.[test'] + pip install '.[test]' pip install click==${{ matrix.click-version }} - name: Test with pytest run: pytest + - name: Check with flake8 + run: flake8 click_default_group.py test.py -v --show-source + - name: Check type hints + if: ${{ startsWith( matrix.click-version, '8.' ) }} + run: python3 -m mypy --follow-imports=silent --python-version "${{ matrix.python-version }}" --strict --implicit-reexport -- click_default_group.py build: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') diff --git a/click_default_group.py b/click_default_group.py index 735c54b..a52eda8 100644 --- a/click_default_group.py +++ b/click_default_group.py @@ -43,11 +43,12 @@ def bar(): bar """ + +import typing as t import warnings import click - __all__ = ['DefaultGroup'] __version__ = '1.2.4' @@ -61,77 +62,109 @@ class DefaultGroup(click.Group): """ - def __init__(self, *args, **kwargs): + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: # To resolve as the default command. if not kwargs.get('ignore_unknown_options', True): raise ValueError('Default group accepts unknown options') self.ignore_unknown_options = True self.default_cmd_name = kwargs.pop('default', None) self.default_if_no_args = kwargs.pop('default_if_no_args', False) - super(DefaultGroup, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) - def set_default_command(self, command): + def set_default_command(self, command: t.Any) -> None: """Sets a command function as the default command.""" cmd_name = command.name self.add_command(command) self.default_cmd_name = cmd_name - def parse_args(self, ctx, args): + def parse_args(self, ctx: click.core.Context, args: t.List[str]) -> t.List[str]: if not args and self.default_if_no_args: args.insert(0, self.default_cmd_name) - return super(DefaultGroup, self).parse_args(ctx, args) + return super().parse_args(ctx, args) - def get_command(self, ctx, cmd_name): + def get_command( + self, ctx: click.core.Context, cmd_name: str + ) -> t.Optional[click.core.Command]: if cmd_name not in self.commands: # No command name matched. - ctx.arg0 = cmd_name + ctx.arg0 = cmd_name # type: ignore cmd_name = self.default_cmd_name - return super(DefaultGroup, self).get_command(ctx, cmd_name) + return super().get_command(ctx, cmd_name) - def resolve_command(self, ctx, args): - base = super(DefaultGroup, self) - cmd_name, cmd, args = base.resolve_command(ctx, args) - if hasattr(ctx, 'arg0'): + def resolve_command( + self, ctx: click.core.Context, args: t.List[str] + ) -> t.Tuple[t.Optional[str], t.Optional[click.core.Command], t.List[str]]: + cmd_name, cmd, args = super().resolve_command(ctx, args) + if cmd and hasattr(ctx, 'arg0'): args.insert(0, ctx.arg0) cmd_name = cmd.name return cmd_name, cmd, args - def format_commands(self, ctx, formatter): - formatter = DefaultCommandFormatter(self, formatter, mark='*') - return super(DefaultGroup, self).format_commands(ctx, formatter) - - def command(self, *args, **kwargs): - default = kwargs.pop('default', False) - decorator = super(DefaultGroup, self).command(*args, **kwargs) + def format_commands( + self, ctx: click.core.Context, formatter: click.formatting.HelpFormatter + ) -> None: + new_formatter = DefaultCommandFormatter(self, formatter, mark="*") + return super().format_commands(ctx, new_formatter) + + @t.overload + def command(self, __func: t.Callable[..., t.Any]) -> click.core.Command: + ... + + @t.overload + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], click.core.Command]: + ... + + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Union[ + t.Callable[[t.Callable[..., t.Any]], click.core.Command], click.core.Command + ]: + default = kwargs.pop("default", False) + decorator: t.Callable[[t.Callable[..., t.Any]], click.core.Command] = ( + super().command(*args, **kwargs) + ) if not default: return decorator - warnings.warn('Use default param of DefaultGroup or ' - 'set_default_command() instead', DeprecationWarning) + warnings.warn( + "Use default param of DefaultGroup or set_default_command() instead", + DeprecationWarning, + ) - def _decorator(f): - cmd = decorator(f) + def _decorator(f: t.Callable[..., t.Any]) -> click.core.Command: + cmd: click.core.Command = decorator(f) self.set_default_command(cmd) return cmd return _decorator -class DefaultCommandFormatter(object): +class DefaultCommandFormatter(click.formatting.HelpFormatter): """Wraps a formatter to mark a default command.""" - def __init__(self, group, formatter, mark='*'): + def __init__( + self, + group: DefaultGroup, + formatter: click.formatting.HelpFormatter, + mark: str = "*", + ): self.group = group self.formatter = formatter self.mark = mark - def __getattr__(self, attr): + super().__init__() + + def __getattr__(self, attr: str) -> t.Any: return getattr(self.formatter, attr) - def write_dl(self, rows, *args, **kwargs): - rows_ = [] - for cmd_name, help in rows: + def write_dl( + self, rows: t.Sequence[t.Tuple[str, str]], *args: t.Any, **kwargs: t.Any + ) -> None: + rows_: t.List[t.Tuple[str, str]] = [] + for cmd_name, text in rows: if cmd_name == self.group.default_cmd_name: - rows_.insert(0, (cmd_name + self.mark, help)) + rows_.insert(0, (cmd_name + self.mark, text)) else: - rows_.append((cmd_name, help)) + rows_.append((cmd_name, text)) return self.formatter.write_dl(rows_, *args, **kwargs) diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml index 8f3772d..18a8978 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,11 +12,7 @@ classifiers = [ "Intended Audience :: Developers", "License :: Public Domain", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -25,7 +21,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -requires-python = ">=2.7" +requires-python = ">=3.7" dependencies = ["click"] dynamic = ["version", "description"] @@ -33,4 +29,4 @@ dynamic = ["version", "description"] Source = "https://github.com/click-contrib/click-default-group" [project.optional-dependencies] -test = ["pytest"] +test = ["pytest", "mypy", "flake8", "flake8-import-order", "pytest-cov", "coveralls"] diff --git a/setup.cfg b/setup.cfg index f5ca88b..e1d7f5e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,7 @@ ignore = E301 import-order-style = google application-import-names = click_default_group +max-line-length = 88 [tool:pytest] python_files = test.py