Skip to content

Commit

Permalink
Merge pull request #3 from jwodder/typing
Browse files Browse the repository at this point in the history
Add type annotations
  • Loading branch information
jwodder authored Oct 9, 2023
2 parents f8cc9cf + 217bf82 commit fbb8997
Show file tree
Hide file tree
Showing 12 changed files with 78 additions and 46 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ jobs:
- 'pypy-3.9'
- 'pypy-3.10'
toxenv:
- py-click6
- py-click7
- py-click8
include:
- python-version: '3.7'
toxenv: lint
- python-version: '3.7'
toxenv: typing
steps:
- name: Check out repository
uses: actions/checkout@v4
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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)
-------------------------
Expand Down
11 changes: 5 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,16 @@ 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/


Installation
============
``click-loglevel`` requires Python 3.7 or higher. Just use `pip
<https://pip.pypa.io>`_ for Python 3 (You have pip, right?) to install
``click-loglevel`` and its dependencies::
<https://pip.pypa.io>`_ for Python 3 (You have pip, right?) to install it::

python3 -m pip install click-loglevel

Expand All @@ -61,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,
Expand Down Expand Up @@ -99,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,
Expand Down
24 changes: 22 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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 >= 6.0
click >= 8.0

[options.packages.find]
where = src

[mypy]
allow_incomplete_defs = False
allow_untyped_defs = False
ignore_missing_imports = False
# <https://github.com/python/mypy/issues/7773>:
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
33 changes: 19 additions & 14 deletions src/click_loglevel.py → src/click_loglevel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,26 @@
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/
Visit <https://github.com/jwodder/click-loglevel> 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"]


Expand All @@ -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):
Expand All @@ -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):
Expand Down
Empty file added src/click_loglevel/py.typed
Empty file.
2 changes: 1 addition & 1 deletion test/data/dict-extra-nonupper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))


Expand Down
2 changes: 1 addition & 1 deletion test/data/dict-extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))


Expand Down
2 changes: 1 addition & 1 deletion test/data/list-extra-nonupper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))


Expand Down
2 changes: 1 addition & 1 deletion test/data/list-extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))


Expand Down
31 changes: 16 additions & 15 deletions test/test_loglevel.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import logging
from pathlib import Path
import subprocess
Expand All @@ -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))


Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -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
11 changes: 8 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
[tox]
envlist = lint,py{36,37,38,39,310,311,312,py3}-click{6,7,8}
envlist = lint,typing,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
Expand All @@ -24,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
Expand Down

0 comments on commit fbb8997

Please sign in to comment.