From 469cc0b2d4837d8d31fac1753727d57b6e2cee6f Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 9 May 2022 13:16:44 -0400 Subject: [PATCH] pip_audit: ratchet down typing (#272) * pip_audit: ratchet down typing * pypi_provider: fix type --- Makefile | 5 +- mypy.ini | 9 --- pip_audit/_cache.py | 8 +-- pip_audit/_cli.py | 8 +-- .../resolvelib/pypi_provider.py | 56 +++++++++++++------ .../resolvelib/resolvelib.py | 8 ++- pip_audit/_fix.py | 4 +- pip_audit/_format/columns.py | 2 +- pip_audit/_format/cyclonedx.py | 2 +- pip_audit/_format/json.py | 2 +- pip_audit/_service/interface.py | 4 +- pip_audit/_state.py | 10 ++-- pip_audit/_virtual_env.py | 3 +- pyproject.toml | 16 ++++++ 14 files changed, 86 insertions(+), 51 deletions(-) delete mode 100644 mypy.ini diff --git a/Makefile b/Makefile index 669e6786..536d5fee 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,7 @@ endif env/pyvenv.cfg: setup.py pyproject.toml # Create our Python 3 virtual environment - rm -rf env - python3 -m venv env + [[ ! -d env ]] || python3 -m venv env ./env/bin/python -m pip install --upgrade pip ./env/bin/python -m pip install -e .[dev] @@ -47,7 +46,7 @@ lint: env/pyvenv.cfg black $(ALL_PY_SRCS) && \ isort $(ALL_PY_SRCS) && \ flake8 $(ALL_PY_SRCS) && \ - mypy $(PY_MODULE) test/ && \ + mypy $(PY_MODULE) && \ interrogate -c pyproject.toml . .PHONY: test tests diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 3f3cb06e..00000000 --- a/mypy.ini +++ /dev/null @@ -1,9 +0,0 @@ -[mypy] -warn_return_any = True -warn_unused_configs = True -warn_unused_ignores = True -warn_no_return = True -strict_equality = True -allow_redefinition = True -check_untyped_defs = True -show_error_codes = True diff --git a/pip_audit/_cache.py b/pip_audit/_cache.py index 6bed51bb..c154c694 100644 --- a/pip_audit/_cache.py +++ b/pip_audit/_cache.py @@ -12,8 +12,8 @@ import pip_api import requests -from cachecontrol import CacheControl # type: ignore -from cachecontrol.caches import FileCache # type: ignore +from cachecontrol import CacheControl +from cachecontrol.caches import FileCache from packaging.version import Version from pip_audit._service.interface import ServiceError @@ -74,7 +74,7 @@ class _SafeFileCache(FileCache): caching directory as a running `pip` process). """ - def __init__(self, directory): + def __init__(self, directory: Path): self._logged_warning = False super().__init__(directory) @@ -134,7 +134,7 @@ def delete(self, key: str) -> None: # pragma: no cover self._logged_warning = True -def caching_session(cache_dir: Optional[Path], *, use_pip=False) -> CacheControl: +def caching_session(cache_dir: Optional[Path], *, use_pip: bool = False) -> CacheControl: """ Return a `requests` style session, with suitable caching middleware. diff --git a/pip_audit/_cli.py b/pip_audit/_cli.py index 10f15f9c..83789527 100644 --- a/pip_audit/_cli.py +++ b/pip_audit/_cli.py @@ -56,7 +56,7 @@ def to_format(self, output_desc: bool) -> VulnerabilityFormat: else: assert_never(self) - def __str__(self): + def __str__(self) -> str: return self.value @@ -77,7 +77,7 @@ def to_service(self, timeout: int, cache_dir: Optional[Path]) -> VulnerabilitySe else: assert_never(self) - def __str__(self): + def __str__(self) -> str: return self.value @@ -101,7 +101,7 @@ def to_bool(self, format_: OutputFormatChoice) -> bool: else: assert_never(self) - def __str__(self): + def __str__(self) -> str: return self.value @@ -117,7 +117,7 @@ class ProgressSpinnerChoice(str, enum.Enum): def __bool__(self) -> bool: return self is ProgressSpinnerChoice.On - def __str__(self): + def __str__(self) -> str: return self.value diff --git a/pip_audit/_dependency_source/resolvelib/pypi_provider.py b/pip_audit/_dependency_source/resolvelib/pypi_provider.py index e5e7a626..2dcc8ddf 100644 --- a/pip_audit/_dependency_source/resolvelib/pypi_provider.py +++ b/pip_audit/_dependency_source/resolvelib/pypi_provider.py @@ -6,24 +6,25 @@ """ import itertools -from email.message import EmailMessage +from email.message import EmailMessage, Message from email.parser import BytesParser from io import BytesIO from operator import attrgetter from pathlib import Path from tempfile import TemporaryDirectory -from typing import BinaryIO, Iterator, List, Optional, Set, cast +from typing import Any, BinaryIO, Iterator, List, Mapping, Optional, Set, Union, cast from urllib.parse import urlparse from zipfile import ZipFile import html5lib import requests -from cachecontrol import CacheControl # type: ignore +from cachecontrol import CacheControl from packaging.requirements import Requirement from packaging.specifiers import SpecifierSet from packaging.utils import canonicalize_name, parse_sdist_filename, parse_wheel_filename from packaging.version import Version from resolvelib.providers import AbstractProvider +from resolvelib.resolvers import RequirementInformation from pip_audit._cache import caching_session from pip_audit._state import AuditState @@ -68,10 +69,10 @@ def __init__( self._timeout = timeout self._state = state - self._metadata: Optional[EmailMessage] = None + self._metadata: Optional[Message] = None self._dependencies: Optional[List[Requirement]] = None - def __repr__(self): # pragma: no cover + def __repr__(self) -> str: # pragma: no cover """ A string representation for `Candidate`. """ @@ -80,7 +81,7 @@ def __repr__(self): # pragma: no cover return f"<{self.name}[{','.join(self.extras)}]=={self.version} wheel={self.is_wheel}>" @property - def metadata(self) -> EmailMessage: + def metadata(self) -> Message: """ Return the package metadata for this candidate. """ @@ -94,7 +95,7 @@ def metadata(self) -> EmailMessage: self._metadata = self._get_metadata_for_sdist() return self._metadata - def _get_dependencies(self): + def _get_dependencies(self) -> Iterator[Requirement]: """ Computes the dependency set for this candidate. """ @@ -119,7 +120,7 @@ def dependencies(self) -> List[Requirement]: self._dependencies = list(self._get_dependencies()) return self._dependencies - def _get_metadata_for_wheel(self): + def _get_metadata_for_wheel(self) -> Message: """ Extracts the metadata for this candidate, if it's a wheel. """ @@ -138,7 +139,7 @@ def _get_metadata_for_wheel(self): # If we didn't find the metadata, return an empty dict return EmailMessage() # pragma: no cover - def _get_metadata_for_sdist(self): + def _get_metadata_for_sdist(self) -> Message: """ Extracts the metadata for this candidate, if it's a source distribution. """ @@ -173,7 +174,12 @@ def _get_metadata_for_sdist(self): def get_project_from_indexes( - index_urls: List[str], session, project, extras, timeout: Optional[int], state: AuditState + index_urls: List[str], + session: CacheControl, + project: str, + extras: Set[str], + timeout: Optional[int], + state: AuditState, ) -> Iterator[Candidate]: """Return candidates from all indexes created from the project name and extras.""" project_found = False @@ -192,7 +198,12 @@ def get_project_from_indexes( def get_project_from_index( - index_url: str, session, project, extras, timeout: Optional[int], state: AuditState + index_url: str, + session: CacheControl, + project: str, + extras: Set[str], + timeout: Optional[int], + state: AuditState, ) -> Iterator[Candidate]: """Return candidates from an index created from the project name and extras.""" url = index_url + "/" + project @@ -272,19 +283,32 @@ def __init__( self.session = caching_session(cache_dir, use_pip=True) self._state = state - def identify(self, requirement_or_candidate): + def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str: """ See `resolvelib.providers.AbstractProvider.identify`. """ return canonicalize_name(requirement_or_candidate.name) - def get_preference(self, identifier, resolutions, candidates, information, backtrack_causes): + # TODO: Typing. See: https://github.com/sarugaku/resolvelib/issues/104 + def get_preference( # type: ignore[override, no-untyped-def] + self, + identifier: Any, + resolutions: Mapping[Any, Any], + candidates: Mapping[Any, Iterator[Any]], + information: Mapping[Any, Iterator[RequirementInformation]], + backtrack_causes: Any, + ): """ See `resolvelib.providers.AbstractProvider.get_preference`. """ return sum(1 for _ in candidates[identifier]) - def find_matches(self, identifier, requirements, incompatibilities): + def find_matches( + self, + identifier: Any, + requirements: Mapping[Any, Iterator[Any]], + incompatibilities: Mapping[Any, Iterator[Any]], + ) -> Iterator[Any]: """ See `resolvelib.providers.AbstractProvider.find_matches`. """ @@ -333,13 +357,13 @@ def find_matches(self, identifier, requirements, incompatibilities): else: yield from candidates - def is_satisfied_by(self, requirement, candidate): + def is_satisfied_by(self, requirement: Any, candidate: Any) -> bool: """ See `resolvelib.providers.AbstractProvider.is_satisfied_by`. """ return candidate.version in requirement.specifier - def get_dependencies(self, candidate): + def get_dependencies(self, candidate: Any) -> Any: """ See `resolvelib.providers.AbstractProvider.get_dependencies`. """ diff --git a/pip_audit/_dependency_source/resolvelib/resolvelib.py b/pip_audit/_dependency_source/resolvelib/resolvelib.py index b719c227..7e775c12 100644 --- a/pip_audit/_dependency_source/resolvelib/resolvelib.py +++ b/pip_audit/_dependency_source/resolvelib/resolvelib.py @@ -5,9 +5,9 @@ import logging from pathlib import Path -from typing import List, Optional, cast +from typing import List, Optional, Union -from packaging.requirements import Requirement +from packaging.requirements import Requirement as _Requirement from pip_api import Requirement as ParsedRequirement from requests.exceptions import HTTPError from resolvelib import BaseReporter, Resolver @@ -23,6 +23,9 @@ PYPI_URL = "https://pypi.org/simple" +Requirement = Union[_Requirement, ParsedRequirement] + + class ResolveLibResolver(DependencyResolver): """ An implementation of `DependencyResolver` that uses `resolvelib` as its @@ -62,7 +65,6 @@ def resolve(self, req: Requirement) -> List[Dependency]: # since the latter is a subclass. But only the latter knows whether the # requirement is editable, so we need to check for it here. if isinstance(req, ParsedRequirement): - req = cast(ParsedRequirement, req) if req.editable and self._skip_editable: return [ SkippedDependency(name=req.name, skip_reason="requirement marked as editable") diff --git a/pip_audit/_fix.py b/pip_audit/_fix.py index 0a4cc6b0..165117ac 100644 --- a/pip_audit/_fix.py +++ b/pip_audit/_fix.py @@ -4,7 +4,7 @@ import logging from dataclasses import dataclass -from typing import Dict, Iterator, List, cast +from typing import Any, Dict, Iterator, List, cast from packaging.version import Version @@ -29,7 +29,7 @@ class FixVersion: dep: ResolvedDependency - def __init__(self, *_args, **_kwargs) -> None: # pragma: no cover + def __init__(self, *_args: Any, **_kwargs: Any) -> None: # pragma: no cover """ A stub constructor that always fails. """ diff --git a/pip_audit/_format/columns.py b/pip_audit/_format/columns.py index 85b3d954..fef06e44 100644 --- a/pip_audit/_format/columns.py +++ b/pip_audit/_format/columns.py @@ -41,7 +41,7 @@ def __init__(self, output_desc: bool): self.output_desc = output_desc @property - def is_manifest(self): + def is_manifest(self) -> bool: """ See `VulnerabilityFormat.is_manifest`. """ diff --git a/pip_audit/_format/cyclonedx.py b/pip_audit/_format/cyclonedx.py index 1db597fc..33b1faa9 100644 --- a/pip_audit/_format/cyclonedx.py +++ b/pip_audit/_format/cyclonedx.py @@ -69,7 +69,7 @@ def __init__(self, inner_format: "CycloneDxFormat.InnerFormat"): self._inner_format = inner_format @property - def is_manifest(self): + def is_manifest(self) -> bool: """ See `VulnerabilityFormat.is_manifest`. """ diff --git a/pip_audit/_format/json.py b/pip_audit/_format/json.py index f1d5d1fa..45ac591f 100644 --- a/pip_audit/_format/json.py +++ b/pip_audit/_format/json.py @@ -27,7 +27,7 @@ def __init__(self, output_desc: bool): self.output_desc = output_desc @property - def is_manifest(self): + def is_manifest(self) -> bool: """ See `VulnerabilityFormat.is_manifest`. """ diff --git a/pip_audit/_service/interface.py b/pip_audit/_service/interface.py index f4209a5d..92f05507 100644 --- a/pip_audit/_service/interface.py +++ b/pip_audit/_service/interface.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import Dict, Iterator, List, Set, Tuple +from typing import Any, Dict, Iterator, List, Set, Tuple from packaging.utils import canonicalize_name from packaging.version import Version @@ -28,7 +28,7 @@ class Dependency: Use the `canonicalized_name` property when a canonicalized form is necessary. """ - def __init__(self, *_args, **_kwargs) -> None: + def __init__(self, *_args: Any, **_kwargs: Any) -> None: """ A stub constructor that always fails. """ diff --git a/pip_audit/_state.py b/pip_audit/_state.py index 86bdd773..5b8d3d53 100644 --- a/pip_audit/_state.py +++ b/pip_audit/_state.py @@ -9,7 +9,7 @@ from logging.handlers import MemoryHandler from typing import Any, Dict, List, Sequence -from progress.spinner import Spinner as BaseSpinner # type: ignore +from progress.spinner import Spinner as BaseSpinner class AuditState: @@ -30,7 +30,7 @@ def __init__(self, *, members: Sequence["_StateActor"] = []): self._members = members - def update_state(self, message: str): + def update_state(self, message: str) -> None: """ Called whenever `pip_audit`'s internal state changes in a way that's meaningful to expose to a user. @@ -64,7 +64,9 @@ def __enter__(self) -> "AuditState": # pragma: no cover self.initialize() return self - def __exit__(self, _exc_type, _exc_value, _exc_traceback): # pragma: no cover + def __exit__( + self, _exc_type: Any, _exc_value: Any, _exc_traceback: Any + ) -> None: # pragma: no cover """ Helper to ensure `finalize` gets called when the `pip-audit` state falls out of scope of a `with` statement. @@ -122,7 +124,7 @@ def __init__(self, message: str = "", **kwargs: Dict[str, Any]): ) self.prev_handlers: List[logging.Handler] = [] - def _writeln_truncated(self, line: str): + def _writeln_truncated(self, line: str) -> None: """ Wraps `BaseSpinner.writeln`, providing reasonable truncation behavior when a line would otherwise overflow its terminal row and cause the progress diff --git a/pip_audit/_virtual_env.py b/pip_audit/_virtual_env.py index 713810e3..b92588fc 100644 --- a/pip_audit/_virtual_env.py +++ b/pip_audit/_virtual_env.py @@ -5,6 +5,7 @@ import json import logging import venv +from types import SimpleNamespace from typing import Iterator, List, Optional, Tuple from packaging.version import Version @@ -54,7 +55,7 @@ def __init__(self, install_args: List[str], state: AuditState = AuditState()): self._packages: Optional[List[Tuple[str, Version]]] = None self._state = state - def post_setup(self, context): + def post_setup(self, context: SimpleNamespace) -> None: """ Install the custom package and populate the list of installed packages. diff --git a/pyproject.toml b/pyproject.toml index db00634f..289e2cd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,3 +78,19 @@ omit = ["pip_audit/_cli.py"] exclude = ["setup.py", "env", "test", "pip_audit/_cli.py", "pip_audit/_version.py"] ignore-semiprivate = true fail-under = 100 + +[tool.mypy] +allow_redefinition = true +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +ignore_missing_imports = true +no_implicit_optional = true +show_error_codes = true +strict_equality = true +warn_no_return = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true