From e88d61367c8e33b8d1f757fae01bdadbd5e2ac43 Mon Sep 17 00:00:00 2001 From: Seth Morton Date: Sat, 21 Dec 2024 08:50:49 -0800 Subject: [PATCH] Address errors found by mypy.stubtype These errors fell into one of the following categories - Stubs that existed in the .pyi file that had no corresponding function in the .py file (likely becuase functionality was removed but the stub was not updated) - The annotation in the .pyi file did not match the annotation in the .py file - The .pyi file simply had no entry for a function in the .py file - Annotations existed, but they were in the .py file and not the .pyi file The last category is the most interesting. The behavior of mypy is to prioritize the .pyi file if it exists, which means that if there is a .pyi file any annotations in the .py file are ignored by mypy. This behavior was surprising, since the expectation is that the annotations would be the union of the .pyi and .py file, but instead if pulls from only the .pyi file (likely to avoid conflicts if annotations mismatch). Going forward, hopefully the presence of the mypy.stubtype check will ensure that new code adds annotations to the .pyi file instead of in the .py file. --- path/__init__.py | 73 ++------------ path/__init__.pyi | 240 ++++++++++++++++++++++++++++++++++++++++++---- path/classes.py | 2 +- path/matchers.py | 13 --- path/matchers.pyi | 9 +- 5 files changed, 238 insertions(+), 99 deletions(-) diff --git a/path/__init__.py b/path/__init__.py index fc002f83..01c9b6a9 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -24,7 +24,6 @@ from __future__ import annotations -import builtins import contextlib import datetime import errno @@ -52,19 +51,6 @@ with contextlib.suppress(ImportError): import grp -from typing import ( - TYPE_CHECKING, - Callable, - Iterator, - overload, -) - -if TYPE_CHECKING: - from _typeshed import ( - OpenBinaryMode, - OpenTextMode, - ) - from . import classes, masks, matchers from .compat.py38 import removeprefix, removesuffix @@ -232,7 +218,7 @@ def cwd(cls): return cls(os.getcwd()) @classmethod - def home(cls) -> Path: + def home(cls): return cls(os.path.expanduser('~')) # @@ -660,45 +646,6 @@ def bytes(self): with self.open('rb') as f: return f.read() - @overload - def chunks( - self, - size: int, - mode: OpenTextMode = ..., - buffering: int = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] | None = ..., - ) -> Iterator[str]: ... - - @overload - def chunks( - self, - size: int, - mode: OpenBinaryMode, - buffering: int = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] | None = ..., - ) -> Iterator[builtins.bytes]: ... - - @overload - def chunks( - self, - size: int, - mode: str, - buffering: int = ..., - encoding: str | None = ..., - errors: str | None = ..., - newline: str | None = ..., - closefd: bool = ..., - opener: Callable[[str, int], int] | None = ..., - ) -> Iterator[str | builtins.bytes]: ... - def chunks(self, size, *args, **kwargs): """Returns a generator yielding chunks of the file, so it can be read piece by piece with a simple for loop. @@ -742,12 +689,12 @@ def read_bytes(self): def write_text( self, - text: str, - encoding: str | None = None, - errors: str = 'strict', - linesep: str | None = os.linesep, - append: bool = False, - ) -> None: + text, + encoding=None, + errors='strict', + linesep=os.linesep, + append=False, + ): r"""Write the given text to this file. The default behavior is to overwrite any existing file; @@ -1199,7 +1146,7 @@ def renames(self, new): os.renames(self, new) return self._next_class(new) - def replace(self, target_or_old: Path | str, *args) -> Path: + def replace(self, target_or_old, *args): """ Replace a path or substitute substrings. @@ -1314,7 +1261,7 @@ def remove_p(self): # --- Links - def hardlink_to(self, target: str) -> None: + def hardlink_to(self, target): """ Create a hard link at self, pointing to target. @@ -1330,7 +1277,7 @@ def link(self, newpath): os.link(self, newpath) return self._next_class(newpath) - def symlink_to(self, target: str, target_is_directory: bool = False) -> None: + def symlink_to(self, target, target_is_directory=False): """ Create a symbolic link at self, pointing to target. diff --git a/path/__init__.pyi b/path/__init__.pyi index e6a980be..e21eaf9e 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -4,22 +4,45 @@ import builtins import contextlib import os import sys +from datetime import datetime +from io import ( + BufferedRandom, + BufferedReader, + BufferedWriter, + FileIO, + TextIOWrapper, +) +from numbers import Number from types import ModuleType, TracebackType from typing import ( IO, Any, AnyStr, + BinaryIO, Callable, Generator, Iterable, Iterator, + Literal, + overload, ) +from typing_extensions import Never from _typeshed import ( + ExcInfo, + FileDescriptorOrPath, + OpenBinaryMode, + OpenBinaryModeReading, + OpenBinaryModeUpdating, + OpenBinaryModeWriting, + OpenTextMode, Self, ) from . import classes +from . import masks + +__all__ = ['Path', 'TempDir'] # Type for the match argument for several methods _Match = str | Callable[[str], bool] | Callable[[Path], bool] | None @@ -39,6 +62,7 @@ class Traversal: class Path(str): module: Any + def __new__(cls: type[Self], other: str = ...) -> Self: ... def __init__(self, other: Any = ...) -> None: ... @classmethod def using_module(cls, module: ModuleType) -> type[Path]: ... @@ -48,9 +72,7 @@ class Path(str): def __repr__(self) -> str: ... def __add__(self: Self, more: str) -> Self: ... def __radd__(self: Self, other: str) -> Self: ... - def __div__(self: Self, rel: str) -> Self: ... def __truediv__(self: Self, rel: str) -> Self: ... - def __rdiv__(self: Self, rel: str) -> Self: ... def __rtruediv__(self: Self, rel: str) -> Self: ... def __enter__(self: Self) -> Self: ... def __exit__( @@ -59,6 +81,10 @@ class Path(str): exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: ... + @classmethod + def cwd(cls: type[Self]) -> Self: ... + @classmethod + def home(cls: type[Self]) -> Self: ... def absolute(self: Self) -> Self: ... def normcase(self: Self) -> Self: ... def normpath(self: Self) -> Self: ... @@ -120,12 +146,133 @@ class Path(str): ) -> bool: ... def glob(self: Self, pattern: str) -> list[Self]: ... def iglob(self: Self, pattern: str) -> Iterator[Self]: ... + @overload + def chunks( + self, + size: int, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] | None = ..., + ) -> Iterator[str]: ... + @overload + def chunks( + self, + size: int, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] | None = ..., + ) -> Iterator[builtins.bytes]: ... + @overload + def chunks( + self, + size: int, + mode: str, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] | None = ..., + ) -> Iterator[str | builtins.bytes]: ... + @overload + def open( + file: FileDescriptorOrPath, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> TextIOWrapper: ... + @overload + def open( + file: FileDescriptorOrPath, + mode: OpenBinaryMode, + buffering: Literal[0], + encoding: None = ..., + errors: None = ..., + newline: None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> FileIO: ... + @overload + def open( + file: FileDescriptorOrPath, + mode: OpenBinaryModeUpdating, + buffering: Literal[-1, 1] = ..., + encoding: None = ..., + errors: None = ..., + newline: None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> BufferedRandom: ... + @overload + def open( + file: FileDescriptorOrPath, + mode: OpenBinaryModeWriting, + buffering: Literal[-1, 1] = ..., + encoding: None = ..., + errors: None = ..., + newline: None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> BufferedWriter: ... + @overload + def open( + file: FileDescriptorOrPath, + mode: OpenBinaryModeReading, + buffering: Literal[-1, 1] = ..., + encoding: None = ..., + errors: None = ..., + newline: None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> BufferedReader: ... + @overload + def open( + file: FileDescriptorOrPath, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: None = ..., + errors: None = ..., + newline: None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> BinaryIO: ... + @overload + def open( + file: FileDescriptorOrPath, + mode: str, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = True, + opener: Callable[[str, int], int] | None = ..., + ) -> IO[Any]: ... def bytes(self) -> builtins.bytes: ... def write_bytes(self, bytes: builtins.bytes, append: bool = ...) -> None: ... def read_text( self, encoding: str | None = ..., errors: str | None = ... ) -> str: ... def read_bytes(self) -> builtins.bytes: ... + def write_text( + self, + text: str, + encoding: str | None = ..., + errors: str = ..., + linesep: str | None = ..., + append: bool = ..., + ) -> None: ... def lines( self, encoding: str | None = ..., @@ -150,9 +297,11 @@ class Path(str): def islink(self) -> bool: ... def ismount(self) -> bool: ... def samefile(self, other: str) -> bool: ... + def set_atime(self, value: Number | datetime) -> None: ... def getatime(self) -> float: ... @property def atime(self) -> float: ... + def set_mtime(self, value: Number | datetime) -> None: ... def getmtime(self) -> float: ... @property def mtime(self) -> float: ... @@ -162,6 +311,8 @@ class Path(str): def getsize(self) -> int: ... @property def size(self) -> int: ... + @property + def permissions(self) -> masks.Permissions: ... def access( self, mode: int, @@ -170,13 +321,14 @@ class Path(str): effective_ids: bool = ..., follow_symlinks: bool = ..., ) -> bool: ... - def stat(self) -> os.stat_result: ... + def stat(self, *, follow_symlinks: bool = ...) -> os.stat_result: ... def lstat(self) -> os.stat_result: ... def get_owner(self) -> str: ... @property def owner(self) -> str: ... if sys.platform != 'win32': + def group(self, *, follow_symlinks: bool = ...) -> str: ... def statvfs(self) -> os.statvfs_result: ... def pathconf(self, name: str | int) -> int: ... @@ -195,6 +347,7 @@ class Path(str): def rename(self: Self, new: str) -> Self: ... def renames(self: Self, new: str) -> Self: ... + def replace(self: Self, target_or_old: str, *args) -> Self: ... def mkdir(self: Self, mode: int = ...) -> Self: ... def mkdir_p(self: Self, mode: int = ...) -> Self: ... def makedirs(self: Self, mode: int = ...) -> Self: ... @@ -209,16 +362,18 @@ class Path(str): def unlink(self: Self) -> Self: ... def unlink_p(self: Self) -> Self: ... def link(self: Self, newpath: str) -> Self: ... + def hardlink_to(self, target: str) -> None: ... def symlink(self: Self, newlink: str | None = ...) -> Self: ... + def symlink_to(self, target: str, target_is_directory: bool = False) -> None: ... def readlink(self: Self) -> Self: ... def readlinkabs(self: Self) -> Self: ... - def copyfile(self, dst: str, *, follow_symlinks: bool = ...) -> str: ... - def copymode(self, dst: str, *, follow_symlinks: bool = ...) -> None: ... - def copystat(self, dst: str, *, follow_symlinks: bool = ...) -> None: ... - def copy(self, dst: str, *, follow_symlinks: bool = ...) -> Any: ... - def copy2(self, dst: str, *, follow_symlinks: bool = ...) -> Any: ... + def copyfile(src: str, dst: str, *, follow_symlinks: bool = ...) -> str: ... + def copymode(src: str, dst: str, *, follow_symlinks: bool = ...) -> None: ... + def copystat(src: str, dst: str, *, follow_symlinks: bool = ...) -> None: ... + def copy(src: str, dst: str, *, follow_symlinks: bool = ...) -> Any: ... + def copy2(src: str, dst: str, *, follow_symlinks: bool = ...) -> Any: ... def copytree( - self, + src: str, dst: str, symlinks: bool = ..., ignore: Callable[[str, list[str]], Iterable[str]] | None = ..., @@ -227,13 +382,53 @@ class Path(str): dirs_exist_ok: bool = ..., ) -> Any: ... def move( - self, dst: str, copy_function: Callable[[str, str], None] = ... + src: str, dst: str, copy_function: Callable[[str, str], None] = ... ) -> Any: ... - def rmtree( - self, - ignore_errors: bool = ..., - onerror: Callable[[Any, Any, Any], Any] | None = ..., - ) -> None: ... + if sys.version_info >= (3, 12): + @overload + def rmtree( + path: str, + ignore_errors: bool, + onerror: Callable[[Callable[..., Any], str, ExcInfo], object], + *, + onexc: None = ..., + dir_fd: int | None = ..., + ) -> None: ... + @overload + def rmtree( + path: str, + ignore_errors: bool = ..., + *, + onerror: Callable[[Callable[..., Any], str, ExcInfo], object], + onexc: None = ..., + dir_fd: int | None = ..., + ) -> None: ... + @overload + def rmtree( + path: str, + ignore_errors: bool = ..., + *, + onerror: None = ..., + onexc: Callable[[Callable[..., Any], str, BaseException], object] + | None = ..., + dir_fd: int | None = ..., + ) -> None: ... + + elif sys.version_info >= (3, 11): + def rmtree( + path: str, + ignore_errors: bool = ..., + onerror: Callable[[Callable[..., Any], str, ExcInfo], object] | None = ..., + *, + dir_fd: int | None = ..., + ) -> None: ... + + else: + def rmtree( + path: str, + ignore_errors: bool = ..., + onerror: Callable[[Callable[..., Any], str, ExcInfo], object] | None = ..., + ) -> None: ... def rmtree_p(self: Self) -> Self: ... def chdir(self) -> None: ... def cd(self) -> None: ... @@ -281,7 +476,7 @@ class ExtantFile(Path): class SpecialResolver: class ResolverScope: def __init__(self, paths: SpecialResolver, scope: str) -> None: ... - def __getattr__(self, class_: str) -> MultiPathType: ... + def __getattr__(self, class_: str) -> _MultiPathType: ... def __init__( self, @@ -293,19 +488,19 @@ class SpecialResolver: multipath: bool = ..., ): ... def __getattr__(self, scope: str) -> ResolverScope: ... - def get_dir(self, scope: str, class_: str) -> MultiPathType: ... + def get_dir(self, scope: str, class_: str) -> _MultiPathType: ... class Multi: @classmethod - def for_class(cls, path_cls: type) -> type[MultiPathType]: ... + def for_class(cls, path_cls: type) -> type[_MultiPathType]: ... @classmethod - def detect(cls, input: str) -> MultiPathType: ... + def detect(cls, input: str) -> _MultiPathType: ... def __iter__(self) -> Iterator[Path]: ... @classes.ClassProperty @classmethod def _next_class(cls) -> type[Path]: ... -class MultiPathType(Multi, Path): +class _MultiPathType(Multi, Path): pass class TempDir(Path): @@ -318,7 +513,7 @@ class TempDir(Path): prefix: AnyStr | None = ..., dir: AnyStr | os.PathLike[AnyStr] | None = ..., ) -> Self: ... - def __init__(self) -> None: ... + def __init__(self, *args, **kwargs) -> None: ... def __enter__(self) -> Path: ... # type: ignore[explicit-override, override] def __exit__( self, @@ -328,5 +523,8 @@ class TempDir(Path): ) -> None: ... class Handlers: + def strict(msg: str) -> Never: ... # type: ignore[misc] + def warn(msg: str) -> None: ... # type: ignore[misc] + def ignore(msg: str) -> None: ... # type: ignore[misc] @classmethod def _resolve(cls, param: str | Callable[[str], None]) -> Callable[[str], None]: ... diff --git a/path/classes.py b/path/classes.py index b6101d0a..f36da387 100644 --- a/path/classes.py +++ b/path/classes.py @@ -2,7 +2,7 @@ class ClassProperty(property): - def __get__(self, cls, owner): + def __get__(self, cls, owner=None): return self.fget.__get__(None, owner)() diff --git a/path/matchers.py b/path/matchers.py index 20ca92e2..25c252eb 100644 --- a/path/matchers.py +++ b/path/matchers.py @@ -2,19 +2,6 @@ import fnmatch import ntpath -from typing import Any, overload - - -@overload -def load(param: None) -> Null: ... - - -@overload -def load(param: str) -> Pattern: ... - - -@overload -def load(param: Any) -> Any: ... def load(param): diff --git a/path/matchers.pyi b/path/matchers.pyi index 4c4925d3..80acd0b1 100644 --- a/path/matchers.pyi +++ b/path/matchers.pyi @@ -1,11 +1,18 @@ from __future__ import annotations -from typing import Callable +from typing import Any, Callable, overload from typing_extensions import Literal from path import Path +@overload +def load(param: None) -> Null: ... +@overload +def load(param: str) -> Pattern: ... +@overload +def load(param: Any) -> Any: ... + class Base: pass