From 0300d81bfb458ff3d51d3a13cc9555052211f24c Mon Sep 17 00:00:00 2001 From: John Newbigin <john.newbigin@rea-group.com> Date: Wed, 14 Feb 2024 16:47:45 +1100 Subject: [PATCH 1/8] Add type hints and related changes to get mypy to pass --- click_default_group.py | 87 +++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 30 deletions(-) diff --git a/click_default_group.py b/click_default_group.py index 735c54b..ebbb06f 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,103 @@ 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) - 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) From 7383622eae149ce75e4564ebf33cf3462289d4da Mon Sep 17 00:00:00 2001 From: John Newbigin <john.newbigin@rea-group.com> Date: Wed, 14 Feb 2024 16:48:23 +1100 Subject: [PATCH 2/8] Add marker to indicate type hints --- py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 py.typed diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..e69de29 From c8fb5ae2e2a6c3485d950832901cdc0d8631df06 Mon Sep 17 00:00:00 2001 From: John Newbigin <john.newbigin@rea-group.com> Date: Wed, 14 Feb 2024 16:58:26 +1100 Subject: [PATCH 3/8] Add mypy to the github actions workflow --- .github/workflows/build.yaml | 4 +++- click_default_group.py | 6 ++++-- pyproject.toml | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 240b669..eef1887 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -20,10 +20,12 @@ 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 type hints + 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 ebbb06f..adfe4a4 100644 --- a/click_default_group.py +++ b/click_default_group.py @@ -123,8 +123,10 @@ def command( 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: t.Callable[..., t.Any]) -> click.core.Command: cmd: click.core.Command = decorator(f) diff --git a/pyproject.toml b/pyproject.toml index 8f3772d..67a77f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,4 +33,4 @@ dynamic = ["version", "description"] Source = "https://github.com/click-contrib/click-default-group" [project.optional-dependencies] -test = ["pytest"] +test = ["pytest", "mypy"] From c10c413d3c6cd6b886c0f578f873af40d77a4960 Mon Sep 17 00:00:00 2001 From: John Newbigin <john.newbigin@rea-group.com> Date: Wed, 14 Feb 2024 17:02:39 +1100 Subject: [PATCH 4/8] Limit the click versions to check types on --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index eef1887..767b234 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"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: From b00df0bfa06e017addd347f9ee0580b312472324 Mon Sep 17 00:00:00 2001 From: John Newbigin <john.newbigin@rea-group.com> Date: Thu, 15 Feb 2024 09:29:45 +1100 Subject: [PATCH 5/8] Add flake8 to github actions workflow --- .github/workflows/build.yaml | 2 ++ click_default_group.py | 10 +++++++--- pyproject.toml | 2 +- setup.cfg | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 767b234..3dbc7ec 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -24,6 +24,8 @@ jobs: 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 run: python3 -m mypy --follow-imports=silent --python-version "${{ matrix.python-version }}" --strict --implicit-reexport -- click_default_group.py diff --git a/click_default_group.py b/click_default_group.py index adfe4a4..a52eda8 100644 --- a/click_default_group.py +++ b/click_default_group.py @@ -107,12 +107,14 @@ def format_commands( return super().format_commands(ctx, new_formatter) @t.overload - def command(self, __func: t.Callable[..., t.Any]) -> click.core.Command: ... + 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]: ... + ) -> t.Callable[[t.Callable[..., t.Any]], click.core.Command]: + ... def command( self, *args: t.Any, **kwargs: t.Any @@ -120,7 +122,9 @@ def command( 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) + decorator: t.Callable[[t.Callable[..., t.Any]], click.core.Command] = ( + super().command(*args, **kwargs) + ) if not default: return decorator warnings.warn( diff --git a/pyproject.toml b/pyproject.toml index 67a77f0..513ab68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,4 +33,4 @@ dynamic = ["version", "description"] Source = "https://github.com/click-contrib/click-default-group" [project.optional-dependencies] -test = ["pytest", "mypy"] +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 From a4eb72bd462e4ef6ff37f7c71f55ebf521c9c82c Mon Sep 17 00:00:00 2001 From: John Newbigin <john.newbigin@rea-group.com> Date: Thu, 15 Feb 2024 09:30:42 +1100 Subject: [PATCH 6/8] We are not testing for these python versions so let's not advertise that they are supported --- pyproject.toml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 513ab68..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"] From aa5ab61e460cdf488a9b17c01cb05ea0052e6dca Mon Sep 17 00:00:00 2001 From: John Newbigin <john.newbigin@rea-group.com> Date: Thu, 15 Feb 2024 09:34:04 +1100 Subject: [PATCH 7/8] Re-test on older versions of click --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3dbc7ec..db6449e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - click-version: ["8.1.3", "8.1.7"] + 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: From 94d6e7e9a988915c6eccdd458b418d37640e586a Mon Sep 17 00:00:00 2001 From: John Newbigin <john.newbigin@rea-group.com> Date: Thu, 15 Feb 2024 09:43:35 +1100 Subject: [PATCH 8/8] Only type check click version 8 --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index db6449e..f610a72 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,6 +27,7 @@ jobs: - 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: