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..430681b1 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,31 @@ +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/newsfragments/220.bugfix.rst b/newsfragments/220.bugfix.rst new file mode 100644 index 00000000..5b04804e --- /dev/null +++ b/newsfragments/220.bugfix.rst @@ -0,0 +1 @@ +Add type annotation for iterdir. diff --git a/path/__init__.py b/path/__init__.py index 827eebd0..7ebafad5 100644 --- a/path/__init__.py +++ b/path/__init__.py @@ -83,6 +83,7 @@ from . import matchers from . import masks from . import classes +from .compat.py38 import removesuffix __all__ = ['Path', 'TempDir'] @@ -173,7 +174,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 +309,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 +387,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 +532,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 +546,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 +563,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 +573,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 +599,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 @@ -1519,7 +1556,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..20cb89da 100644 --- a/path/__init__.pyi +++ b/path/__init__.pyi @@ -184,15 +184,10 @@ class Path(str): ... # --- 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 = ..., 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..1f3189bd 100644 --- a/path/matchers.py +++ b/path/matchers.py @@ -66,7 +66,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/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 =