diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6a48acf0a5..2860654b5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,6 +22,15 @@ repos: hooks: - id: ruff +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v2.4.0 + hooks: + - id: mypy + # Type-check pipenv's own code only; vendor/ and patched/ are excluded + # in [tool.mypy] and imports into them are skipped (follow_imports). + pass_filenames: false + args: [] + - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index 08397b1959..f332eabd2d 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -91,6 +91,11 @@ and install the pre-commit hooks locally: $ pre-commit run --all-files --verbose ``` +The pre-commit hooks include [mypy](https://mypy.readthedocs.io/), which type-checks +pipenv's own code (`pipenv/vendor/` and `pipenv/patched/` are excluded). Its +settings live under `[tool.mypy]` in `pyproject.toml`, so you can also run it +directly with `mypy`. + ### Code Review Contributions will not be merged until they have been code reviewed. You should diff --git a/news/5335.process.rst b/news/5335.process.rst new file mode 100644 index 0000000000..2c23dcfa7a --- /dev/null +++ b/news/5335.process.rst @@ -0,0 +1 @@ +Enable mypy type checking for pipenv's own code (excluding ``pipenv/vendor`` and ``pipenv/patched``) as a pre-commit hook, which also gates CI. Existing type errors were fixed, mostly by correcting annotations and converting legacy ``# type:`` comments to inline annotations. diff --git a/pipenv/cli/options.py b/pipenv/cli/options.py index 15aeb03d86..85ac584f27 100644 --- a/pipenv/cli/options.py +++ b/pipenv/cli/options.py @@ -2,6 +2,7 @@ import os import re from pathlib import Path +from typing import NoReturn from pipenv.project import Project from pipenv.utils.internet import is_valid_url @@ -500,7 +501,7 @@ class _PipenvArgumentParser(argparse.ArgumentParser): """ArgumentParser subclass that appends 'Did you mean …?' suggestions when the user types an unrecognised subcommand name.""" - def error(self, message: str) -> None: + def error(self, message: str) -> NoReturn: import difflib m = re.search(r"invalid choice: '([^']+)'", message) diff --git a/pipenv/environment.py b/pipenv/environment.py index df2f6b7287..e0c8f750de 100644 --- a/pipenv/environment.py +++ b/pipenv/environment.py @@ -43,7 +43,7 @@ if typing.TYPE_CHECKING: from types import ModuleType - from typing import ContextManager, Generator + from typing import Any, Generator, Iterator from pipenv.project import Project, TPipfile, TSource from pipenv.vendor import tomlkit @@ -54,10 +54,10 @@ class Environment: def __init__( self, - prefix: str | None = None, + prefix: str | Path | None = None, python: str | None = None, is_venv: bool = False, - base_working_set: list[importlib_metadata.Distribution] = None, + base_working_set: Iterable[importlib_metadata.Distribution] | None = None, pipfile: tomlkit.toml_document.TOMLDocument | TPipfile | None = None, sources: list[TSource] | None = None, project: Project | None = None, @@ -65,7 +65,7 @@ def __init__( super().__init__() self._modules = {"pipenv": pipenv} self.base_working_set = base_working_set if base_working_set else BASE_WORKING_SET - prefix = normalize_path(prefix) + prefix = normalize_path(prefix if prefix else sys.prefix) self._python = None if python is not None: self._python = Path(python).absolute().as_posix() @@ -79,30 +79,20 @@ def __init__( if project and not pipfile: pipfile = project.pipfile.parsed self.pipfile = pipfile - self.extra_dists = [] + self.extra_dists: list[importlib_metadata.Distribution] = [] if self.is_venv and prefix is not None and not Path(prefix).exists(): return self.prefix = Path(prefix if prefix else sys.prefix) - self._base_paths = {} + self._base_paths: dict[str, Any] | None = {} if self.is_venv: self._base_paths = self.get_paths() self.sys_paths = get_paths() def safe_import(self, name: str) -> ModuleType: """Helper utility for reimporting previously imported modules while inside the env""" - module = None if name not in self._modules: self._modules[name] = importlib.import_module(name) - module = self._modules[name] - if not module: - dist = next( - iter(dist for dist in self.base_working_set if dist.project_name == name), - None, - ) - if dist: - dist.activate() - module = importlib.import_module(name) - return module + return self._modules[name] @cached_property def python_version(self) -> str | None: @@ -157,7 +147,7 @@ def install_scheme(self): return "posix_prefix" @cached_property - def base_paths(self) -> dict[str, str]: + def base_paths(self) -> dict[str, Any]: """ Returns the context appropriate paths for the environment. @@ -182,19 +172,21 @@ def base_paths(self) -> dict[str, str]: """ prefix = Path(self.prefix) - paths = {} + paths: dict[str, Any] = {} if self._base_paths: paths = self._base_paths.copy() else: try: - paths = self.get_paths() + paths = self.get_paths() or {} except Exception: - paths = get_paths( - self.install_scheme, - vars={ - "base": prefix, - "platbase": prefix, - }, + paths.update( + get_paths( + self.install_scheme, + vars={ + "base": prefix, + "platbase": prefix, + }, + ) ) current_version = get_python_version() try: @@ -207,16 +199,18 @@ def base_paths(self) -> dict[str, str]: # Sometimes virtualenvs are made using virtualenv interpreters and there is no # include directory, which will cause this approach to fail. This failsafe # will make sure we fall back to the shell execution to find the real include path - paths = self.get_include_path() + paths = self.get_include_path() or {} paths.update(self.get_lib_paths()) paths["scripts"] = self.script_basedir if not paths: - paths = get_paths( - self.install_scheme, - vars={ - "base": prefix, - "platbase": prefix, - }, + paths.update( + get_paths( + self.install_scheme, + vars={ + "base": prefix, + "platbase": prefix, + }, + ) ) if not os.path.exists(paths["purelib"]) and not os.path.exists(paths["platlib"]): lib_paths = self.get_lib_paths() @@ -329,7 +323,7 @@ def build_command( py_command = py_command % lines_as_str return py_command - def get_paths(self) -> dict[str, str] | None: + def get_paths(self) -> dict[str, Any] | None: """ Get the paths for the environment by running a subcommand @@ -412,7 +406,7 @@ def get_lib_paths(self) -> dict[str, str]: return paths return {} - def get_include_path(self) -> dict[str, str] | None: + def get_include_path(self) -> dict[str, Any] | None: """Get the include path for the environment :return: The python include path for the environment @@ -464,7 +458,7 @@ def scripts_dir(self) -> str: return self.paths["scripts"] @property - def libdir(self) -> str: + def libdir(self) -> tuple[str, str]: purelib = self.paths.get("purelib", None) if purelib and os.path.exists(purelib): return "purelib", purelib @@ -507,12 +501,13 @@ def get_distributions(self) -> Generator[importlib_metadata.Distribution, None, def find_egg(self, egg_dist: importlib_metadata.Distribution) -> str | None: """Find an egg by name in the given environment""" site_packages = self.libdir[1] - search_filename = f"{egg_dist._normalized_name}.egg-link" + search_filename = f"{egg_dist._normalized_name}.egg-link" # type: ignore[attr-defined] + user_site: str | None try: user_site = site.getusersitepackages() except AttributeError: user_site = site.USER_SITE - search_locations = [site_packages, user_site] + search_locations = [loc for loc in (site_packages, user_site) if loc] for site_directory in search_locations: egg = os.path.join(site_directory, search_filename) if os.path.isfile(egg): @@ -525,7 +520,7 @@ def locate_dist(self, dist: importlib_metadata.Distribution) -> str: If the egg - link doesn 't exist, return the supplied distribution.""" location = self.find_egg(dist) - return location or dist._path + return location or dist._path # type: ignore[attr-defined] def dist_is_in_project(self, dist: importlib_metadata.Distribution) -> bool: """Determine whether the supplied distribution is in the environment.""" @@ -548,12 +543,13 @@ def get_installed_packages(self) -> list[importlib_metadata.Distribution]: packages = [ pkg for pkg in workingset - if self.dist_is_in_project(pkg) and pkg._normalized_name != "python" + if self.dist_is_in_project(pkg) + and pkg._normalized_name != "python" # type: ignore[attr-defined] ] return packages @contextlib.contextmanager - def get_finder(self, pre: bool = False) -> ContextManager[PackageFinder]: + def get_finder(self, pre: bool = False) -> Iterator[PackageFinder]: from pipenv.patched.pip._internal.commands.install import InstallCommand from .utils.resolver import get_package_finder @@ -561,6 +557,7 @@ def get_finder(self, pre: bool = False) -> ContextManager[PackageFinder]: pip_command = InstallCommand( name="InstallCommand", summary="pip Install command." ) + assert self.project is not None and self.pipfile is not None pip_args = prepare_pip_source_args(self.sources) pip_options, _ = pip_command.parser.parse_args(pip_args) pip_options.cache_dir = self.project.s.PIPENV_CACHE_DIR @@ -581,11 +578,12 @@ def get_finder(self, pre: bool = False) -> ContextManager[PackageFinder]: def get_package_info( self, pre: bool = False ) -> Generator[importlib_metadata.Distribution, None, None]: + assert self.pipfile is not None packages = self.get_installed_packages() with self.get_finder() as finder: for dist in packages: - name = dist._normalized_name + name = dist._normalized_name # type: ignore[attr-defined] all_candidates = finder.find_all_candidates(name) allow_prereleases = self.pipfile.get("pre", False) if not allow_prereleases and finder.release_control is not None: @@ -614,7 +612,7 @@ def get_package_info( else: pass # This is dirty but makes the rest of the code much cleaner - dist.latest_version = remote_version + dist.latest_version = remote_version # type: ignore[attr-defined] yield dist def get_outdated_packages( @@ -623,7 +621,7 @@ def get_outdated_packages( return [ pkg for pkg in self.get_package_info(pre=pre) - if pkg.latest_version > parse_version(pkg.version) + if pkg.latest_version > parse_version(pkg.version) # type: ignore[attr-defined] ] @classmethod @@ -735,7 +733,11 @@ def is_installed(self, pkgname): :rtype: bool """ - return any(d for d in self.get_distributions() if d._normalized_name == pkgname) + return any( + d + for d in self.get_distributions() + if d._normalized_name == pkgname # type: ignore[attr-defined] + ) def is_satisfied(self, req: InstallRequirement): match = next( @@ -743,7 +745,8 @@ def is_satisfied(self, req: InstallRequirement): d for d in self.get_distributions() if req.name - and canonicalize_name(d._normalized_name) == canonicalize_name(req.name) + and canonicalize_name(d._normalized_name) # type: ignore[attr-defined] + == canonicalize_name(req.name) ), None, ) @@ -767,8 +770,10 @@ def is_satisfied(self, req: InstallRequirement): else: parsed_url = urlparse(requested_path) local_path = parsed_url.path - return requested_path and os.path.samefile(local_path, match.location) - elif match.has_metadata("direct_url.json"): + return requested_path and os.path.samefile( + local_path, str(match.locate_file("")) + ) + elif match.read_text("direct_url.json") is not None: # Direct URL installs we assume are not satisfied since we may be # installing from Pipfile and have insufficient information to determine # if the content has changed. diff --git a/pipenv/exceptions.py b/pipenv/exceptions.py index f26794a001..0f4d8f7d13 100644 --- a/pipenv/exceptions.py +++ b/pipenv/exceptions.py @@ -1,7 +1,7 @@ import itertools import sys -from collections import namedtuple from traceback import format_tb +from typing import NamedTuple, Optional from pipenv.patched.pip._vendor.rich.console import Console from pipenv.patched.pip._vendor.rich.text import Text @@ -60,11 +60,11 @@ def unstyle(text: str) -> str: return str(text) -KnownException = namedtuple( - "KnownException", - ["exception_name", "match_string", "show_from_string", "prefix"], -) -KnownException.__new__.__defaults__ = (None, None, None, "") +class KnownException(NamedTuple): + exception_name: Optional[str] = None + match_string: Optional[str] = None + show_from_string: Optional[str] = None + prefix: str = "" KNOWN_EXCEPTIONS = [ KnownException("PermissionError", prefix="Permission Denied:"), diff --git a/pipenv/project.py b/pipenv/project.py index 5f976f6076..5b2fbd892b 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -267,6 +267,7 @@ def installed_package_names(self): def get_environment(self, allow_global: bool = False) -> Environment: is_venv = is_in_virtualenv() + prefix: str | Path if allow_global and not is_venv: prefix = sys.prefix python = sys.executable diff --git a/pipenv/resolver/main.py b/pipenv/resolver/main.py index 25200dbfe5..1b1cdea88b 100644 --- a/pipenv/resolver/main.py +++ b/pipenv/resolver/main.py @@ -76,6 +76,7 @@ def _ensure_modules() -> None: "typing_extensions", location=typing_ext_path, ) + assert spec is not None and spec.loader is not None typing_extensions = importlib.util.module_from_spec(spec) sys.modules["typing_extensions"] = typing_extensions spec.loader.exec_module(typing_extensions) diff --git a/pipenv/resolver/pure_python_metadata.py b/pipenv/resolver/pure_python_metadata.py index dd64f75a64..ba10cc822d 100644 --- a/pipenv/resolver/pure_python_metadata.py +++ b/pipenv/resolver/pure_python_metadata.py @@ -478,6 +478,7 @@ def _fetch_via_wheel_head(session: Any, wheel_url: str) -> CoreMetadata: ) try: zf = zipfile.ZipFile(partial) + break except zipfile.BadZipFile: if window >= total_length or window >= _MAX_CENTRAL_DIRECTORY_WINDOW: raise MetadataFetchError( @@ -486,8 +487,6 @@ def _fetch_via_wheel_head(session: Any, wheel_url: str) -> CoreMetadata: ) # Double the window and retry. window = min(window * 2, total_length, _MAX_CENTRAL_DIRECTORY_WINDOW) - continue - break try: info = _find_metadata_member(zf) diff --git a/pipenv/resolver/pure_python_provider.py b/pipenv/resolver/pure_python_provider.py index ee842b4ada..62222b7202 100644 --- a/pipenv/resolver/pure_python_provider.py +++ b/pipenv/resolver/pure_python_provider.py @@ -1131,7 +1131,7 @@ def is_satisfied_by(self, requirement, candidate) -> bool: ... # ``Provides-Extra``. Empty frozenset → metadata not loaded; # admit per the lazy-metadata clause above. Non-empty → # strict subset check. - candidate_extras = getattr(candidate, "extras", frozenset()) + candidate_extras: frozenset[str] = getattr(candidate, "extras", frozenset()) if candidate_extras and not requirement.extras <= candidate_extras: return False diff --git a/pipenv/resolver/schema.py b/pipenv/resolver/schema.py index 7e5d5e2ceb..ff97ebf01e 100644 --- a/pipenv/resolver/schema.py +++ b/pipenv/resolver/schema.py @@ -292,7 +292,7 @@ def from_install_requirement( entry: dict = {"name": name} # ---- VCS branch (locking.py:60-83) ---- - is_vcs_dep = next( + is_vcs_dep: str | bool | None = next( iter([v for v in VCS_LIST if v in pipfile_entry]), None ) if getattr(req, "link", None) is not None and getattr(req.link, "is_vcs", False): diff --git a/pipenv/routines/install.py b/pipenv/routines/install.py index 25beb5b6ae..8be2834213 100644 --- a/pipenv/routines/install.py +++ b/pipenv/routines/install.py @@ -7,6 +7,7 @@ from collections import defaultdict from dataclasses import replace from tempfile import NamedTemporaryFile +from typing import Any from pipenv import environments, exceptions from pipenv.patched.pip._internal.exceptions import PipError @@ -743,7 +744,7 @@ def install_build_system_packages( pypi_mirror=pypi_mirror, ) - procs = queue.Queue(maxsize=1) + procs: queue.Queue = queue.Queue(maxsize=1) cmds = pip_install_deps( project, deps=build_requires, @@ -787,7 +788,7 @@ def do_install_dependencies( pypi_mirror=target.pypi_mirror, requirements_dir=requirements_dir, ) - procs = queue.Queue(maxsize=1) + procs: queue.Queue = queue.Queue(maxsize=1) categories = list(sel.categories) if sel.categories else [] if not categories: if sel.dev: @@ -810,8 +811,8 @@ def do_install_dependencies( effective_ignore_hashes = exec_opts.ignore_hashes or policy.skip_lock for pipfile_category in categories: - lockfile = None - pipfile = None + lockfile: Any = None + pipfile: Any = None if policy.skip_lock: if not exec_opts.bare and not project.s.is_quiet(): console.print("Installing dependencies from Pipfile...", style="bold") @@ -1097,7 +1098,9 @@ def batch_install( ) else: # Sort the dependencies out by index -- include editable/vcs in the default group - deps_by_index = defaultdict(lambda: defaultdict(list)) + deps_by_index: defaultdict[str, defaultdict[Any, list]] = defaultdict( + lambda: defaultdict(list) + ) for dependency, pip_line in deps_to_install: index = project.sources.default["name"] if dependency.name and dependency.name in lockfile_section: diff --git a/pipenv/routines/update.py b/pipenv/routines/update.py index 5d4a64e5f9..ece79a6246 100644 --- a/pipenv/routines/update.py +++ b/pipenv/routines/update.py @@ -195,7 +195,7 @@ def check_version_conflicts( Check if updating a package would create version conflicts with its dependents. Returns set of conflicting packages. """ - conflicts = set() + conflicts: Set[str] = set() # Handle various wildcard patterns if new_version == "*": diff --git a/pipenv/shells.py b/pipenv/shells.py index b624d0cd74..e3bb3197dd 100644 --- a/pipenv/shells.py +++ b/pipenv/shells.py @@ -388,22 +388,24 @@ def fork(self, venv, cwd, args): # Two dimensional dict. First is the shell type, second is the emulator type. # Example: SHELL_LOOKUP['powershell']['cmder'] => CmderPowershell. -SHELL_LOOKUP = collections.defaultdict( +SHELL_LOOKUP: collections.defaultdict[ + str, collections.defaultdict[str, type[Shell]] +] = collections.defaultdict( lambda: collections.defaultdict(lambda: Shell), { - "bash": collections.defaultdict( + "bash": collections.defaultdict[str, type[Shell]]( lambda: Bash, {"msys": MsysBash}, ), - "cmd": collections.defaultdict( + "cmd": collections.defaultdict[str, type[Shell]]( lambda: Shell, {"cmder": CmderCommandPrompt}, ), - "powershell": collections.defaultdict( + "powershell": collections.defaultdict[str, type[Shell]]( lambda: Shell, {"cmder": CmderPowershell}, ), - "pwsh": collections.defaultdict( + "pwsh": collections.defaultdict[str, type[Shell]]( lambda: Shell, {"cmder": CmderPowershell}, ), diff --git a/pipenv/utils/dependencies.py b/pipenv/utils/dependencies.py index 06b678fd2c..59ffa909b3 100644 --- a/pipenv/utils/dependencies.py +++ b/pipenv/utils/dependencies.py @@ -10,7 +10,18 @@ from functools import lru_cache from pathlib import Path from tempfile import NamedTemporaryFile, TemporaryDirectory -from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Mapping, Optional, Sequence, Tuple, Union +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Mapping, + Optional, + Sequence, + Tuple, + Union, + overload, +) from urllib.parse import unquote, urlparse, urlsplit, urlunparse, urlunsplit from pipenv.exceptions import PipenvUsageError @@ -34,6 +45,7 @@ if TYPE_CHECKING: from pipenv.patched.pip._internal.commands.install import InstallCommand + from pipenv.patched.pip._internal.models.link import Link from pipenv.utils import err from pipenv.utils.fileutils import ( create_tracked_tempdir, @@ -163,7 +175,7 @@ def pep423_name(name): return name -def translate_markers(pipfile_entry): +def translate_markers(pipfile_entry: Mapping[str, Any]) -> Dict[str, Any]: from pipenv.patched.pip._vendor.packaging.markers import default_environment allowed_marker_keys = ["markers"] + list(default_environment().keys()) @@ -254,6 +266,12 @@ def extract_vcs_url(vcs_url): return clean_url +@overload +def add_ssh_scheme_to_git_uri(uri: str) -> str: ... +@overload +def add_ssh_scheme_to_git_uri(uri: bytes) -> bytes: ... +@overload +def add_ssh_scheme_to_git_uri(uri: None) -> None: ... def add_ssh_scheme_to_git_uri(uri: str | bytes | None) -> str | bytes | None: """Clean VCS URIs from pip format, preserving ``None`` unchanged.""" if uri is None: @@ -959,7 +977,7 @@ def get_pip_command() -> "InstallCommand": def determine_vcs_revision_hash( - package: InstallRequirement, vcs_type: str, revision: str + package: InstallRequirement, vcs_type: str, revision: str | None ): from pipenv.patched.pip._internal.utils.misc import hide_url @@ -1078,8 +1096,7 @@ def find_package_name_from_filename(filename, file): return None -def create_link(link): - # type: (AnyStr) -> Link +def create_link(link: str) -> "Link": from pipenv.patched.pip._internal.models.link import Link if not isinstance(link, str): @@ -1153,7 +1170,7 @@ def has_name_with_extras(requirement): return match is not None -def expand_env_variables(line) -> AnyStr: +def expand_env_variables(line: str) -> str: """Expand the env vars in a line following pip's standard. https://pip.pypa.io/en/stable/reference/pip_install/#id10. @@ -1182,7 +1199,7 @@ def expansive_install_req_from_line( user_supplied: bool = False, config_settings: Optional[Dict[str, Union[str, List[str]]]] = None, expand_env: bool = False, -) -> (InstallRequirement, str): +) -> Tuple[InstallRequirement, Optional[str]]: """Create an InstallRequirement from a pip-style requirement line. InstallRequirement is a pip internal construct that represents an installable requirement, and is used as an intermediary between the pip command and the resolver. @@ -1407,12 +1424,14 @@ def _validate_pipfile_entry(name: str, pipfile_dict: Dict[str, Any]) -> None: ) -def install_req_from_pipfile(name: str, pipfile: Dict[str, Any]) -> Tuple[Any, Any, str]: +def install_req_from_pipfile( + name: str, pipfile: Union[str, Dict[str, Any]] +) -> Tuple[Any, Any, str]: """ Creates an InstallRequirement from a name and a pipfile entry. Enhanced to handle environment variables within VCS URLs. """ - _pipfile = {} + _pipfile: Dict[str, Any] = {} vcs = None if hasattr(pipfile, "keys"): diff --git a/pipenv/utils/fileutils.py b/pipenv/utils/fileutils.py index 0af0d927e9..ee716fc2a8 100644 --- a/pipenv/utils/fileutils.py +++ b/pipenv/utils/fileutils.py @@ -8,7 +8,7 @@ from contextlib import contextmanager from pathlib import Path, PureWindowsPath from tempfile import TemporaryDirectory -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Iterator, Optional from urllib import parse as urllib_parse from urllib import request as urllib_request from urllib.parse import quote @@ -53,7 +53,7 @@ def url_to_path(url: str) -> Path: if os.name == "nt": # code taken from click _winconsole.py - from ctypes import create_unicode_buffer, windll + from ctypes import create_unicode_buffer, windll # type: ignore[attr-defined] def get_long_path(short_path: str) -> str: BUFFER_SIZE = 500 @@ -63,7 +63,7 @@ def get_long_path(short_path: str) -> str: return buffer.value -def normalize_path(path: str) -> str: +def normalize_path(path: str | Path) -> str: """Return a case-normalized absolute variable-expanded path.""" # Convert to string if it's a Path object path_str = str(path) @@ -195,8 +195,7 @@ def open_file(link, session: Optional["PipSession"] = None, stream: bool = False @contextmanager -def temp_path(): - # type: () -> Iterator[None] +def temp_path() -> Iterator[None]: """A context manager which allows the ability to set sys.path temporarily. >>> path_from_virtualenv = load_path("/path/to/venv/bin/python") @@ -228,7 +227,7 @@ def temp_path(): sys.path = list(path) -TRACKED_TEMPORARY_DIRECTORIES = [] +TRACKED_TEMPORARY_DIRECTORIES: list[TemporaryDirectory] = [] def create_tracked_tempdir(*args: Any, **kwargs: Any) -> Path: @@ -246,8 +245,7 @@ def create_tracked_tempdir(*args: Any, **kwargs: Any) -> Path: return Path(tempdir.name) -def check_for_unc_path(path): - # type: (Path) -> bool +def check_for_unc_path(path: Path) -> bool: """Checks to see if a pathlib `Path` object is a unc path or not.""" return bool( os.name == "nt" diff --git a/pipenv/utils/funktools.py b/pipenv/utils/funktools.py index 323757cca4..7fdcfe8325 100644 --- a/pipenv/utils/funktools.py +++ b/pipenv/utils/funktools.py @@ -318,7 +318,7 @@ def _find_icacls_exe(): return None -def set_write_bit(fn: str) -> None: +def set_write_bit(fn: str | Path) -> None: """Set read-write permissions for the current user on the target path. Fail silently if the path doesn't exist. @@ -366,9 +366,9 @@ def set_write_bit(fn: str) -> None: return if not path.is_dir(): - for path in [fn, Path(fn).parent]: + for target in [fn, Path(fn).parent]: try: - os.chflags(path, 0) + os.chflags(target, 0) # type: ignore[attr-defined] except AttributeError: # noqa: PERF203 pass return None diff --git a/pipenv/utils/locking.py b/pipenv/utils/locking.py index 91fe758128..f711f1b386 100644 --- a/pipenv/utils/locking.py +++ b/pipenv/utils/locking.py @@ -129,7 +129,9 @@ def prepare_lockfile( @contextmanager -def atomic_open_for_write(target, binary=False, newline=None, encoding=None) -> None: +def atomic_open_for_write( + target, binary=False, newline=None, encoding=None +) -> Iterator[Any]: """Atomically open `target` for writing. This is based on Lektor's `atomic_open()` utility, but simplified a lot to handle only writing, and skip many multiprocess/thread edge cases @@ -228,7 +230,7 @@ class Lockfile: ) _requirements: Optional[List[Any]] = field(default_factory=list) _dev_requirements: Optional[List[Any]] = field(default_factory=list) - projectfile: ProjectFile = None + projectfile: Optional[ProjectFile] = None newlines: str = DEFAULT_NEWLINES def __post_init__(self): @@ -325,11 +327,9 @@ def lockfile_from_pipfile(cls, pipfile_path): def load_projectfile( cls, path: Optional[str] = None, create: bool = True, data: Optional[Dict] = None ) -> "ProjectFile": - if not path: - path = os.curdir - path = Path(path).absolute() - project_path = path if path.is_dir() else path.parent - lockfile_path = path if path.is_file() else project_path / "Pipfile.lock" + abs_path = Path(path or os.curdir).absolute() + project_path = abs_path if abs_path.is_dir() else abs_path.parent + lockfile_path = abs_path if abs_path.is_file() else project_path / "Pipfile.lock" if not project_path.exists(): raise OSError(f"Project does not exist: {project_path.as_posix()}") elif not lockfile_path.exists() and not create: @@ -383,9 +383,7 @@ def from_data( projectfile = ProjectFile( line_ending=DEFAULT_NEWLINES, - location=str( - lockfile_path - ), # Convert to string if ProjectFile expects a string + location=lockfile_path, model=lockfile, ) @@ -402,7 +400,7 @@ def load(cls, path: Optional[str], create: bool = True) -> "Lockfile": projectfile = cls.load_projectfile(path, create=create) except JSONDecodeError: # Convert to Path object and resolve to absolute path - path_obj = Path(path).resolve() + path_obj = Path(path or os.curdir).resolve() # Determine if the path is a directory or file if path_obj.is_dir(): @@ -422,14 +420,12 @@ def load(cls, path: Optional[str], create: bool = True) -> "Lockfile": # Create Path object from projectfile location lockfile_path = Path(projectfile.location) - # Create instance with required arguments - creation_args = { - "projectfile": projectfile, - "lockfile": projectfile.model, - "newlines": projectfile.line_ending, - "path": lockfile_path, - } - return cls(**creation_args) + return cls( + projectfile=projectfile, + lockfile=projectfile.model, + newlines=projectfile.line_ending, + path=lockfile_path, + ) @classmethod def create(cls, path: Optional[str], create: bool = True) -> "Lockfile": @@ -505,7 +501,7 @@ def get_requirements( from pipenv.utils.dependencies import requirement_from_lockfile if categories: - deps = {} + deps: Dict[str, Any] = {} for category in categories: if category == "packages": category = "default" @@ -565,5 +561,6 @@ def requirements_list(self, category: str) -> List[Dict]: return [] def write(self) -> None: + assert self.projectfile is not None # set in __post_init__ self.projectfile.model = copy.deepcopy(self.lockfile) self.projectfile.write() diff --git a/pipenv/utils/markers.py b/pipenv/utils/markers.py index c5d8bc914a..f3440520a2 100644 --- a/pipenv/utils/markers.py +++ b/pipenv/utils/markers.py @@ -4,7 +4,7 @@ from collections.abc import Mapping, Set from dataclasses import dataclass, fields from functools import reduce -from typing import Optional +from typing import Any, Iterator, List, Optional, Tuple, Type, Union from pipenv.patched.pip._vendor.distlib.util import parse_marker from pipenv.patched.pip._vendor.packaging.markers import InvalidMarker, Marker @@ -68,22 +68,21 @@ def from_pipfile(cls, name, pipfile): return None -def is_instance(item, cls): - # type: (Any, Type) -> bool +def is_instance(item: Any, cls: Type) -> bool: return isinstance(item, cls) or item.__class__.__name__ == cls.__name__ -def _tuplize_version(version): - # type: (str) -> Union[Tuple[()], Tuple[int, ...], Tuple[int, int, str]] - output = [] +def _tuplize_version(version: str) -> Tuple[Any, ...]: + output: List[Any] = [] for idx, part in enumerate(version.split(".")): if part == "*": break if idx in (0, 1): # Only convert the major and minor identifiers into integers (if present), # the patch identifier can include strings like 'b' marking a beta: ex 3.11.0b1 - part = int(part) - output.append(part) + output.append(int(part)) + else: + output.append(part) return tuple(output) @@ -97,8 +96,7 @@ def _format_version(version) -> str: REPLACE_RANGES = {">": ">=", "<=": "<"} -def _format_pyspec(specifier): - # type: (Union[str, Specifier]) -> Specifier +def _format_pyspec(specifier: Union[str, Specifier]) -> Specifier: if isinstance(specifier, str): if not specifier.startswith(tuple(Specifier._operators.keys())): specifier = f"=={specifier}" @@ -164,8 +162,7 @@ def _get_specs(specset): # TODO: Rename this to something meaningful -def _group_by_op(specs): - # type: (Union[Set[Specifier], SpecifierSet]) -> Iterator +def _group_by_op(specs: Union[Set[Specifier], SpecifierSet]) -> Iterator: specs = [_get_specs(x) for x in list(specs)] flattened = [ ((op, len(version) > 2), version) for spec in specs for op, version in spec @@ -176,8 +173,7 @@ def _group_by_op(specs): # TODO: rename this to something meaningful -def normalize_specifier_set(specs): - # type: (Union[str, SpecifierSet]) -> Optional[Set[Specifier]] +def normalize_specifier_set(specs: Union[str, SpecifierSet]) -> Optional[Set[Specifier]]: """Given a specifier set, a string, or an iterable, normalize the specifiers. @@ -209,8 +205,7 @@ def normalize_specifier_set(specs): # TODO: Check if this is used by anything public otherwise make it private # And rename it to something meaningful -def get_sorted_version_string(version_set): - # type: (Set[AnyStr]) -> AnyStr +def get_sorted_version_string(version_set: Set[Any]) -> str: version_list = [ _format_version(version) for version in sorted(version_set, key=_version_sort_key) @@ -277,8 +272,7 @@ def cleanup_pyspecs(specs, joiner="or"): ) -def _version_sort_key(value): - # type: (Union[AnyStr, Tuple]) -> Tuple +def _version_sort_key(value: Union[str, Tuple]) -> Tuple: value = _format_version(value) first = value.split(",")[0].strip() try: @@ -288,8 +282,7 @@ def _version_sort_key(value): # TODO: Rename this to something meaningful -def fix_version_tuple(version_tuple): - # type: (Tuple[AnyStr, AnyStr]) -> Tuple[AnyStr, AnyStr] +def fix_version_tuple(version_tuple: Tuple[str, Tuple[int, int]]) -> Tuple[str, Tuple[int, int]]: op, version = version_tuple max_major = max(MAX_VERSIONS.keys()) if version[0] > max_major: @@ -305,15 +298,13 @@ def fix_version_tuple(version_tuple): return (op, version) -def _ensure_marker(marker): - # type: (Union[str, Marker]) -> Marker +def _ensure_marker(marker: Union[str, Marker]) -> Marker: if not is_instance(marker, Marker): return Marker(str(marker)) return marker -def gen_marker(mkr): - # type: (List[str]) -> Marker +def gen_marker(mkr: List[str]) -> Marker: m = Marker("python_version == '1'") m._markers.pop() m._markers.append(mkr) @@ -499,8 +490,7 @@ def contains_pyversion(marker): return _markers_contains_pyversion(marker._markers) -def _split_specifierset_str(specset_str, prefix="=="): - # type: (str, str) -> Set[Specifier] +def _split_specifierset_str(specset_str: str, prefix: str = "==") -> Set[Specifier]: """Take a specifierset string and split it into a list to join for specifier sets. @@ -544,8 +534,7 @@ def _get_specifiers_from_markers(marker_item): return specifiers -def get_specset(marker_list): - # type: (List) -> Optional[SpecifierSet] +def get_specset(marker_list: List) -> Optional[SpecifierSet]: specset = set() for marker_parts in marker_list: if not isinstance(marker_parts, str): @@ -624,16 +613,16 @@ def _contains_micro_version(version_string): return re.search(r"\d+\.\d+\.\d+", version_string) is not None -def merge_markers(m1, m2): - # type: (Marker, Marker) -> Optional[Marker] +def merge_markers(m1: Marker, m2: Marker) -> Optional[Marker]: if not all((m1, m2)): return next(iter(v for v in (m1, m2) if v), None) _markers = [str(_ensure_marker(marker)) for marker in (m1, m2)] - marker_str = " and ".join([normalize_marker_str(m) for m in _markers if m]) + # normalize_marker_str only returns None for falsy input, filtered here. + marker_str = " and ".join([normalize_marker_str(m) for m in _markers if m]) # type: ignore[misc] return _ensure_marker(normalize_marker_str(marker_str)) -def normalize_marker_str(marker) -> str: +def normalize_marker_str(marker) -> Optional[str]: marker_str = "" if not marker: return None diff --git a/pipenv/utils/pip.py b/pipenv/utils/pip.py index e755b50cfe..35ec0a2861 100644 --- a/pipenv/utils/pip.py +++ b/pipenv/utils/pip.py @@ -1,7 +1,7 @@ import os import tempfile from pathlib import Path -from typing import List, Optional +from typing import Dict, List, Optional from pipenv.patched.pip._internal.utils.misc import get_runnable_pip from pipenv.utils import err @@ -216,7 +216,7 @@ def get_pip_args( src_dir: Optional[str] = None, extra_pip_args: Optional[List] = None, ) -> List[str]: - arg_map = { + arg_map: Dict[str, List[str]] = { "pre": ["--pre"], "verbose": ["--verbose"], "upgrade": ["--upgrade"], @@ -224,12 +224,12 @@ def get_pip_args( "no_build_isolation": ["--no-build-isolation"], "no_use_pep517": ["--no-use-pep517"], "no_deps": ["--no-deps"], - "src_dir": src_dir, + "src_dir": ["--src", src_dir] if src_dir else [], } arg_set = ["--no-input"] if project.settings.get("disable_pip_input", True) else [] - for key in arg_map: + for key, args in arg_map.items(): if key in locals() and locals().get(key): - arg_set.extend(arg_map.get(key)) + arg_set.extend(args) arg_set += extra_pip_args or [] return list(dict.fromkeys(arg_set)) diff --git a/pipenv/utils/pipfile.py b/pipenv/utils/pipfile.py index 2d8995b7bf..fb2e82e4b9 100644 --- a/pipenv/utils/pipfile.py +++ b/pipenv/utils/pipfile.py @@ -269,9 +269,10 @@ def ensure_pipfile( project.pipfile.write_toml(p) -def reorder_source_keys(data): - # type: (tomlkit.toml_document.TOMLDocument) -> tomlkit.toml_document.TOMLDocument - sources = [] # type: sources_type +def reorder_source_keys( + data: tomlkit.toml_document.TOMLDocument, +) -> tomlkit.toml_document.TOMLDocument: + sources: list[Any] = [] for source_key in ["source", "sources"]: sources.extend(data.get(source_key, tomlkit.aot()).value) new_source_aot = tomlkit.aot() @@ -294,7 +295,9 @@ class ProjectFile: model: Any | None = field(default_factory=dict) @classmethod - def read(cls, location: str, model_cls, invalid_ok: bool = False) -> ProjectFile: + def read( + cls, location: str | Path, model_cls, invalid_ok: bool = False + ) -> ProjectFile: # Convert string location to Path path = Path(location) if not path.exists() and not invalid_ok: @@ -311,8 +314,7 @@ def read(cls, location: str, model_cls, invalid_ok: bool = False) -> ProjectFile return cls(location=path, line_ending=line_ending, model=model) def write(self) -> None: - kwargs = {"encoding": "utf-8", "newline": self.line_ending} - with self.location.open("w", **kwargs) as f: + with self.location.open("w", encoding="utf-8", newline=self.line_ending) as f: if self.model: self.model.dump(f) @@ -326,8 +328,7 @@ def dumps(self) -> str: class PipfileLoader(pipfiles.Pipfile): @classmethod - def validate(cls, data): - # type: (tomlkit.toml_document.TOMLDocument) -> None + def validate(cls, data: tomlkit.toml_document.TOMLDocument) -> None: for key, klass in pipfiles.PIPFILE_SECTIONS.items(): if key not in data or key == "sources": continue @@ -335,8 +336,9 @@ def validate(cls, data): klass.validate(data[key]) @classmethod - def ensure_package_sections(cls, data): - # type: (tomlkit.toml_document.TOMLDocument[Text, Any]) -> tomlkit.toml_document.TOMLDocument[Text, Any] + def ensure_package_sections( + cls, data: tomlkit.toml_document.TOMLDocument + ) -> tomlkit.toml_document.TOMLDocument: """Ensure that all pipfile package sections are present in the given toml document. @@ -365,8 +367,7 @@ def populate_source(cls, source): return source @classmethod - def load(cls, f, encoding=None): - # type: (Any, Text) -> PipfileLoader + def load(cls, f: Any, encoding: str | None = None) -> PipfileLoader: content = f.read() if encoding is not None: content = content.decode(encoding) @@ -389,16 +390,14 @@ def load(cls, f, encoding=None): instance._data = dict(instance._data) return instance - def __contains__(self, key): - # type: (Text) -> bool + def __contains__(self, key: str) -> bool: if key not in self._data: package_keys = self._data.get("packages", {}).keys() dev_package_keys = self._data.get("dev-packages", {}).keys() return any(key in pkg_list for pkg_list in (package_keys, dev_package_keys)) return True - def __getattribute__(self, key): - # type: (Text) -> Any + def __getattribute__(self, key: str) -> Any: if key == "source": return self._data[key] return super().__getattribute__(key) @@ -453,7 +452,7 @@ def extended_keys(self): def get_deps(self, dev=False, only=True): from pipenv.utils.dependencies import merge_items - deps = {} # type: Dict[Text, Dict[Text, Union[List[Text], Text]]] + deps: dict[str, dict[str, list[str] | str]] = {} if dev: deps.update(dict(self.pipfile._data.get("dev-packages", {}))) if only: @@ -499,8 +498,8 @@ def __getattr__(self, k, *args, **kwargs): return retval @property - def requires_python(self): - # type: () -> bool + def requires_python(self) -> str | None: + assert self.pipfile is not None return getattr( self.pipfile.requires, "python_version", @@ -508,13 +507,12 @@ def requires_python(self): ) @property - def allow_prereleases(self): - # type: () -> bool + def allow_prereleases(self) -> bool: + assert self.pipfile is not None return self.pipfile.get("pipenv", {}).get("allow_prereleases", False) @classmethod - def read_projectfile(cls, path): - # type: (Text) -> ProjectFile + def read_projectfile(cls, path: str | Path) -> ProjectFile: """Read the specified project file and provide an interface for writing/updating. @@ -526,7 +524,9 @@ def read_projectfile(cls, path): return pf @classmethod - def load_projectfile(cls, path: str, create: bool = False) -> ProjectFile: + def load_projectfile( + cls, path: str | Path, create: bool = False + ) -> ProjectFile: """...""" if not path: @@ -542,17 +542,15 @@ def load_projectfile(cls, path: str, create: bool = False) -> ProjectFile: return cls.read_projectfile(pipfile_path) @classmethod - def load(cls, path: str, create: bool = False) -> PlettePipfile: + def load(cls, path: str | Path, create: bool = False) -> PlettePipfile: """...""" projectfile = cls.load_projectfile(path, create=create) - pipfile = projectfile.model - creation_args = { - "projectfile": projectfile, - "pipfile": pipfile, - "path": Path(projectfile.location), - } - return cls(**creation_args) + return cls( + projectfile=projectfile, + pipfile=projectfile.model, + path=Path(projectfile.location), + ) # ============================================================================ @@ -1375,6 +1373,4 @@ def _normalize_section(section): data[category] = _normalize_section(values) content = json.dumps(data, sort_keys=True, separators=(",", ":")) - if isinstance(content, str): - content = content.encode("utf-8") - return hashlib.sha256(content).hexdigest() + return hashlib.sha256(content.encode("utf-8")).hexdigest() diff --git a/pipenv/utils/prefetch.py b/pipenv/utils/prefetch.py index 126eff8c75..67d012edef 100644 --- a/pipenv/utils/prefetch.py +++ b/pipenv/utils/prefetch.py @@ -372,7 +372,7 @@ def _download_and_verify( _LOGGER.debug("prefetch GET %s failed: %s", url, exc) return False try: - status = getattr(response, "status", None) or getattr( + status: Any = getattr(response, "status", None) or getattr( response, "status_code", 0 ) if int(status) != 200: diff --git a/pipenv/utils/pylock.py b/pipenv/utils/pylock.py index e1678f04f9..ddfbd3b39b 100644 --- a/pipenv/utils/pylock.py +++ b/pipenv/utils/pylock.py @@ -59,7 +59,7 @@ class PylockFile: def from_lockfile( cls, lockfile_path: Union[str, Path], - pylock_path: Union[str, Path] = None, + pylock_path: Optional[Union[str, Path]] = None, dev_groups: Optional[List[str]] = None, ) -> "PylockFile": """Create a PylockFile from a Pipfile.lock file. @@ -98,7 +98,7 @@ def from_lockfile( raise ValueError(f"Invalid Pipfile.lock file: {e}") # Create the basic pylock.toml structure - pylock_data = { + pylock_data: Dict[str, Any] = { "lock-version": "1.0", "environments": [], "extras": [], @@ -144,7 +144,7 @@ def from_lockfile( # Process packages default_packages = lockfile_data.get("default", {}) - default_entries = {} + default_entries: Dict[str, Any] = {} for section in ["default", "develop"]: packages = lockfile_data.get(section, {}) for name, package_data in packages.items(): @@ -163,7 +163,7 @@ def from_lockfile( ) continue - package = {"name": name} + package: Dict[str, Any] = {"name": name} if section == "default": default_entries[name] = package @@ -330,7 +330,7 @@ def from_path(cls, path: Union[str, Path]) -> "PylockFile": def from_pyproject( cls, pyproject_path: Union[str, Path], - pylock_path: Union[str, Path] = None, + pylock_path: Optional[Union[str, Path]] = None, ) -> "PylockFile": """Create a PylockFile skeleton from a pyproject.toml file. @@ -493,7 +493,7 @@ def write(self) -> None: try: # Ensure all values are properly formatted for TOML # Create a deep copy of the data to avoid modifying the original - data_copy = {} + data_copy: Dict[str, Any] = {} for key, value in self.data.items(): if isinstance(value, dict): data_copy[key] = value.copy() @@ -872,7 +872,7 @@ def convert_to_pipenv_lockfile( A dictionary in Pipfile.lock format """ # Create the basic structure - lockfile = { + lockfile: Dict[str, Any] = { "_meta": { "hash": {"sha256": ""}, # We don't have a hash in pylock.toml "pipfile-spec": 6, @@ -940,7 +940,7 @@ def convert_to_pipenv_lockfile( package_entry["version"] = "*" # Add hashes if present - hashes = [] + hashes: List[str] = [] if "wheels" in package: hashes.extend( f"sha256:{wheel['hashes']['sha256']}" @@ -1081,7 +1081,7 @@ def _pylock_source_from_lockfile_entry(package_data: Dict[str, Any]) -> Dict[str if "path" not in package_data and "file" not in package_data: return {} - location = package_data.get("path") or package_data.get("file") + location = package_data.get("path") or package_data["file"] if "file" in package_data or location.lower().endswith(ARCHIVE_SUFFIXES): archive = {"path" if "path" in package_data else "url": location} hashes = { @@ -1151,7 +1151,7 @@ def _lockfile_source_from_pylock_package(package: Dict[str, Any]) -> Dict[str, A return {} -def find_pylock_file(directory: Union[str, Path] = None) -> Optional[Path]: +def find_pylock_file(directory: Optional[Union[str, Path]] = None) -> Optional[Path]: """Find a pylock.toml file in the given directory. Args: diff --git a/pipenv/utils/resolver.py b/pipenv/utils/resolver.py index e598b8123d..b36b9efca0 100644 --- a/pipenv/utils/resolver.py +++ b/pipenv/utils/resolver.py @@ -11,7 +11,7 @@ import warnings from concurrent.futures import ThreadPoolExecutor from pathlib import Path -from typing import Any, Dict, List, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union from pipenv import environments from pipenv.exceptions import ResolutionFailure @@ -392,15 +392,15 @@ def create( cls, deps: Dict[str, str], project, - index_lookup: Dict[str, str] = None, - markers_lookup: Dict[str, str] = None, - sources: List[str] = None, - req_dir: str = None, + index_lookup: Optional[Dict[str, str]] = None, + markers_lookup: Optional[Dict[str, str]] = None, + sources: Optional[List[str]] = None, + req_dir: Optional[Union[str, Path]] = None, clear: bool = False, pre: bool = False, - pipfile_category: str = None, - resolved_default_deps: Dict[str, Any] = None, - extra_pip_args: List[str] = None, + pipfile_category: Optional[str] = None, + resolved_default_deps: Optional[Dict[str, Any]] = None, + extra_pip_args: Optional[List[str]] = None, ) -> "Resolver": if not req_dir: req_dir = create_tracked_tempdir(suffix="-requirements", prefix="pipenv-") @@ -1137,8 +1137,8 @@ def clean_results(self) -> List[Dict[str, Any]]: # Global cache for resolution results to avoid repeated expensive subprocess calls -_resolution_cache = {} -_resolution_cache_timestamp = {} +_resolution_cache: Dict[str, Any] = {} +_resolution_cache_timestamp: Dict[str, float] = {} def _generate_resolution_cache_key( diff --git a/pipenv/utils/toml.py b/pipenv/utils/toml.py index 5630d0f857..7232d4b5a3 100644 --- a/pipenv/utils/toml.py +++ b/pipenv/utils/toml.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Any, Dict, Union from pipenv.vendor.plette.models import Package, PackageCollection from pipenv.vendor.tomlkit.container import Container, OutOfOrderTableProxy @@ -111,8 +111,7 @@ def convert_toml_table(section): return parsed -def tomlkit_value_to_python(toml_value): - # type: (Union[Array, AoT, TOML_DICT_TYPES, Item]) -> Union[List, Dict] +def tomlkit_value_to_python(toml_value: Union[Array, AoT, TOML_DICT_TYPES, Item]) -> Any: value_type = type(toml_value).__name__ if ( isinstance(toml_value, TOML_DICT_OBJECTS + (dict,)) @@ -132,12 +131,11 @@ def tomlkit_value_to_python(toml_value): return toml_value -def tomlkit_dict_to_python(toml_dict): - # type: (TOML_DICT_TYPES) -> Dict +def tomlkit_dict_to_python(toml_dict: TOML_DICT_TYPES) -> Dict: value_type = type(toml_dict).__name__ if toml_dict is None: raise TypeError("Invalid type NoneType when converting toml dict to python") - converted = None # type: Optional[Dict] + converted: Any = None if isinstance(toml_dict, (InlineTable, Table)) or value_type in ( "InlineTable", "Table", diff --git a/pipenv/utils/unpack.py b/pipenv/utils/unpack.py index 1ab1085ea4..d042f87cdb 100644 --- a/pipenv/utils/unpack.py +++ b/pipenv/utils/unpack.py @@ -44,7 +44,7 @@ def unpack_url( verbosity: int, download_dir: Optional[str] = None, hashes: Optional[Hashes] = None, -) -> Optional[File]: +) -> File: """Unpack link into location, downloading if required. :param hashes: A Hashes object, one of whose embedded hashes must match, diff --git a/pyproject.toml b/pyproject.toml index 2354be87bf..9f820a846e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -333,6 +333,8 @@ showcontent = true [tool.mypy] ignore_missing_imports = true follow_imports = "skip" -html_report = "mypyhtml" -python_version = "3.7" -mypy_path = "typeshed/pyi:typeshed/imports" +python_version = "3.10" +files = [ "pipenv" ] +exclude = [ "^pipenv/vendor/", "^pipenv/patched/" ] +# Bodies of unannotated functions are not checked yet; silence the reminder. +disable_error_code = [ "annotation-unchecked" ]