diff --git a/.coveragerc b/.coveragerc index 4b078827..13651a64 100644 --- a/.coveragerc +++ b/.coveragerc @@ -9,3 +9,7 @@ disable_warnings = [report] show_missing = True +exclude_also = + # jaraco/skeleton#97 + @overload + if TYPE_CHECKING: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b8224099..a15c74a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,40 +1,31 @@ name: tests -on: [push, pull_request] +on: + merge_group: + push: + branches-ignore: + # temporary GH branches relating to merge queues (jaraco/skeleton#93) + - gh-readonly-queue/** + tags: + # required if branches-ignore is supplied (jaraco/skeleton#103) + - '**' + pull_request: permissions: contents: read env: - # Environment variables to support color support (jaraco/skeleton#66): - # Request colored output from CLI tools supporting it. Different tools - # interpret the value differently. For some, just being set is sufficient. - # For others, it must be a non-zero integer. For yet others, being set - # to a non-empty value is sufficient. For tox, it must be one of - # , 0, 1, false, no, off, on, true, yes. The only enabling value - # in common is "1". + # Environment variable to support color support (jaraco/skeleton#66) FORCE_COLOR: 1 - # MyPy's color enforcement (must be a non-zero number) - MYPY_FORCE_COLOR: -42 - # Recognized by the `py` package, dependency of `pytest` (must be "1") - PY_COLORS: 1 - # Make tox-wrapped tools see color requests - TOX_TESTENV_PASSENV: >- - FORCE_COLOR - MYPY_FORCE_COLOR - NO_COLOR - PY_COLORS - PYTEST_THEME - PYTEST_THEME_MODE # Suppress noisy pip warnings PIP_DISABLE_PIP_VERSION_CHECK: 'true' PIP_NO_PYTHON_VERSION_WARNING: 'true' PIP_NO_WARN_SCRIPT_LOCATION: 'true' - # Disable the spinner, noise in GHA; TODO(webknjaz): Fix this upstream - # Must be "1". - TOX_PARALLEL_NO_SPINNER: 1 + # Ensure tests can sense settings about the environment + TOX_OVERRIDE: >- + testenv.pass_env+=GITHUB_*,FORCE_COLOR jobs: @@ -43,7 +34,6 @@ jobs: matrix: python: - "3.8" - - "3.11" - "3.12" platform: - ubuntu-latest @@ -54,43 +44,51 @@ jobs: platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - - python: pypy3.9 + - python: "3.11" + platform: ubuntu-latest + - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.12' }} + continue-on-error: ${{ matrix.python == '3.13' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} allow-prereleases: true - name: Install tox - run: | - python -m pip install tox + run: python -m pip install tox - name: Run run: tox - docs: + collateral: + strategy: + fail-fast: false + matrix: + job: + - diffcov + - docs runs-on: ubuntu-latest - env: - TOXENV: docs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v4 + with: + python-version: 3.x - name: Install tox - run: | - python -m pip install tox - - name: Run - run: tox + run: python -m pip install tox + - name: Eval ${{ matrix.job }} + run: tox -e ${{ matrix.job }} check: # This job does nothing and is only used for the branch protection if: always() needs: - test - - docs + - collateral runs-on: ubuntu-latest @@ -109,14 +107,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: python-version: 3.x - name: Install tox - run: | - python -m pip install tox + run: python -m pip install tox - name: Run run: tox -e release env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af502010..5a4a7e91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ repos: -- repo: https://github.com/psf/black - rev: 22.6.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.8 hooks: - - id: black + - id: ruff + - id: ruff-format diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 053c7287..85dfea9d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,6 +7,10 @@ python: # required boilerplate readthedocs/readthedocs.org#10401 build: - os: ubuntu-22.04 + os: ubuntu-lts-latest tools: - python: "3" + python: latest + # post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114 + jobs: + post_checkout: + - git fetch --unshallow || true diff --git a/NEWS.rst b/NEWS.rst index 4efbedb5..9679c566 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,40 @@ +v16.10.1 +======== + +Bugfixes +-------- + +- Add type annotation for iterdir. (#215) + + +v16.10.0 +======== + +Features +-------- + +- Added .with_name and .with_stem. +- Prefer .suffix to .ext and deprecate .ext. + + +v16.9.0 +======= + +Features +-------- + +- Added ``.iterdir()`` and deprecated ``.listdir()``. (#214) + + +v16.8.0 +======= + +Features +-------- + +- Use '.' as the default path. (#216) + + v16.7.1 ======= diff --git a/README.rst b/README.rst index 87620c33..6141f96c 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ .. image:: https://img.shields.io/pypi/pyversions/path.svg -.. image:: https://github.com/jaraco/path/workflows/tests/badge.svg +.. image:: https://github.com/jaraco/path/actions/workflows/main.yml/badge.svg :target: https://github.com/jaraco/path/actions?query=workflow%3A%22tests%22 :alt: tests @@ -11,14 +11,10 @@ :target: https://github.com/astral-sh/ruff :alt: Ruff -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Code style: Black - .. image:: https://readthedocs.org/projects/path/badge/?version=latest :target: https://path.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2023-informational +.. image:: https://img.shields.io/badge/skeleton-2024-informational :target: https://blog.jaraco.com/skeleton .. image:: https://tidelift.com/badges/package/pypi/path @@ -63,6 +59,8 @@ based on ``path``. Advantages ========== +Path pie provides a superior experience to similar offerings. + Python 3.4 introduced `pathlib `_, which shares many characteristics with ``path``. In particular, @@ -75,9 +73,9 @@ has several advantages over ``pathlib``: - ``path`` implements ``Path`` objects as a subclass of ``str``, and as a result these ``Path`` objects may be passed directly to other APIs that expect simple text representations of paths, whereas with ``pathlib``, one - must first cast values to strings before passing them to APIs unaware of - ``pathlib``. This shortcoming was somewhat `mitigated by PEP 519 - `_, in Python 3.6. + must first cast values to strings before passing them to APIs that do + not honor `PEP 519 `_ + ``PathLike`` interface. - ``path`` give quality of life features beyond exposing basic functionality of a path. ``path`` provides methods like ``rmtree`` (from shlib) and ``remove_p`` (remove a file if it exists), properties like ``.permissions``, @@ -85,16 +83,19 @@ has several advantages over ``pathlib``: - As a PyPI-hosted package, ``path`` is free to iterate faster than a stdlib package. Contributions are welcome and encouraged. -- ``path`` provides a uniform abstraction over its Path object, +- ``path`` provides superior portability using a uniform abstraction + over its single Path object, freeing the implementer to subclass it readily. One cannot subclass a ``pathlib.Path`` to add functionality, but must subclass ``Path``, ``PosixPath``, and ``WindowsPath``, even - if one only wishes to add a ``__dict__`` to the subclass + to do something as simple as to add a ``__dict__`` to the subclass instances. ``path`` instead allows the ``Path.module`` object to be overridden by subclasses, defaulting to the ``os.path``. Even advanced uses of ``path.Path`` that subclass the model do not need to be concerned with - OS-specific nuances. + OS-specific nuances. ``path.Path`` objects are inherently "pure", + not requiring the author to distinguish between pure and non-pure + variants. This path project has the explicit aim to provide compatibility with ``pathlib`` objects where possible, such that a ``path.Path`` @@ -119,10 +120,3 @@ Available as part of the Tidelift Subscription. This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. - -Security Contact -================ - -To report a security vulnerability, please use the -`Tidelift security contact `_. -Tidelift will coordinate the fix and disclosure. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..54f99acb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Security Contact + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. diff --git a/path/__init__.py b/path/__init__.py index 827eebd0..40da95a4 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -21,6 +21,7 @@ # Concatenate paths with / foo_txt = Path("bar") / "foo.txt" """ + from __future__ import annotations import builtins @@ -83,6 +84,7 @@ from . import matchers from . import masks from . import classes +from .compat.py38 import removesuffix __all__ = ['Path', 'TempDir'] @@ -173,7 +175,10 @@ class Path(str): .. seealso:: :mod:`os.path` """ - def __init__(self, other=''): + def __new__(cls, other='.'): + return super().__new__(cls, other) + + def __init__(self, other='.'): if other is None: raise TypeError("Invalid initial value for path: None") with contextlib.suppress(AttributeError): @@ -305,11 +310,28 @@ def stem(self): base, ext = self.module.splitext(self.name) return base + def with_stem(self, stem): + """Return a new path with the stem changed. + + >>> Path('/home/guido/python.tar.gz').with_stem("foo") + Path('/home/guido/foo.gz') + """ + return self.with_name(stem + self.suffix) + @property - def ext(self): + def suffix(self): """The file extension, for example ``'.py'``.""" - f, ext = self.module.splitext(self) - return ext + f, suffix = self.module.splitext(self) + return suffix + + @property + def ext(self): + warnings.warn( + ".ext is deprecated; use suffix", + DeprecationWarning, + stacklevel=2, + ) + return self.suffix def with_suffix(self, suffix): """Return a new path with the file suffix changed (or added, if none) @@ -366,6 +388,14 @@ def drive(self): """, ) + def with_name(self, name): + """Return a new path with the name changed. + + >>> Path('/home/guido/python.tar.gz').with_name("foo.zip") + Path('/home/guido/foo.zip') + """ + return self._next_class(removesuffix(self, self.name) + name) + def splitpath(self): """Return two-tuple of ``.parent``, ``.name``. @@ -503,8 +533,8 @@ def relpathto(self, dest): # --- Listing, searching, walking, and matching - def listdir(self, match=None): - """List of items in this directory. + def iterdir(self, match=None): + """Yields items in this directory. Use :meth:`files` or :meth:`dirs` instead if you want a listing of just files or just subdirectories. @@ -517,7 +547,15 @@ def listdir(self, match=None): .. seealso:: :meth:`files`, :meth:`dirs` """ match = matchers.load(match) - return list(filter(match, (self / child for child in os.listdir(self)))) + return filter(match, (self / child for child in os.listdir(self))) + + def listdir(self, match=None): + warnings.warn( + ".listdir is deprecated; use iterdir", + DeprecationWarning, + stacklevel=2, + ) + return list(self.iterdir(match=match)) def dirs(self, *args, **kwargs): """List of this directory's subdirectories. @@ -526,9 +564,9 @@ def dirs(self, *args, **kwargs): This does not walk recursively into subdirectories (but see :meth:`walkdirs`). - Accepts parameters to :meth:`listdir`. + Accepts parameters to :meth:`iterdir`. """ - return [p for p in self.listdir(*args, **kwargs) if p.isdir()] + return [p for p in self.iterdir(*args, **kwargs) if p.isdir()] def files(self, *args, **kwargs): """List of the files in self. @@ -536,10 +574,10 @@ def files(self, *args, **kwargs): The elements of the list are Path objects. This does not walk into subdirectories (see :meth:`walkfiles`). - Accepts parameters to :meth:`listdir`. + Accepts parameters to :meth:`iterdir`. """ - return [p for p in self.listdir(*args, **kwargs) if p.isfile()] + return [p for p in self.iterdir(*args, **kwargs) if p.isfile()] def walk(self, match=None, errors='strict'): """Iterator over files and subdirs, recursively. @@ -562,7 +600,7 @@ def walk(self, match=None, errors='strict'): match = matchers.load(match) try: - childList = self.listdir() + childList = self.iterdir() except Exception as exc: errors(f"Unable to list directory '{self}': {exc}") return @@ -656,8 +694,7 @@ def open( newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ..., - ) -> TextIOWrapper: - ... + ) -> TextIOWrapper: ... @overload def open( @@ -669,8 +706,7 @@ def open( newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> FileIO: - ... + ) -> FileIO: ... @overload def open( @@ -682,8 +718,7 @@ def open( newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> BufferedRandom: - ... + ) -> BufferedRandom: ... @overload def open( @@ -695,8 +730,7 @@ def open( newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> BufferedReader: - ... + ) -> BufferedReader: ... @overload def open( @@ -708,8 +742,7 @@ def open( newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> BufferedWriter: - ... + ) -> BufferedWriter: ... @overload def open( @@ -721,8 +754,7 @@ def open( newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> BinaryIO: - ... + ) -> BinaryIO: ... @overload def open( @@ -734,8 +766,7 @@ def open( newline: Optional[str] = ..., closefd: bool = ..., opener: Callable[[str, int], int] = ..., - ) -> IO[Any]: - ... + ) -> IO[Any]: ... def open(self, *args, **kwargs): """Open this file and return a corresponding file object. @@ -761,8 +792,7 @@ def chunks( newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ..., - ) -> Iterator[str]: - ... + ) -> Iterator[str]: ... @overload def chunks( @@ -775,8 +805,7 @@ def chunks( newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ..., - ) -> Iterator[builtins.bytes]: - ... + ) -> Iterator[builtins.bytes]: ... @overload def chunks( @@ -789,8 +818,7 @@ def chunks( newline: Optional[str] = ..., closefd: bool = ..., opener: Optional[Callable[[str, int], int]] = ..., - ) -> Iterator[Union[str, builtins.bytes]]: - ... + ) -> Iterator[Union[str, builtins.bytes]]: ... def chunks(self, size, *args, **kwargs): """Returns a generator yielding chunks of the file, so it can @@ -853,8 +881,7 @@ def write_text( errors: str = ..., linesep: Optional[str] = ..., append: bool = ..., - ) -> None: - ... + ) -> None: ... @overload def write_text( @@ -864,8 +891,7 @@ def write_text( errors: str = ..., linesep: Optional[str] = ..., append: bool = ..., - ) -> None: - ... + ) -> None: ... def write_text( self, text, encoding=None, errors='strict', linesep=os.linesep, append=False @@ -1519,7 +1545,7 @@ def merge_tree( dst = self._next_class(dst) dst.makedirs_p() - sources = self.listdir() + sources = list(self.iterdir()) _ignored = ignore(self, [item.name for item in sources]) def ignored(item): diff --git a/path/__init__.pyi b/path/__init__.pyi index d141d52d..f37700bb 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -30,228 +30,116 @@ from . import classes # Type for the match argument for several methods _Match = Optional[Union[str, Callable[[str], bool], Callable[[Path], bool]]] - class TreeWalkWarning(Warning): pass - class Traversal: follow: Callable[[Path], bool] - def __init__(self, follow: Callable[[Path], bool]): - ... - + def __init__(self, follow: Callable[[Path], bool]): ... def __call__( self, walker: Generator[Path, Optional[Callable[[], bool]], None], - ) -> Iterator[Path]: - ... - + ) -> Iterator[Path]: ... class Path(str): module: Any - def __init__(self, other: Any = ...) -> None: - ... - + def __init__(self, other: Any = ...) -> None: ... @classmethod - def using_module(cls, module: ModuleType) -> Type[Path]: - ... - + def using_module(cls, module: ModuleType) -> Type[Path]: ... @classes.ClassProperty @classmethod - def _next_class(cls: Type[Self]) -> Type[Self]: - ... - - 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 _next_class(cls: Type[Self]) -> Type[Self]: ... + 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__( self, exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], - ) -> None: - ... - + ) -> None: ... @classmethod - def getcwd(cls: Type[Self]) -> Self: - ... - - def abspath(self: Self) -> Self: - ... - - def normcase(self: Self) -> Self: - ... - - def normpath(self: Self) -> Self: - ... - - def realpath(self: Self) -> Self: - ... - - def expanduser(self: Self) -> Self: - ... - - def expandvars(self: Self) -> Self: - ... - - def dirname(self: Self) -> Self: - ... - - def basename(self: Self) -> Self: - ... - - def expand(self: Self) -> Self: - ... - + def getcwd(cls: Type[Self]) -> Self: ... + def abspath(self: Self) -> Self: ... + def normcase(self: Self) -> Self: ... + def normpath(self: Self) -> Self: ... + def realpath(self: Self) -> Self: ... + def expanduser(self: Self) -> Self: ... + def expandvars(self: Self) -> Self: ... + def dirname(self: Self) -> Self: ... + def basename(self: Self) -> Self: ... + def expand(self: Self) -> Self: ... @property - def stem(self) -> str: - ... - + def stem(self) -> str: ... @property - def ext(self) -> str: - ... - - def with_suffix(self: Self, suffix: str) -> Self: - ... - + def ext(self) -> str: ... + def with_suffix(self: Self, suffix: str) -> Self: ... @property - def drive(self: Self) -> Self: - ... - + def drive(self: Self) -> Self: ... @property - def parent(self: Self) -> Self: - ... - + def parent(self: Self) -> Self: ... @property - def name(self: Self) -> Self: - ... - - def splitpath(self: Self) -> Tuple[Self, str]: - ... - - def splitdrive(self: Self) -> Tuple[Self, Self]: - ... - - def splitext(self: Self) -> Tuple[Self, str]: - ... - - def stripext(self: Self) -> Self: - ... - + def name(self: Self) -> Self: ... + def splitpath(self: Self) -> Tuple[Self, str]: ... + def splitdrive(self: Self) -> Tuple[Self, Self]: ... + def splitext(self: Self) -> Tuple[Self, str]: ... + def stripext(self: Self) -> Self: ... @classes.multimethod - def joinpath(cls: Self, first: str, *others: str) -> Self: - ... - - def splitall(self: Self) -> List[Union[Self, str]]: - ... - - def parts(self: Self) -> Tuple[Union[Self, str], ...]: - ... - - def _parts(self: Self) -> Iterator[Union[Self, str]]: - ... - - def _parts_iter(self: Self) -> Iterator[Union[Self, str]]: - ... - - def relpath(self: Self, start: str = ...) -> Self: - ... - - def relpathto(self: Self, dest: str) -> Self: - ... + def joinpath(cls: Self, first: str, *others: str) -> Self: ... + def splitall(self: Self) -> List[Union[Self, str]]: ... + def parts(self: Self) -> Tuple[Union[Self, str], ...]: ... + def _parts(self: Self) -> Iterator[Union[Self, str]]: ... + def _parts_iter(self: Self) -> Iterator[Union[Self, str]]: ... + def relpath(self: Self, start: str = ...) -> Self: ... + def relpathto(self: Self, dest: str) -> Self: ... # --- Listing, searching, walking, and matching - def listdir(self: Self, match: _Match = ...) -> List[Self]: - ... - - def dirs(self: Self, match: _Match = ...) -> List[Self]: - ... - - def files(self: Self, match: _Match = ...) -> List[Self]: - ... - + def iterdir(self: Self) -> Iterator[Self]: ... + def listdir(self: Self, match: _Match = ...) -> List[Self]: ... + def dirs(self: Self, match: _Match = ...) -> List[Self]: ... + def files(self: Self, match: _Match = ...) -> List[Self]: ... def walk( self: Self, match: _Match = ..., errors: str = ..., - ) -> Generator[Self, Optional[Callable[[], bool]], None]: - ... - + ) -> Generator[Self, Optional[Callable[[], bool]], None]: ... def walkdirs( self: Self, match: _Match = ..., errors: str = ..., - ) -> Iterator[Self]: - ... - + ) -> Iterator[Self]: ... def walkfiles( self: Self, match: _Match = ..., errors: str = ..., - ) -> Iterator[Self]: - ... - + ) -> Iterator[Self]: ... def fnmatch( self, pattern: Union[Path, str], normcase: Optional[Callable[[str], str]] = ..., - ) -> bool: - ... - - def glob(self: Self, pattern: str) -> List[Self]: - ... - - def iglob(self: Self, pattern: str) -> Iterator[Self]: - ... - - def bytes(self) -> builtins.bytes: - ... - - def write_bytes(self, bytes: builtins.bytes, append: bool = ...) -> None: - ... - + ) -> bool: ... + def glob(self: Self, pattern: str) -> List[Self]: ... + def iglob(self: Self, pattern: str) -> Iterator[Self]: ... + def bytes(self) -> builtins.bytes: ... + def write_bytes(self, bytes: builtins.bytes, append: bool = ...) -> None: ... def read_text( self, encoding: Optional[str] = ..., errors: Optional[str] = ... - ) -> str: - ... - - def read_bytes(self) -> builtins.bytes: - ... - - def text(self, encoding: Optional[str] = ..., errors: str = ...) -> str: - ... - + ) -> str: ... + def read_bytes(self) -> builtins.bytes: ... + def text(self, encoding: Optional[str] = ..., errors: str = ...) -> str: ... def lines( self, encoding: Optional[str] = ..., errors: Optional[str] = ..., retain: bool = ..., - ) -> List[str]: - ... - + ) -> List[str]: ... def write_lines( self, lines: List[str], @@ -259,67 +147,29 @@ class Path(str): errors: str = ..., linesep: Optional[str] = ..., append: bool = ..., - ) -> None: - ... - - def read_md5(self) -> builtins.bytes: - ... - - def read_hash(self, hash_name: str) -> builtins.bytes: - ... - - def read_hexhash(self, hash_name: str) -> str: - ... - - def isabs(self) -> bool: - ... - - def exists(self) -> bool: - ... - - def isdir(self) -> bool: - ... - - def isfile(self) -> bool: - ... - - def islink(self) -> bool: - ... - - def ismount(self) -> bool: - ... - - def samefile(self, other: str) -> bool: - ... - - def getatime(self) -> float: - ... - + ) -> None: ... + def read_md5(self) -> builtins.bytes: ... + def read_hash(self, hash_name: str) -> builtins.bytes: ... + def read_hexhash(self, hash_name: str) -> str: ... + def isabs(self) -> bool: ... + def exists(self) -> bool: ... + def isdir(self) -> bool: ... + def isfile(self) -> bool: ... + def islink(self) -> bool: ... + def ismount(self) -> bool: ... + def samefile(self, other: str) -> bool: ... + def getatime(self) -> float: ... @property - def atime(self) -> float: - ... - - def getmtime(self) -> float: - ... - + def atime(self) -> float: ... + def getmtime(self) -> float: ... @property - def mtime(self) -> float: - ... - - def getctime(self) -> float: - ... - + def mtime(self) -> float: ... + def getctime(self) -> float: ... @property - def ctime(self) -> float: - ... - - def getsize(self) -> int: - ... - + def ctime(self) -> float: ... + def getsize(self) -> int: ... @property - def size(self) -> int: - ... - + def size(self) -> int: ... def access( self, mode: int, @@ -327,29 +177,16 @@ class Path(str): dir_fd: Optional[int] = ..., effective_ids: bool = ..., follow_symlinks: bool = ..., - ) -> bool: - ... - - def stat(self) -> os.stat_result: - ... - - def lstat(self) -> os.stat_result: - ... - - def get_owner(self) -> str: - ... - + ) -> bool: ... + def stat(self) -> os.stat_result: ... + def lstat(self) -> os.stat_result: ... + def get_owner(self) -> str: ... @property - def owner(self) -> str: - ... + def owner(self) -> str: ... if sys.platform != 'win32': - - def statvfs(self) -> os.statvfs_result: - ... - - def pathconf(self, name: Union[str, int]) -> int: - ... + def statvfs(self) -> os.statvfs_result: ... + def pathconf(self, name: Union[str, int]) -> int: ... def utime( self, @@ -358,91 +195,38 @@ class Path(str): ns: Tuple[int, int] = ..., dir_fd: Optional[int] = ..., follow_symlinks: bool = ..., - ) -> Path: - ... - - def chmod(self: Self, mode: Union[str, int]) -> Self: - ... + ) -> Path: ... + def chmod(self: Self, mode: Union[str, int]) -> Self: ... if sys.platform != 'win32': - def chown( self: Self, uid: Union[int, str] = ..., gid: Union[int, str] = ... - ) -> Self: - ... - - def rename(self: Self, new: str) -> Self: - ... - - def renames(self: Self, new: str) -> Self: - ... - - def mkdir(self: Self, mode: int = ...) -> Self: - ... - - def mkdir_p(self: Self, mode: int = ...) -> Self: - ... - - def makedirs(self: Self, mode: int = ...) -> Self: - ... - - def makedirs_p(self: Self, mode: int = ...) -> Self: - ... - - def rmdir(self: Self) -> Self: - ... - - def rmdir_p(self: Self) -> Self: - ... - - def removedirs(self: Self) -> Self: - ... - - def removedirs_p(self: Self) -> Self: - ... - - def touch(self: Self) -> Self: - ... - - def remove(self: Self) -> Self: - ... - - def remove_p(self: Self) -> Self: - ... - - def unlink(self: Self) -> Self: - ... - - def unlink_p(self: Self) -> Self: - ... - - def link(self: Self, newpath: str) -> Self: - ... - - def symlink(self: Self, newlink: Optional[str] = ...) -> Self: - ... - - 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: - ... - + ) -> Self: ... + + def rename(self: Self, new: str) -> Self: ... + def renames(self: Self, new: str) -> Self: ... + def mkdir(self: Self, mode: int = ...) -> Self: ... + def mkdir_p(self: Self, mode: int = ...) -> Self: ... + def makedirs(self: Self, mode: int = ...) -> Self: ... + def makedirs_p(self: Self, mode: int = ...) -> Self: ... + def rmdir(self: Self) -> Self: ... + def rmdir_p(self: Self) -> Self: ... + def removedirs(self: Self) -> Self: ... + def removedirs_p(self: Self) -> Self: ... + def touch(self: Self) -> Self: ... + def remove(self: Self) -> Self: ... + def remove_p(self: Self) -> Self: ... + def unlink(self: Self) -> Self: ... + def unlink_p(self: Self) -> Self: ... + def link(self: Self, newpath: str) -> Self: ... + def symlink(self: Self, newlink: Optional[str] = ...) -> Self: ... + 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 copytree( self, dst: str, @@ -451,28 +235,18 @@ class Path(str): copy_function: Callable[[str, str], None] = ..., ignore_dangling_symlinks: bool = ..., dirs_exist_ok: bool = ..., - ) -> Any: - ... - - def move(self, dst: str, copy_function: Callable[[str, str], None] = ...) -> Any: - ... - + ) -> Any: ... + def move( + self, dst: str, copy_function: Callable[[str, str], None] = ... + ) -> Any: ... def rmtree( self, ignore_errors: bool = ..., onerror: Optional[Callable[[Any, Any, Any], Any]] = ..., - ) -> None: - ... - - def rmtree_p(self: Self) -> Self: - ... - - def chdir(self) -> None: - ... - - def cd(self) -> None: - ... - + ) -> None: ... + def rmtree_p(self: Self) -> Self: ... + def chdir(self) -> None: ... + def cd(self) -> None: ... def merge_tree( self, dst: str, @@ -480,18 +254,13 @@ class Path(str): *, copy_function: Callable[[str, str], None] = ..., ignore: Callable[[Any, List[str]], Union[List[str], Set[str]]] = ..., - ) -> None: - ... + ) -> None: ... if sys.platform != 'win32': - - def chroot(self) -> None: - ... + def chroot(self) -> None: ... if sys.platform == 'win32': - - def startfile(self: Self, operation: Optional[str] = ...) -> Self: - ... + def startfile(self: Self, operation: Optional[str] = ...) -> Self: ... @contextlib.contextmanager def in_place( @@ -502,42 +271,27 @@ class Path(str): errors: Optional[str] = ..., newline: Optional[str] = ..., backup_extension: Optional[str] = ..., - ) -> Iterator[Tuple[IO[Any], IO[Any]]]: - ... - + ) -> Iterator[Tuple[IO[Any], IO[Any]]]: ... @classes.ClassProperty @classmethod - def special(cls) -> Callable[[Optional[str]], SpecialResolver]: - ... - + def special(cls) -> Callable[[Optional[str]], SpecialResolver]: ... class DirectoryNotEmpty(OSError): @staticmethod - def translate() -> Iterator[None]: - ... - - -def only_newer(copy_func: Callable[[str, str], None]) -> Callable[[str, str], None]: - ... + def translate() -> Iterator[None]: ... +def only_newer(copy_func: Callable[[str, str], None]) -> Callable[[str, str], None]: ... class ExtantPath(Path): - def _validate(self) -> None: - ... - + def _validate(self) -> None: ... class ExtantFile(Path): - def _validate(self) -> None: - ... - + def _validate(self) -> None: ... class SpecialResolver: class ResolverScope: - def __init__(self, paths: SpecialResolver, scope: str) -> None: - ... - - def __getattr__(self, class_: str) -> MultiPathType: - ... + def __init__(self, paths: SpecialResolver, scope: str) -> None: ... + def __getattr__(self, class_: str) -> MultiPathType: ... def __init__( self, @@ -547,70 +301,44 @@ class SpecialResolver: version: Optional[str] = ..., roaming: bool = ..., multipath: bool = ..., - ): - ... - - def __getattr__(self, scope: str) -> ResolverScope: - ... - - def get_dir(self, scope: str, class_: str) -> MultiPathType: - ... - + ): ... + def __getattr__(self, scope: str) -> ResolverScope: ... + 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 __iter__(self) -> Iterator[Path]: - ... - + def detect(cls, input: str) -> MultiPathType: ... + def __iter__(self) -> Iterator[Path]: ... @classes.ClassProperty @classmethod - def _next_class(cls) -> Type[Path]: - ... - + def _next_class(cls) -> Type[Path]: ... class MultiPathType(Multi, Path): pass - class TempDir(Path): @classes.ClassProperty @classmethod - def _next_class(cls) -> Type[Path]: - ... - + def _next_class(cls) -> Type[Path]: ... def __new__( cls: Type[Self], suffix: Optional[AnyStr] = ..., prefix: Optional[AnyStr] = ..., dir: Optional[Union[AnyStr, os.PathLike[AnyStr]]] = ..., - ) -> Self: - ... - - def __init__(self) -> None: - ... - - def __enter__(self) -> Path: # type: ignore - ... - + ) -> Self: ... + def __init__(self) -> None: ... + def __enter__(self) -> Path: ... # type: ignore def __exit__( self, exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], - ) -> None: - ... - + ) -> None: ... class Handlers: @classmethod def _resolve( cls, param: Union[str, Callable[[str], None]] - ) -> Callable[[str], None]: - ... + ) -> Callable[[str], None]: ... diff --git a/path/classes.pyi b/path/classes.pyi index 26cc2c85..2878c48b 100644 --- a/path/classes.pyi +++ b/path/classes.pyi @@ -1,14 +1,8 @@ from typing import Any, Callable, Optional - class ClassProperty(property): - def __get__(self, cls: Any, owner: Optional[type] = ...) -> Any: - ... - + def __get__(self, cls: Any, owner: Optional[type] = ...) -> Any: ... class multimethod: - def __init__(self, func: Callable[..., Any]): - ... - - def __get__(self, instance: Any, owner: Optional[type]) -> Any: - ... + def __init__(self, func: Callable[..., Any]): ... + def __get__(self, instance: Any, owner: Optional[type]) -> Any: ... diff --git a/path/compat/py38.py b/path/compat/py38.py new file mode 100644 index 00000000..a5f0fc1c --- /dev/null +++ b/path/compat/py38.py @@ -0,0 +1,15 @@ +import sys + + +if sys.version_info < (3, 9): + + def removesuffix(self, suffix): + # suffix='' should not call self[:-0]. + if suffix and self.endswith(suffix): + return self[: -len(suffix)] + else: + return self[:] +else: + + def removesuffix(self, suffix): + return self.removesuffix(suffix) diff --git a/path/matchers.py b/path/matchers.py index cb0df10d..1c023897 100644 --- a/path/matchers.py +++ b/path/matchers.py @@ -8,18 +8,15 @@ @overload -def load(param: None) -> Null: - ... +def load(param: None) -> Null: ... @overload -def load(param: str) -> Pattern: - ... +def load(param: str) -> Pattern: ... @overload -def load(param: Any) -> Any: - ... +def load(param: Any) -> Any: ... def load(param): @@ -66,7 +63,7 @@ def __call__(self, path): class CaseInsensitive(Pattern): """ A Pattern with a ``'normcase'`` property, suitable for passing to - :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, + :meth:`iterdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`, :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive. For example, to get all files ending in .py, .Py, .pY, or .PY in the diff --git a/path/matchers.pyi b/path/matchers.pyi index 2d28710d..4c4925d3 100644 --- a/path/matchers.pyi +++ b/path/matchers.pyi @@ -9,22 +9,13 @@ from path import Path class Base: pass - class Null(Base): - def __call__(self, path: str) -> Literal[True]: - ... - + def __call__(self, path: str) -> Literal[True]: ... class Pattern(Base): - def __init__(self, pattern: str) -> None: - ... - - def get_pattern(self, normcase: Callable[[str], str]) -> str: - ... - - def __call__(self, path: Path) -> bool: - ... - + def __init__(self, pattern: str) -> None: ... + def get_pattern(self, normcase: Callable[[str], str]) -> str: ... + def __call__(self, path: Path) -> bool: ... class CaseInsensitive(Pattern): normcase: Callable[[str], str] diff --git a/pyproject.toml b/pyproject.toml index dce944df..a853c578 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,4 @@ requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" -[tool.black] -skip-string-normalization = true - [tool.setuptools_scm] diff --git a/pytest.ini b/pytest.ini index d9a15ed1..9a0f3bce 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,20 +1,15 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules +addopts= + --doctest-modules + --import-mode importlib +consider_namespace_packages=true filterwarnings= ## upstream # Ensure ResourceWarnings are emitted default::ResourceWarning - # shopkeep/pytest-black#55 - ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning - ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning - ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning - - # shopkeep/pytest-black#67 - ignore:'encoding' argument not specified::pytest_black - # realpython/pytest-mypy#152 ignore:'encoding' argument not specified::pytest_mypy @@ -24,4 +19,7 @@ filterwarnings= # pypa/build#615 ignore:'encoding' argument not specified::build.env + # dateutil/dateutil#1284 + ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz + ## end upstream diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..70612985 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,28 @@ +[lint] +extend-select = [ + "C901", + "W", +] +ignore = [ + # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q000", + "Q001", + "Q002", + "Q003", + "COM812", + "COM819", + "ISC001", + "ISC002", +] + +[format] +# Enable preview, required for quote-style = "preserve" +preview = true +# https://docs.astral.sh/ruff/settings/#format-quote-style +quote-style = "preserve" diff --git a/setup.cfg b/setup.cfg index b7045915..293e6b52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,37 +17,25 @@ classifiers = Topic :: Software Development :: Libraries :: Python Modules [options] -packages = find_namespace: include_package_data = true python_requires = >=3.8 install_requires = -[options.packages.find] -exclude = - build* - dist* - docs* - tests* - [options.extras_require] testing = # upstream - pytest >= 6 + pytest >= 6, != 8.1.1 pytest-checkdocs >= 2.4 - pytest-black >= 0.3.7; \ - # workaround for jaraco/skeleton#22 - python_implementation != "PyPy" pytest-cov - pytest-mypy >= 0.9.1; \ - # workaround for jaraco/skeleton#22 - python_implementation != "PyPy" + pytest-mypy pytest-enabler >= 2.2 - pytest-ruff + pytest-ruff >= 0.2.1 # local appdirs packaging pywin32; platform_system == "Windows" and python_version < "3.12" + more_itertools # required for checkdocs on README.rst pygments diff --git a/test_path.py b/test_path.py index d1053692..6dbe143b 100644 --- a/test_path.py +++ b/test_path.py @@ -30,6 +30,7 @@ import stat import pytest +from more_itertools import ilen import path from path import Path @@ -78,6 +79,12 @@ def test_relpath(self): d = Path('D:\\') assert d.relpathto(boz) == boz + def test_construction_without_args(self): + """ + Path class will construct a path to current directory when called with no arguments. + """ + assert Path() == '.' + def test_construction_from_none(self): """ """ with pytest.raises(TypeError): @@ -124,9 +131,9 @@ def test_properties(self): assert f.name == 'xyzzy.py' assert f.parent.name == os_choose(nt='Lib', posix='lib') - # .ext - assert f.ext == '.py' - assert f.parent.ext == '' + # .suffix + assert f.suffix == '.py' + assert f.parent.suffix == '' # .drive assert f.drive == os_choose(nt='C:', posix='') @@ -424,7 +431,7 @@ def test_chroot(monkeypatch): results = [] monkeypatch.setattr(os, 'chroot', results.append) Path().chroot() - assert results == [''] + assert results == [Path()] @pytest.mark.skipif("not hasattr(Path, 'startfile')") @@ -432,7 +439,7 @@ def test_startfile(monkeypatch): results = [] monkeypatch.setattr(os, 'startfile', results.append) Path().startfile() - assert results == [''] + assert results == [Path()] class TestScratchDir: @@ -506,7 +513,7 @@ def test_touch(self, tmpdir): def test_listing(self, tmpdir): d = Path(tmpdir) - assert d.listdir() == [] + assert list(d.iterdir()) == [] f = 'testfile.txt' af = d / f @@ -515,7 +522,7 @@ def test_listing(self, tmpdir): try: assert af.exists() - assert d.listdir() == [af] + assert list(d.iterdir()) == [af] # .glob() assert d.glob('testfile.txt') == [af] @@ -539,7 +546,7 @@ def test_listing(self, tmpdir): with open(f, 'w', encoding='utf-8') as fobj: fobj.write('some text\n') try: - files2 = d.listdir() + files2 = list(d.iterdir()) files.sort() files2.sort() assert files == files2 @@ -550,7 +557,7 @@ def test_listing(self, tmpdir): @pytest.fixture def bytes_filename(self, tmpdir): - name = br'r\xe9\xf1emi' + name = rb'r\xe9\xf1emi' base = str(tmpdir).encode('ascii') try: with open(os.path.join(base, name), 'wb'): @@ -559,17 +566,17 @@ def bytes_filename(self, tmpdir): raise pytest.skip(f"Invalid encodings disallowed {exc}") return name - def test_listdir_other_encoding(self, tmpdir, bytes_filename): # pragma: nocover + def test_iterdir_other_encoding(self, tmpdir, bytes_filename): # pragma: nocover """ Some filesystems allow non-character sequences in path names. - ``.listdir`` should still function in this case. + ``.iterdir`` should still function in this case. See issue #61 for details. """ # first demonstrate that os.listdir works assert os.listdir(str(tmpdir).encode('ascii')) # now try with path - results = Path(tmpdir).listdir() + results = Path(tmpdir).iterdir() (res,) = results assert isinstance(res, Path) assert len(res.basename()) == len(bytes_filename) @@ -657,7 +664,7 @@ def test_shutil(self, tmpdir): testA.copytree(testC) assert testC.isdir() self.assertSetsEqual( - testC.listdir(), + testC.iterdir(), [testC / testCopy.name, testC / testFile.name, testCopyOfLink], ) assert not testCopyOfLink.islink() @@ -670,7 +677,7 @@ def test_shutil(self, tmpdir): testA.copytree(testC, True) assert testC.isdir() self.assertSetsEqual( - testC.listdir(), + testC.iterdir(), [testC / testCopy.name, testC / testFile.name, testCopyOfLink], ) if hasattr(os, 'symlink'): @@ -680,7 +687,7 @@ def test_shutil(self, tmpdir): # Clean up. testDir.rmtree() assert not testDir.exists() - self.assertList(d.listdir(), []) + self.assertList(d.iterdir(), []) def assertList(self, listing, expected): assert sorted(listing) == sorted(expected) @@ -696,7 +703,7 @@ def test_patterns(self, tmpdir): for name in names: (e / name).touch() - self.assertList(d.listdir('*.tmp'), [d / 'x.tmp', d / 'xdir.tmp']) + self.assertList(d.iterdir('*.tmp'), [d / 'x.tmp', d / 'xdir.tmp']) self.assertList(d.files('*.tmp'), [d / 'x.tmp']) self.assertList(d.dirs('*.tmp'), [d / 'xdir.tmp']) self.assertList( @@ -916,7 +923,7 @@ def test_with_nonexisting_dst_kwargs(self): self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name, } - assert set(self.subdir_b.listdir()) == expected + assert set(self.subdir_b.iterdir()) == expected self.check_link() def test_with_nonexisting_dst_args(self): @@ -926,7 +933,7 @@ def test_with_nonexisting_dst_args(self): self.subdir_b / self.test_file.name, self.subdir_b / self.test_link.name, } - assert set(self.subdir_b.listdir()) == expected + assert set(self.subdir_b.iterdir()) == expected self.check_link() def test_with_existing_dst(self): @@ -947,7 +954,7 @@ def test_with_existing_dst(self): self.subdir_b / self.test_link.name, self.subdir_b / test_new.name, } - assert set(self.subdir_b.listdir()) == expected + assert set(self.subdir_b.iterdir()) == expected self.check_link() assert len(Path(self.subdir_b / self.test_file.name).bytes()) == 5000 @@ -959,7 +966,7 @@ def test_copytree_parameters(self): self.subdir_a.merge_tree(self.subdir_b, ignore=ignore) assert self.subdir_b.isdir() - assert self.subdir_b.listdir() == [self.subdir_b / self.test_file.name] + assert list(self.subdir_b.iterdir()) == [self.subdir_b / self.test_file.name] def test_only_newer(self): """ @@ -979,6 +986,10 @@ def test_nested(self): self.subdir_a.merge_tree(self.subdir_b) assert self.subdir_b.joinpath('subsub').isdir() + def test_listdir(self): + with pytest.deprecated_call(): + Path().listdir() + class TestChdir: def test_chdir_or_cd(self, tmpdir): @@ -1105,22 +1116,22 @@ def normcase(path): assert p.fnmatch('foobar', normcase=normcase) assert p.fnmatch('FOO[ABC]AR', normcase=normcase) - def test_listdir_simple(self): + def test_iterdir_simple(self): p = Path('.') - assert len(p.listdir()) == len(os.listdir('.')) + assert ilen(p.iterdir()) == len(os.listdir('.')) - def test_listdir_empty_pattern(self): + def test_iterdir_empty_pattern(self): p = Path('.') - assert p.listdir('') == [] + assert list(p.iterdir('')) == [] - def test_listdir_patterns(self, tmpdir): + def test_iterdir_patterns(self, tmpdir): p = Path(tmpdir) (p / 'sub').mkdir() (p / 'File').touch() - assert p.listdir('s*') == [p / 'sub'] - assert len(p.listdir('*')) == 2 + assert list(p.iterdir('s*')) == [p / 'sub'] + assert ilen(p.iterdir('*')) == 2 - def test_listdir_custom_module(self, tmpdir): + def test_iterdir_custom_module(self, tmpdir): """ Listdir patterns should honor the case sensitivity of the path module used by that Path class. @@ -1129,14 +1140,14 @@ def test_listdir_custom_module(self, tmpdir): p = always_unix(tmpdir) (p / 'sub').mkdir() (p / 'File').touch() - assert p.listdir('S*') == [] + assert list(p.iterdir('S*')) == [] always_win = Path.using_module(ntpath) p = always_win(tmpdir) - assert p.listdir('S*') == [p / 'sub'] - assert p.listdir('f*') == [p / 'File'] + assert list(p.iterdir('S*')) == [p / 'sub'] + assert list(p.iterdir('f*')) == [p / 'File'] - def test_listdir_case_insensitive(self, tmpdir): + def test_iterdir_case_insensitive(self, tmpdir): """ Listdir patterns should honor the case sensitivity of the path module used by that Path class. @@ -1144,8 +1155,8 @@ def test_listdir_case_insensitive(self, tmpdir): p = Path(tmpdir) (p / 'sub').mkdir() (p / 'File').touch() - assert p.listdir(matchers.CaseInsensitive('S*')) == [p / 'sub'] - assert p.listdir(matchers.CaseInsensitive('f*')) == [p / 'File'] + assert list(p.iterdir(matchers.CaseInsensitive('S*'))) == [p / 'sub'] + assert list(p.iterdir(matchers.CaseInsensitive('f*'))) == [p / 'File'] assert p.files(matchers.CaseInsensitive('S*')) == [] assert p.dirs(matchers.CaseInsensitive('f*')) == [] diff --git a/tox.ini b/tox.ini index 1093e028..4c39a5b1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,5 @@ -[tox] -toxworkdir={env:TOX_WORK_DIR:.tox} - - [testenv] +description = perform primary checks (tests, style, types, coverage) deps = setenv = PYTHONWARNDEFAULTENCODING = 1 @@ -12,32 +9,47 @@ usedevelop = True extras = testing +[testenv:diffcov] +description = run tests and check that diff from main is covered +deps = + {[testenv]deps} + diff-cover +commands = + pytest {posargs} --cov-report xml + diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html + diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 + [testenv:docs] +description = build the documentation extras = docs testing changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html - python -m sphinxlint + python -m sphinxlint \ + # workaround for sphinx-contrib/sphinx-lint#83 + --jobs 1 [testenv:finalize] +description = assemble changelog and tag a release skip_install = True deps = towncrier jaraco.develop >= 7.23 -passenv = * +pass_env = * commands = python -m jaraco.develop.finalize [testenv:release] +description = publish the package to PyPI and GitHub skip_install = True deps = build twine>=3 jaraco.develop>=7.1 -passenv = +pass_env = TWINE_PASSWORD GITHUB_TOKEN setenv =