diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index b3e7b65..3bf7659 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -24,6 +24,7 @@ jobs: python -m pip install --upgrade pip python -m pip install testtools python -m pip install ruff + python -m pip install mypy - name: Test with testtools run: | python -m testtools.run extras.tests.test_suite @@ -33,3 +34,6 @@ jobs: - name: Check formatting with ruff run: | python -m ruff format --check . + - name: Type check with mypy + run: | + python -m mypy --strict --ignore-missing-imports --follow-imports=skip --exclude tests . diff --git a/extras/__init__.py b/extras/__init__.py index 3b33ef1..4c18b37 100644 --- a/extras/__init__.py +++ b/extras/__init__.py @@ -3,6 +3,7 @@ """Extensions to the Python standard library.""" import sys +from typing import Optional, Callable, Sequence __all__ = [ "try_import", @@ -24,7 +25,11 @@ __version__ = (1, 0, 0, "final", 0) -def try_import(name, alternative=None, error_callback=None): +def try_import( + name: str, + alternative: Optional[object] = None, + error_callback: Optional[Callable[[ImportError], None]] = None, +) -> object: """Attempt to import ``name``. If it fails, return ``alternative``. When supporting multiple versions of Python or optional dependencies, it @@ -38,7 +43,7 @@ def try_import(name, alternative=None, error_callback=None): when the module cannot be loaded. """ module_segments = name.split(".") - last_error = None + last_error: Optional[ImportError] = None remainder = [] # module_name will be what successfully imports. We cannot walk from the # __import__ result because in import loops (A imports A.B, which imports @@ -48,7 +53,7 @@ def try_import(name, alternative=None, error_callback=None): try: __import__(module_name) except ImportError: - last_error = sys.exc_info()[1] + last_error = sys.exc_info()[1] # type: ignore remainder.append(module_segments.pop()) continue else: @@ -60,7 +65,7 @@ def try_import(name, alternative=None, error_callback=None): module = sys.modules[module_name] nonexistent = object() for segment in reversed(remainder): - module = getattr(module, segment, nonexistent) + module = getattr(module, segment, nonexistent) # type: ignore if module is nonexistent: if last_error is not None and error_callback is not None: error_callback(last_error) @@ -71,7 +76,11 @@ def try_import(name, alternative=None, error_callback=None): _RAISE_EXCEPTION = object() -def try_imports(module_names, alternative=_RAISE_EXCEPTION, error_callback=None): +def try_imports( + module_names: Sequence[str], + alternative: object = _RAISE_EXCEPTION, + error_callback: Optional[Callable[[ImportError], None]] = None, +) -> object: """Attempt to import modules. Tries to import the first module in ``module_names``. If it can be diff --git a/extras/py.typed b/extras/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index 91767ed..60821d6 100755 --- a/setup.py +++ b/setup.py @@ -1,26 +1,28 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """Distutils installer for extras.""" -from setuptools import setup +from setuptools import setup, Command import os.path +from typing import cast import extras -testtools_cmd = extras.try_import("testtools.TestCommand") +testtools_cmd = cast(Command, extras.try_import("testtools.TestCommand")) -def get_version(): +def get_version() -> str: """Return the version of extras that we are building.""" version = ".".join(str(component) for component in extras.__version__[0:3]) return version -def get_long_description(): +def get_long_description() -> str: readme_path = os.path.join(os.path.dirname(__file__), "README.rst") - return open(readme_path).read() + with open(readme_path) as f: + return f.read() -cmdclass = {} +cmdclass: dict[str, type[Command]] = {} if testtools_cmd is not None: cmdclass["test"] = testtools_cmd @@ -53,5 +55,6 @@ def get_long_description(): "extras", "extras.tests", ], + package_data={"extras": ["py.typed"]}, cmdclass=cmdclass, )