From 984efea8430023f60929eee62070c307ae10d01f Mon Sep 17 00:00:00 2001 From: "John T. Wodder II" Date: Mon, 9 Oct 2023 13:22:39 -0400 Subject: [PATCH 1/3] Drop support for Click 6 and 7 --- .github/workflows/test.yml | 2 -- CHANGELOG.md | 1 + README.rst | 7 +++---- setup.cfg | 2 +- src/click_loglevel.py | 4 ++-- tox.ini | 4 +--- 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc6fa6a..900b16e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,8 +30,6 @@ jobs: - 'pypy-3.9' - 'pypy-3.10' toxenv: - - py-click6 - - py-click7 - py-click8 include: - python-version: '3.7' diff --git a/CHANGELOG.md b/CHANGELOG.md index f95808d..e8b9531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ v0.5.0 (in development) ----------------------- - Support Python 3.10, 3.11, and 3.12 - Drop support for Python 3.6 +- Require Click >= 8.0 v0.4.0.post1 (2021-06-05) ------------------------- diff --git a/README.rst b/README.rst index 2289bce..2ed60ed 100644 --- a/README.rst +++ b/README.rst @@ -33,8 +33,8 @@ the ``logging`` log level names (``CRITICAL``, ``ERROR``, ``WARNING``, into their corresponding numeric values. It also accepts integer values and leaves them as-is. Custom log levels are also supported. -Starting in version 0.4.0, if you're using this package with Click 8, shell -completion of log level names (both built-in and custom) is also supported. +Starting in version 0.4.0, shell completion of log level names (both built-in +and custom) is also supported. .. _Click: https://palletsprojects.com/p/click/ @@ -42,8 +42,7 @@ completion of log level names (both built-in and custom) is also supported. Installation ============ ``click-loglevel`` requires Python 3.7 or higher. Just use `pip -`_ for Python 3 (You have pip, right?) to install -``click-loglevel`` and its dependencies:: +`_ for Python 3 (You have pip, right?) to install it:: python3 -m pip install click-loglevel diff --git a/setup.cfg b/setup.cfg index d387936..7a53e35 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,4 +46,4 @@ package_dir = include_package_data = True python_requires = >=3.7 install_requires = - click >= 6.0 + click >= 8.0 diff --git a/src/click_loglevel.py b/src/click_loglevel.py index 174eaa6..929311c 100644 --- a/src/click_loglevel.py +++ b/src/click_loglevel.py @@ -8,8 +8,8 @@ into their corresponding numeric values. It also accepts integer values and leaves them as-is. Custom log levels are also supported. -Starting in version 0.4.0, if you're using this package with Click 8, shell -completion of log level names (both built-in and custom) is also supported. +Starting in version 0.4.0, shell completion of log level names (both built-in +and custom) is also supported. .. _Click: https://palletsprojects.com/p/click/ diff --git a/tox.ini b/tox.ini index 5f15534..c432e25 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,11 @@ [tox] -envlist = lint,py{36,37,38,39,310,311,312,py3}-click{6,7,8} +envlist = lint,py{36,37,38,39,310,311,312,py3}-click8 skip_missing_interpreters = True isolated_build = True minversion = 3.3.0 [testenv] deps = - click6: click~=6.0 - click7: click~=7.0 click8: click~=8.0 pytest pytest-cov From 4d7053f7ea4b463af756b1579afebd8fd5dc5839 Mon Sep 17 00:00:00 2001 From: "John T. Wodder II" Date: Mon, 9 Oct 2023 13:01:40 -0400 Subject: [PATCH 2/3] Enable type-checking --- .github/workflows/test.yml | 2 ++ setup.cfg | 22 ++++++++++++++++++- .../__init__.py} | 0 src/click_loglevel/py.typed | 0 tox.ini | 9 +++++++- 5 files changed, 31 insertions(+), 2 deletions(-) rename src/{click_loglevel.py => click_loglevel/__init__.py} (100%) create mode 100644 src/click_loglevel/py.typed diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 900b16e..8aa9396 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,8 @@ jobs: include: - python-version: '3.7' toxenv: lint + - python-version: '3.7' + toxenv: typing steps: - name: Check out repository uses: actions/checkout@v4 diff --git a/setup.cfg b/setup.cfg index 7a53e35..0c8aeb8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,16 +34,36 @@ classifiers = Environment :: Console Intended Audience :: Developers Topic :: System :: Logging + Typing :: Typed project_urls = Source Code = https://github.com/jwodder/click-loglevel Bug Tracker = https://github.com/jwodder/click-loglevel/issues [options] -py_modules = click_loglevel +packages = find_namespace: package_dir = =src include_package_data = True python_requires = >=3.7 install_requires = click >= 8.0 + +[options.packages.find] +where = src + +[mypy] +allow_incomplete_defs = False +allow_untyped_defs = False +ignore_missing_imports = True +# : +no_implicit_optional = True +implicit_reexport = False +local_partial_types = True +pretty = True +show_error_codes = True +show_traceback = True +strict_equality = True +warn_redundant_casts = True +warn_return_any = True +warn_unreachable = True diff --git a/src/click_loglevel.py b/src/click_loglevel/__init__.py similarity index 100% rename from src/click_loglevel.py rename to src/click_loglevel/__init__.py diff --git a/src/click_loglevel/py.typed b/src/click_loglevel/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini index c432e25..034d35f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = lint,py{36,37,38,39,310,311,312,py3}-click8 +envlist = lint,typing,py{36,37,38,39,310,311,312,py3}-click8 skip_missing_interpreters = True isolated_build = True minversion = 3.3.0 @@ -22,6 +22,13 @@ deps = commands = flake8 src test +[testenv:typing] +deps = + mypy + {[testenv]deps} +commands = + mypy src test + [pytest] addopts = --cov=click_loglevel --no-cov-on-fail filterwarnings = error From 217bf82c588b1f0a203672daff71fbd89ac38429 Mon Sep 17 00:00:00 2001 From: "John T. Wodder II" Date: Mon, 9 Oct 2023 13:08:58 -0400 Subject: [PATCH 3/3] Add type annotations --- CHANGELOG.md | 1 + README.rst | 4 ++-- setup.cfg | 2 +- src/click_loglevel/__init__.py | 29 +++++++++++++++++------------ test/data/dict-extra-nonupper.py | 2 +- test/data/dict-extra.py | 2 +- test/data/list-extra-nonupper.py | 2 +- test/data/list-extra.py | 2 +- test/test_loglevel.py | 31 ++++++++++++++++--------------- 9 files changed, 41 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8b9531..409426d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ v0.5.0 (in development) - Support Python 3.10, 3.11, and 3.12 - Drop support for Python 3.6 - Require Click >= 8.0 +- Add type annotations v0.4.0.post1 (2021-06-05) ------------------------- diff --git a/README.rst b/README.rst index 2ed60ed..44bdda7 100644 --- a/README.rst +++ b/README.rst @@ -60,7 +60,7 @@ Examples @click.command() @click.option("-l", "--log-level", type=LogLevel(), default=logging.INFO) - def main(log_level): + def main(log_level: int) -> None: logging.basicConfig( format="[%(levelname)-8s] %(message)s", level=log_level, @@ -98,7 +98,7 @@ Script with custom log levels: type=LogLevel(extra=["VERBOSE", "NOTICE"]), default=logging.INFO, ) - def main(log_level): + def main(log_level: int) -> None: logging.basicConfig( format="[%(levelname)-8s] %(message)s", level=log_level, diff --git a/setup.cfg b/setup.cfg index 0c8aeb8..7f047ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,7 +55,7 @@ where = src [mypy] allow_incomplete_defs = False allow_untyped_defs = False -ignore_missing_imports = True +ignore_missing_imports = False # : no_implicit_optional = True implicit_reexport = False diff --git a/src/click_loglevel/__init__.py b/src/click_loglevel/__init__.py index 929311c..2ffa5e2 100644 --- a/src/click_loglevel/__init__.py +++ b/src/click_loglevel/__init__.py @@ -16,16 +16,18 @@ Visit for more information. """ +from __future__ import annotations +from collections.abc import Iterable, Iterator, Mapping +import logging +import click +from click.shell_completion import CompletionItem + __version__ = "0.5.0.dev1" __author__ = "John Thorvald Wodder II" __author_email__ = "click-loglevel@varonathe.org" __license__ = "MIT" __url__ = "https://github.com/jwodder/click-loglevel" -from collections.abc import Mapping -import logging -import click - __all__ = ["LogLevel", "LogLevelType"] @@ -47,8 +49,8 @@ class LogLevel(click.ParamType): name = "log-level" LEVELS = ["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] - def __init__(self, extra=None): - self.levels = {lv: getattr(logging, lv) for lv in self.LEVELS} + def __init__(self, extra: Iterable[str] | Mapping[str, int] | None = None) -> None: + self.levels: dict[str, int] = {lv: getattr(logging, lv) for lv in self.LEVELS} level_names = list(self.LEVELS) if extra is not None: if isinstance(extra, Mapping): @@ -61,24 +63,27 @@ def __init__(self, extra=None): level_names.append(lv) self.metavar = "[" + "|".join(level_names) + "]" - def convert(self, value, param, ctx): + def convert( + self, value: str | int, param: click.Parameter | None, ctx: click.Context | None + ) -> int: try: return int(value) except ValueError: + assert isinstance(value, str) try: return self.levels[value.upper()] except KeyError: self.fail(f"{value!r}: invalid log level", param, ctx) - def get_metavar(self, _param): + def get_metavar(self, _param: click.Parameter) -> str: return self.metavar - def shell_complete(self, _ctx, _param, incomplete): - from click.shell_completion import CompletionItem - + def shell_complete( + self, _ctx: click.Context, _param: click.Parameter, incomplete: str + ) -> list[CompletionItem]: return [CompletionItem(c) for c in self.get_completions(incomplete)] - def get_completions(self, incomplete): + def get_completions(self, incomplete: str) -> Iterator[str]: incomplete = incomplete.upper() for lv in self.levels: if lv.startswith(incomplete): diff --git a/test/data/dict-extra-nonupper.py b/test/data/dict-extra-nonupper.py index 47cf873..c2e9ac0 100644 --- a/test/data/dict-extra-nonupper.py +++ b/test/data/dict-extra-nonupper.py @@ -9,7 +9,7 @@ "--log-level", type=LogLevel(extra={"Verbose": 15, "Notice": 25}), ) -def main(log_level): +def main(log_level: int) -> None: click.echo(repr(log_level)) diff --git a/test/data/dict-extra.py b/test/data/dict-extra.py index 50b38fa..75f913c 100644 --- a/test/data/dict-extra.py +++ b/test/data/dict-extra.py @@ -9,7 +9,7 @@ "--log-level", type=LogLevel(extra={"VERBOSE": 15, "NOTICE": 25}), ) -def main(log_level): +def main(log_level: int) -> None: click.echo(repr(log_level)) diff --git a/test/data/list-extra-nonupper.py b/test/data/list-extra-nonupper.py index 4c35fe4..c93a352 100644 --- a/test/data/list-extra-nonupper.py +++ b/test/data/list-extra-nonupper.py @@ -8,7 +8,7 @@ @click.command() @click.option("-l", "--log-level", type=LogLevel(extra=["Verbose", "Notice"])) -def main(log_level): +def main(log_level: int) -> None: click.echo(repr(log_level)) diff --git a/test/data/list-extra.py b/test/data/list-extra.py index f91441c..bd93212 100644 --- a/test/data/list-extra.py +++ b/test/data/list-extra.py @@ -8,7 +8,7 @@ @click.command() @click.option("-l", "--log-level", type=LogLevel(extra=["VERBOSE", "NOTICE"])) -def main(log_level): +def main(log_level: int) -> None: click.echo(repr(log_level)) diff --git a/test/test_loglevel.py b/test/test_loglevel.py index 9e24e08..119e510 100644 --- a/test/test_loglevel.py +++ b/test/test_loglevel.py @@ -1,3 +1,4 @@ +from __future__ import annotations import logging from pathlib import Path import subprocess @@ -12,13 +13,13 @@ @click.command() @click.option("-l", "--log-level", type=LogLevel(), default=logging.INFO) -def lvlcmd(log_level): +def lvlcmd(log_level: int) -> None: click.echo(repr(log_level)) @click.command() @click.option("-l", "--log-level", type=LogLevel()) -def lvlcmd_nodefault(log_level): +def lvlcmd_nodefault(log_level: int) -> None: click.echo(repr(log_level)) @@ -53,25 +54,25 @@ def lvlcmd_nodefault(log_level): @pytest.mark.parametrize("loglevel,value", STANDARD_LEVELS) -def test_loglevel(loglevel, value): +def test_loglevel(loglevel: str | int, value: int) -> None: r = CliRunner().invoke(lvlcmd, ["-l", str(loglevel)]) assert r.exit_code == 0, r.output assert r.output == str(value) + "\n" -def test_loglevel_default(): +def test_loglevel_default() -> None: r = CliRunner().invoke(lvlcmd) assert r.exit_code == 0, r.output assert r.output == str(logging.INFO) + "\n" -def test_loglevel_no_default(): +def test_loglevel_no_default() -> None: r = CliRunner().invoke(lvlcmd_nodefault) assert r.exit_code == 0, r.output assert r.output == "None\n" -def test_loglevel_help(): +def test_loglevel_help() -> None: r = CliRunner().invoke(lvlcmd, ["--help"]) assert r.exit_code == 0, r.output assert "--log-level [NOTSET|DEBUG|INFO|WARNING|ERROR|CRITICAL]" in r.output @@ -85,7 +86,7 @@ def test_loglevel_help(): "VERBOSE", ], ) -def test_invalid_loglevel(value): +def test_invalid_loglevel(value: str) -> None: r = CliRunner().invoke(lvlcmd, ["--log-level", value]) assert r.exit_code != 0, r.output assert f"{value!r}: invalid log level" in r.output @@ -104,7 +105,7 @@ def test_invalid_loglevel(value): ], ) @pytest.mark.parametrize("script", ["list-extra.py", "dict-extra.py"]) -def test_loglevel_extra(loglevel, value, script): +def test_loglevel_extra(loglevel: str | int, value: int, script: str) -> None: r = subprocess.run( [sys.executable, str(DATA_DIR / script), "--log-level", str(loglevel)], stdout=subprocess.PIPE, @@ -115,7 +116,7 @@ def test_loglevel_extra(loglevel, value, script): @pytest.mark.parametrize("script", ["list-extra.py", "dict-extra.py"]) -def test_loglevel_extra_help(script): +def test_loglevel_extra_help(script: str) -> None: r = subprocess.run( [sys.executable, str(DATA_DIR / script), "--help"], stdout=subprocess.PIPE, @@ -137,7 +138,7 @@ def test_loglevel_extra_help(script): ], ) @pytest.mark.parametrize("script", ["list-extra.py", "dict-extra.py"]) -def test_invalid_loglevel_extra(value, script): +def test_invalid_loglevel_extra(value: str, script: str) -> None: r = subprocess.run( [sys.executable, str(DATA_DIR / script), "--log-level", value], stdout=subprocess.PIPE, @@ -167,7 +168,7 @@ def test_invalid_loglevel_extra(value, script): "dict-extra-nonupper.py", ], ) -def test_loglevel_extra_nonupper(loglevel, value, script): +def test_loglevel_extra_nonupper(loglevel: str | int, value: int, script: str) -> None: r = subprocess.run( [sys.executable, str(DATA_DIR / script), "--log-level", str(loglevel)], stdout=subprocess.PIPE, @@ -184,7 +185,7 @@ def test_loglevel_extra_nonupper(loglevel, value, script): "dict-extra-nonupper.py", ], ) -def test_loglevel_extra_nonupper_help(script): +def test_loglevel_extra_nonupper_help(script: str) -> None: r = subprocess.run( [sys.executable, str(DATA_DIR / script), "--help"], stdout=subprocess.PIPE, @@ -212,7 +213,7 @@ def test_loglevel_extra_nonupper_help(script): "dict-extra-nonupper.py", ], ) -def test_invalid_loglevel_extra_nonupper(value, script): +def test_invalid_loglevel_extra_nonupper(value: str, script: str) -> None: r = subprocess.run( [sys.executable, str(DATA_DIR / script), "--log-level", value], stdout=subprocess.PIPE, @@ -241,7 +242,7 @@ def test_invalid_loglevel_extra_nonupper(value, script): ("INFOS", []), ], ) -def test_get_completions(incomplete, completions): +def test_get_completions(incomplete: str, completions: list[str]) -> None: ll = LogLevel() assert list(ll.get_completions(incomplete)) == completions @@ -267,6 +268,6 @@ def test_get_completions(incomplete, completions): ("INFOS", []), ], ) -def test_get_completions_extra(incomplete, completions): +def test_get_completions_extra(incomplete: str, completions: list[str]) -> None: ll = LogLevel(extra={"Verbose": 5, "Notice": 25}) assert list(ll.get_completions(incomplete)) == completions