pax_global_header00006660000000000000000000000064150722510500014507gustar00rootroot0000000000000052 comment=44506435e7924f24bb8b0d84d088b3a133c3396a pathlib-abc-0.5.2/000077500000000000000000000000001507225105000136615ustar00rootroot00000000000000pathlib-abc-0.5.2/.github/000077500000000000000000000000001507225105000152215ustar00rootroot00000000000000pathlib-abc-0.5.2/.github/workflows/000077500000000000000000000000001507225105000172565ustar00rootroot00000000000000pathlib-abc-0.5.2/.github/workflows/build.yaml000066400000000000000000000014431507225105000212430ustar00rootroot00000000000000name: Build on: push: permissions: contents: write id-token: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: hynek/build-and-inspect-python-package@v2 publish: runs-on: ubuntu-latest needs: build if: startsWith(github.ref, 'refs/tags/') environment: name: release url: https://pypi.org/project/pathlib-abc steps: - uses: actions/download-artifact@v4 with: name: Packages path: dist - name: Upload wheel to release uses: svenstaro/upload-release-action@v2 with: file: dist/* tag: ${{ github.ref }} overwrite: true file_glob: true - name: Publish uses: pypa/gh-action-pypi-publish@release/v1 pathlib-abc-0.5.2/.github/workflows/test.yaml000066400000000000000000000013631507225105000211240ustar00rootroot00000000000000name: Test on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: jobs: tests: runs-on: ubuntu-latest env: FORCE_COLOR: "1" strategy: fail-fast: false matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip - run: python -Im pip install tox - run: | python -Im tox run \ -f py$(echo ${{ matrix.python-version }} | tr -d .) pathlib-abc-0.5.2/.gitignore000066400000000000000000000001131507225105000156440ustar00rootroot00000000000000.idea/ .tox/ __pycache__/ dist/ sync.sh venv*/ .python-version docs/_build pathlib-abc-0.5.2/.readthedocs.yaml000066400000000000000000000002741507225105000171130ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.12" sphinx: configuration: docs/conf.py fail_on_warning: true python: install: - requirements: docs/requirements.txt pathlib-abc-0.5.2/CHANGES.rst000066400000000000000000000065521507225105000154730ustar00rootroot00000000000000Changelog ========= Unreleased ---------- - Nothing yet v0.5.2 ------ - Add ``JoinablePath.relative_to()`` and ``JoinablePath.is_relative_to()``. v0.5.1 ------ - Make ``vfsopen()`` try to call ``open()``, like it did before 0.5.0. v0.5.0 ------ - Add ``vfspath()``, which returns the virtual filesystem path as a string. - Replace ``JoinablePath.__str__()`` abstract method with ``__vfspath__()``. - Replace ``magic_open()`` with ``vfsopen()``. The new function takes the same arguments. - Replace ``ReadablePath.__open_rb__()`` abstract method with ``__open_reader__()``, and remove *buffering* argument. - Replace ``WritablePath.__open_wb__()`` abstract method with ``__open_writer__()``, and remove *buffering* and add *mode* arguments. v0.4.3 ------ - Set correct ``__name__`` for ABCs and ``PathParser``. v0.4.2 ------ - Emit encoding warnings from ``magic_open()``, ``ReadablePath.read_text()``, and ``WritablePath.write_text()`` at the correct stack level. v0.4.1 ------ - When ``magic_open()`` is called to open a path in binary mode, raise ``ValueError`` if any of the *encoding*, *errors* or *newline* arguments are given. - In ``ReadablePath.glob()``, raise ``ValueError`` when given an empty pattern. - In ``ReadablePath.glob()`` and ``JoinablePath.full_match()``, stop accepting ``JoinablePath`` objects as patterns. Only strings are allowed. - In ``ReadablePath.copy()`` and ``copy_into()``, stop accepting strings as target paths. Only ``WritablePath`` objects are allowed. v0.4.0 ------ - Several months worth of upstream refactoring: - Rename ``PurePathBase`` to ``JoinablePath``. - Split ``PathBase`` into ``ReadablePath`` and ``WritablePath``. - Replace ``stat()`` with ``info`` attribute and ``PathInfo`` protocol. - Remove many nonessential methods. - Add support for copying between path instances. - Drop support for Python 3.7 and 3.8. v0.3.1 ------ - Add support for Python 3.7. v0.3.0 ------ - Rename ``PathModuleBase`` to ``ParserBase``, and ``PurePathBase.pathmod`` to ``PurePathBase.parser``. - Add ``ParserBase.splitext()``. - Add ``PurePathBase.full_match()``. - Treat a single dot ("``.``") as a valid file extension. - Revert ``match()`` back to 3.12 behaviour (no recursive wildcards). - Replace ``PathBase.glob(follow_symlinks=...)`` with ``recurse_symlinks=...``. - Suppress all ``OSError`` exceptions from ``PathBase.exists()`` and ``is_*()`` methods. - Disallow passing ``bytes`` to initialisers. - Improve walking and globbing performance. - Expand test coverage. - Clarify that we're using the PSF license. v0.2.0 ------ - Add ``PathModuleBase`` ABC to support path syntax customization. - Add CI. Thank you Edgar Ramírez Mondragón! - Return paths with trailing slashes if a glob pattern ends with a slash. - Return both files and directory paths if a glob pattern ends with ``**``, rather than directories only. - Improve ``PathBase.resolve()`` performance by avoiding some path object allocations. - Remove ``PurePathBase.is_reserved()``. - Remove automatic path normalization. Specifically, the ABCs no longer convert alternate separators nor remove either dot or empty segments. - Remove caching of the path drive, root, tail, and string. - Remove deprecation warnings and audit events. v0.1.1 ------ - Improve globbing performance by avoiding re-initialising path objects. - Add docs. v0.1.0 ------ - Initial release. pathlib-abc-0.5.2/LICENSE.txt000066400000000000000000000043471507225105000155140ustar00rootroot000000000000001. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. pathlib-abc-0.5.2/README.rst000066400000000000000000000026031507225105000153510ustar00rootroot00000000000000=========== pathlib-abc =========== |pypi| |docs| Base classes for ``pathlib.Path``-ish objects. Requires Python 3.9+. This package is a preview of ``pathlib`` functionality planned for a future release of Python; specifically, it provides three ABCs that can be used to implement path classes for non-local filesystems, such as archive files and storage servers: ``JoinablePath`` Abstract base class for paths that do not perform I/O. ``ReadablePath`` Abstract base class for paths that support reading. ``WritablePath`` Abstract base class for paths that support writing. These base classes are under active development. Once the base classes reach maturity, they may become part of the Python standard library, and this package will continue to provide a backport for older Python releases. Contributing ------------ Functional changes must be made in the upstream CPython project, and undergo their usual CLA + code review process. Once a change lands in CPython, it can be back-ported here. Other changes (such as CI improvements) can be made as pull requests to this project. .. |pypi| image:: https://img.shields.io/pypi/v/pathlib-abc.svg :target: https://pypi.python.org/pypi/pathlib-abc :alt: Latest version released on PyPi .. |docs| image:: https://readthedocs.org/projects/pathlib-abc/badge :target: http://pathlib-abc.readthedocs.io/en/latest :alt: Documentation pathlib-abc-0.5.2/docs/000077500000000000000000000000001507225105000146115ustar00rootroot00000000000000pathlib-abc-0.5.2/docs/api.rst000066400000000000000000000325011507225105000161150ustar00rootroot00000000000000API Reference ============= .. module:: pathlib_abc Functions --------- This package offers the following functions: .. function:: vfspath(obj) Return the virtual filesystem path (a string) of the given object. Unlike the ``os.fspath()`` function, this function calls the object's :meth:`~JoinablePath.__vfspath__` method. .. function:: vfsopen(obj, mode='r' buffering=-1, \ encoding=None, errors=None, newline=None) Open the given object and return a file object. Unlike the built-in ``open()`` function, this function additionally calls the object's :meth:`~ReadablePath.__open_reader__`, :meth:`~WritablePath.__open_writer__` or :meth:`!__open_updater__` method, as appropriate for the given mode. Protocols --------- This package offers the following protocols: .. class:: PathParser Protocol for path parser objects, which split and join string paths. Subclasses of :class:`JoinablePath` should provide a path parser object as an attribute named :attr:`~JoinablePath.parser`. Path parsers provide a subset of the ``os.path`` API. Python itself provides the ``posixpath`` and ``ntpath`` modules, which can be assigned to :attr:`~JoinablePath.parser` to implement path objects with POSIX or Windows syntax. .. attribute:: sep Character used to separate path components. .. attribute:: altsep Alternative path separator character, or ``None``. .. method:: split(path) Split the path into a pair ``(head, tail)``, where *head* is everything before the final path separator, and *tail* is everything after. Either part may be empty. .. note:: Trailing slashes are meaningful in ``posixpath`` and ``ntpath``, so ``P('foo/').parent`` is ``P('foo')``, and its :attr:`~JoinablePath.name` is the empty string. .. method:: splitext(name) Split the filename into a pair ``(stem, ext)``, where *ext* is a file extension beginning with a dot and containing at most one dot, and *stem* is everything before the extension. .. method:: normcase(path) Return the path with its case normalized. .. note:: This method is used to detect case sensitivity in :meth:`JoinablePath.full_match` and :meth:`ReadablePath.glob`, where it's called with the string containing a mix of upper and lowercase letters. Case-sensitive filesystems should return the string unchanged, whereas case-insensitive filesystems should return the string with its case modified (e.g. with ``upper()`` or ``lower()``.) .. class:: PathInfo Protocol for path information objects, which provide file type info. Subclasses of :class:`ReadablePath` should provide a path information object as an attribute named :attr:`~ReadablePath.info`. .. method:: exists(*, follow_symlinks=True) Return ``True`` if the path is an existing file or directory, or any other kind of file; return ``False`` if the path doesn't exist. If *follow_symlinks* is ``False``, return ``True`` for symlinks without checking if their targets exist. .. method:: is_dir(*, follow_symlinks=True) Return ``True`` if the path is a directory, or a symbolic link pointing to a directory; return ``False`` if the path is (or points to) any other kind of file, or if it doesn't exist. If *follow_symlinks* is ``False``, return ``True`` only if the path is a directory (without following symlinks); return ``False`` if the path is any other kind of file, or if it doesn't exist. .. method:: is_file(*, follow_symlinks=True) Return ``True`` if the path is a file, or a symbolic link pointing to a file; return ``False`` if the path is (or points to) a directory or other non-file, or if it doesn't exist. If *follow_symlinks* is ``False``, return ``True`` only if the path is a file (without following symlinks); return ``False`` if the path is a directory or other other non-file, or if it doesn't exist. .. method:: is_symlink() Return ``True`` if the path is a symbolic link (even if broken); return ``False`` if the path is a directory or any kind of file, or if it doesn't exist. Abstract base classes --------------------- This package offers the following abstract base classes: .. list-table:: :header-rows: 1 - * ABC * Inherits from * Abstract methods * Mixin methods - * :class:`JoinablePath` * * :attr:`~JoinablePath.parser` :meth:`~JoinablePath.__vfspath__` :meth:`~JoinablePath.with_segments` * :attr:`~JoinablePath.parts` :attr:`~JoinablePath.anchor` :attr:`~JoinablePath.parent` :attr:`~JoinablePath.parents` :attr:`~JoinablePath.name` :attr:`~JoinablePath.stem` :attr:`~JoinablePath.suffix` :attr:`~JoinablePath.suffixes` :meth:`~JoinablePath.with_name` :meth:`~JoinablePath.with_stem` :meth:`~JoinablePath.with_suffix` :meth:`~JoinablePath.joinpath` :meth:`~JoinablePath.__truediv__` :meth:`~JoinablePath.__rtruediv__` :meth:`~JoinablePath.relative_to` :meth:`~JoinablePath.is_relative_to` :meth:`~JoinablePath.full_match` - * :class:`ReadablePath` * :class:`JoinablePath` * :attr:`~ReadablePath.info` :meth:`~ReadablePath.__open_reader__` :meth:`~ReadablePath.iterdir` :meth:`~ReadablePath.readlink` * :meth:`~ReadablePath.read_bytes` :meth:`~ReadablePath.read_text` :meth:`~ReadablePath.copy` :meth:`~ReadablePath.copy_into` :meth:`~ReadablePath.glob` :meth:`~ReadablePath.walk` - * :class:`WritablePath` * :class:`JoinablePath` * :meth:`~WritablePath.__open_writer__` :meth:`~WritablePath.mkdir` :meth:`~WritablePath.symlink_to` * :meth:`~WritablePath.write_bytes` :meth:`~WritablePath.write_text` :meth:`~WritablePath._copy_from` .. class:: JoinablePath Abstract base class for path objects without I/O support. .. attribute:: parser (**Abstract attribute**.) Implementation of :class:`PathParser` used for low-level splitting and joining. .. method:: __vfspath__() (**Abstract method**.) Return a string representation of the path, suitable for passing to methods of the :attr:`parser`. .. method:: with_segments(*pathsegments) (**Abstract method**.) Create a new path object of the same type by combining the given *pathsegments*. This method is called whenever a derivative path is created, such as from :attr:`parent` and :meth:`with_name`. .. attribute:: parts Tuple of path components. The default implementation repeatedly calls :meth:`PathParser.split` to decompose the path. .. attribute:: anchor The path's irreducible prefix. The default implementation repeatedly calls :meth:`PathParser.split` until the directory name stops changing. .. attribute:: parent The path's lexical parent. The default implementation calls :meth:`PathParser.split` once. .. attribute:: parents Sequence of the path's lexical parents, beginning with the immediate parent. The default implementation repeatedly calls :meth:`PathParser.split`. .. attribute:: name The path's base name. The name is empty if the path has only an anchor, or ends with a slash. The default implementation calls :meth:`PathParser.split` once. .. attribute:: stem The path's base name with the file extension omitted. The default implementation calls :meth:`PathParser.splitext` on :attr:`name`. .. attribute:: suffix The path's file extension. The default implementation calls :meth:`PathParser.splitext` on :attr:`name`. .. attribute:: suffixes Sequence of the path's file extensions. The default implementation repeatedly calls :meth:`PathParser.splitext` on :attr:`name`. .. method:: with_name(name) Return a new path with a different :attr:`name`. The name may be empty. The default implementation calls :meth:`PathParser.split` to remove the old name, and :meth:`with_segments` to create the new path object. .. method:: with_stem(stem) Return a new path with a different :attr:`stem`, similarly to :meth:`with_name`. .. method:: with_suffix(suffix) Return a new path with a different :attr:`suffix`, similarly to :meth:`with_name`. .. method:: joinpath(*pathsegments) Return a new path with the given path segments joined onto the end. The default implementation calls :meth:`with_segments` with the combined segments. .. method:: __truediv__(pathsegment) Return a new path with the given path segment joined on the end. .. method:: __rtruediv__(pathsegment) Return a new path with the given path segment joined on the beginning. .. method:: relative_to(other, *, walk_up=False) Return a new relative path from *other* to this path. The default implementation compares this path and the parents of *other*; ``__eq__()`` must be implemented for this to work correctly. .. method:: is_relative_to(other) Returns ``True`` is this path is relative to *other*, ``False`` otherwise. The default implementation compares this path and the parents of *other*; ``__eq__()`` must be implemented for this to work correctly. .. method:: full_match(pattern) Return true if the path matches the given glob-style pattern, false otherwise. The default implementation uses :meth:`PathParser.normcase` to establish case sensitivity. .. class:: ReadablePath Abstract base class for path objects with support for reading data. This is a subclass of :class:`JoinablePath` .. attribute:: info (**Abstract attribute**.) Implementation of :class:`PathInfo` that supports querying the file type. .. method:: __open_reader__() (**Abstract method.**) Open the path for reading in binary mode, and return a file object. .. method:: iterdir() (**Abstract method**.) Yield path objects for the directory contents. .. method:: readlink() (**Abstract method**.) Return the symlink target as a new path object. .. method:: read_bytes() Return the binary contents of the path. The default implementation calls :func:`vfsopen`. .. method:: read_text(encoding=None, errors=None, newline=None) Return the text contents of the path. The default implementation calls :func:`vfsopen`. .. method:: copy(target, **kwargs) Copy the path to the given target, which should be an instance of :class:`WritablePath`. The default implementation calls :meth:`WritablePath._copy_from`, passing along keyword arguments. .. method:: copy_into(target_dir, **kwargs) Copy the path *into* the given target directory, which should be an instance of :class:`WritablePath`. See :meth:`copy`. .. method:: glob(pattern, *, recurse_symlinks=True) Yield path objects in the file tree that match the given glob-style pattern. The default implementation uses :attr:`info` and :meth:`iterdir`. .. warning:: For performance reasons, the default value for *recurse_symlinks* is ``True`` in this base class, but for historical reasons, the default is ``False`` in ``pathlib.Path``. Furthermore, ``True`` is the *only* acceptable value for *recurse_symlinks* in this base class. For maximum compatibility, users should supply ``recurse_symlinks=True`` explicitly when globbing recursively. .. method:: walk(top_down=True, on_error=None, follow_symlinks=False) Yield a ``(dirpath, dirnames, filenames)`` triplet for each directory in the file tree, like ``os.walk()``. The default implementation uses :attr:`info` and :meth:`iterdir`. .. class:: WritablePath Abstract base class for path objects with support for writing data. This is a subclass of :class:`JoinablePath` .. method:: __open_writer__(mode) (**Abstract method**.) Open the path for writing in binary mode, and return a file object. The *mode* argument is either ``'w'``, ``'a'``, or ``'x'``. .. method:: mkdir() (**Abstract method**.) Create this path as a directory. .. method:: symlink_to(target, target_is_directory=False) (**Abstract method**.) Create this path as a symlink to the given target. .. method:: write_bytes(data) Write the given binary data to the path, and return the number of bytes written. The default implementation calls :func:`vfsopen`. .. method:: write_text(data, encoding=None, errors=None, newline=None) Write the given text data to the path, and return the number of bytes written. The default implementation calls :func:`vfsopen`. .. method:: _copy_from(source, *, follow_symlinks=True) Copy the path from the given source, which should be an instance of :class:`ReadablePath`. The default implementation uses :attr:`ReadablePath.info` to establish the type of the source path. It uses :func:`vfsopen` to copy regular files; :meth:`~ReadablePath.iterdir` and :meth:`mkdir` to copy directories; and :meth:`~ReadablePath.readlink` and :meth:`symlink_to` to copy symlinks when *follow_symlinks* is false. pathlib-abc-0.5.2/docs/changes.rst000066400000000000000000000000341507225105000167500ustar00rootroot00000000000000.. include:: ../CHANGES.rst pathlib-abc-0.5.2/docs/conf.py000066400000000000000000000002701507225105000161070ustar00rootroot00000000000000project = 'pathlib-abc' copyright = '2023' author = 'Barney Gale' extensions = [ 'sphinx_copybutton', ] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] html_theme = 'furo' pathlib-abc-0.5.2/docs/examples.rst000066400000000000000000000001141507225105000171550ustar00rootroot00000000000000Examples ======== ZipPath ------- .. literalinclude:: examples/zippath.py pathlib-abc-0.5.2/docs/examples/000077500000000000000000000000001507225105000164275ustar00rootroot00000000000000pathlib-abc-0.5.2/docs/examples/zippath.py000066400000000000000000000046611507225105000204670ustar00rootroot00000000000000import posixpath from pathlib_abc import PathInfo, ReadablePath class MissingInfo(PathInfo): __slots__ = () def exists(self, follow_symlinks=True): return False def is_dir(self, follow_symlinks=True): return False def is_file(self, follow_symlinks=True): return False def is_symlink(self): return False class ZipPathInfo(PathInfo): __slots__ = ('zip_info', 'children') def __init__(self): self.zip_info = None self.children = {} def exists(self, follow_symlinks=True): return True def is_dir(self, follow_symlinks=True): if self.zip_info is None: return True else: return self.zip_info.filename.endswith('/') def is_file(self, follow_symlinks=True): if self.zip_info is None: return False else: return not self.zip_info.filename.endswith('/') def is_symlink(self): return False def resolve(self, path, create=False): if not path: return self name, _, path = path.partition('/') if not name: info = self elif name in self.children: info = self.children[name] elif create: info = self.children[name] = ZipPathInfo() else: return MissingInfo() return info.resolve(path, create) class ZipPath(ReadablePath): __slots__ = ('_segments', 'zip_file') parser = posixpath def __init__(self, *pathsegments, zip_file): self._segments = pathsegments self.zip_file = zip_file if not hasattr(zip_file, 'filetree'): # Read the contents into a tree of ZipPathInfo objects. zip_file.filetree = ZipPathInfo() for zip_info in zip_file.filelist: info = zip_file.filetree.resolve(zip_info.filename, create=True) info.zip_info = zip_info def __str__(self): if not self._segments: return '' return self.parser.join(*self._segments) def with_segments(self, *pathsegments): return type(self)(*pathsegments, zip_file=self.zip_file) @property def info(self): return self.zip_file.filetree.resolve(str(self)) def __open_rb__(self, buffering=-1): return self.zip_file.open(self.info.zip_info, 'r') def iterdir(self): return (self / name for name in self.info.children) def readlink(self): raise NotImplementedError pathlib-abc-0.5.2/docs/index.rst000066400000000000000000000001351507225105000164510ustar00rootroot00000000000000.. include:: ../README.rst Contents -------- .. toctree:: api examples changes pathlib-abc-0.5.2/docs/requirements.txt000066400000000000000000000000361507225105000200740ustar00rootroot00000000000000sphinx sphinx-copybutton furo pathlib-abc-0.5.2/pathlib_abc/000077500000000000000000000000001507225105000161115ustar00rootroot00000000000000pathlib-abc-0.5.2/pathlib_abc/__init__.py000066400000000000000000000404371507225105000202320ustar00rootroot00000000000000""" Protocols for supporting classes in pathlib. """ # This module also provides abstract base classes for rich path objects. # These ABCs are a *private* part of the Python standard library, but they're # made available as a PyPI package called "pathlib-abc". It's possible they'll # become an official part of the standard library in future. # # Three ABCs are provided -- _JoinablePath, _ReadablePath and _WritablePath from abc import ABC, abstractmethod from pathlib_abc._glob import _GlobberBase from pathlib_abc._os import ( copyfileobj, ensure_different_files, ensure_distinct_paths, vfsopen, vfspath) from typing import Optional, Protocol, runtime_checkable try: from io import text_encoding except ImportError: def text_encoding(encoding): return encoding __all__ = ['PathParser', 'PathInfo', 'JoinablePath', 'ReadablePath', 'WritablePath', 'vfsopen', 'vfspath'] def _explode_path(path, split): """ Split the path into a 2-tuple (anchor, parts), where *anchor* is the uppermost parent of the path (equivalent to path.parents[-1]), and *parts* is a reversed list of parts following the anchor. """ parent, name = split(path) names = [] while path != parent: names.append(name) path = parent parent, name = split(path) return path, names @runtime_checkable class PathParser(Protocol): """Protocol for path parsers, which do low-level path manipulation. Path parsers provide a subset of the os.path API, specifically those functions needed to provide JoinablePath functionality. Each JoinablePath subclass references its path parser via a 'parser' class attribute. """ sep: str altsep: Optional[str] def split(self, path: str) -> tuple[str, str]: ... def splitext(self, path: str) -> tuple[str, str]: ... def normcase(self, path: str) -> str: ... @runtime_checkable class PathInfo(Protocol): """Protocol for path info objects, which support querying the file type. Methods may return cached results. """ def exists(self, *, follow_symlinks: bool = True) -> bool: ... def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... def is_file(self, *, follow_symlinks: bool = True) -> bool: ... def is_symlink(self) -> bool: ... class _PathGlobber(_GlobberBase): """Provides shell-style pattern matching and globbing for ReadablePath. """ @staticmethod def lexists(path): return path.info.exists(follow_symlinks=False) @staticmethod def scandir(path): return ((child.info, child.name, child) for child in path.iterdir()) @staticmethod def concat_path(path, text): return path.with_segments(vfspath(path) + text) stringify_path = staticmethod(vfspath) class JoinablePath(ABC): """Abstract base class for pure path objects. This class *does not* provide several magic methods that are defined in its implementation PurePath. They are: __init__, __fspath__, __bytes__, __reduce__, __hash__, __eq__, __lt__, __le__, __gt__, __ge__. """ __slots__ = () @property @abstractmethod def parser(self): """Implementation of pathlib._types.Parser used for low-level path parsing and manipulation. """ raise NotImplementedError @abstractmethod def with_segments(self, *pathsegments): """Construct a new path object from any number of path-like objects. Subclasses may override this method to customize how new path objects are created from methods like `iterdir()`. """ raise NotImplementedError @abstractmethod def __vfspath__(self): """Return the string representation of the path.""" raise NotImplementedError @property def anchor(self): """The concatenation of the drive and root, or ''.""" return _explode_path(vfspath(self), self.parser.split)[0] @property def name(self): """The final path component, if any.""" return self.parser.split(vfspath(self))[1] @property def suffix(self): """ The final component's last suffix, if any. This includes the leading period. For example: '.txt' """ return self.parser.splitext(self.name)[1] @property def suffixes(self): """ A list of the final component's suffixes, if any. These include the leading periods. For example: ['.tar', '.gz'] """ split = self.parser.splitext stem, suffix = split(self.name) suffixes = [] while suffix: suffixes.append(suffix) stem, suffix = split(stem) return suffixes[::-1] @property def stem(self): """The final path component, minus its last suffix.""" return self.parser.splitext(self.name)[0] def with_name(self, name): """Return a new path with the file name changed.""" split = self.parser.split if split(name)[0]: raise ValueError(f"Invalid name {name!r}") path = vfspath(self) path = path.removesuffix(split(path)[1]) + name return self.with_segments(path) def with_stem(self, stem): """Return a new path with the stem changed.""" suffix = self.suffix if not suffix: return self.with_name(stem) elif not stem: # If the suffix is non-empty, we can't make the stem empty. raise ValueError(f"{self!r} has a non-empty suffix") else: return self.with_name(stem + suffix) def with_suffix(self, suffix): """Return a new path with the file suffix changed. If the path has no suffix, add given suffix. If the given suffix is an empty string, remove the suffix from the path. """ stem = self.stem if not stem: # If the stem is empty, we can't make the suffix non-empty. raise ValueError(f"{self!r} has an empty name") elif suffix and not suffix.startswith('.'): raise ValueError(f"Invalid suffix {suffix!r}") else: return self.with_name(stem + suffix) @property def parts(self): """An object providing sequence-like access to the components in the filesystem path.""" anchor, parts = _explode_path(vfspath(self), self.parser.split) if anchor: parts.append(anchor) return tuple(reversed(parts)) def joinpath(self, *pathsegments): """Combine this path with one or several arguments, and return a new path representing either a subpath (if all arguments are relative paths) or a totally different path (if one of the arguments is anchored). """ return self.with_segments(vfspath(self), *pathsegments) def __truediv__(self, key): try: return self.with_segments(vfspath(self), key) except TypeError: return NotImplemented def __rtruediv__(self, key): try: return self.with_segments(key, vfspath(self)) except TypeError: return NotImplemented @property def parent(self): """The logical parent of the path.""" path = vfspath(self) parent = self.parser.split(path)[0] if path != parent: return self.with_segments(parent) return self @property def parents(self): """A sequence of this path's logical parents.""" split = self.parser.split path = vfspath(self) parent = split(path)[0] parents = [] while path != parent: parents.append(self.with_segments(parent)) path = parent parent = split(path)[0] return tuple(parents) def relative_to(self, other, *, walk_up=False): """Return the relative path to another path identified by the passed arguments. If the operation is not possible (because this is not related to the other path), raise ValueError. The *walk_up* parameter controls whether `..` may be used to resolve the path. """ parts = [] for path in (other,) + other.parents: if self.is_relative_to(path): break elif not walk_up: raise ValueError(f"{self!r} is not in the subpath of {other!r}") elif path.name == '..': raise ValueError(f"'..' segment in {other!r} cannot be walked") else: parts.append('..') else: raise ValueError(f"{self!r} and {other!r} have different anchors") return self.with_segments(*parts, *self.parts[len(path.parts):]) def is_relative_to(self, other): """Return True if the path is relative to another path or False. """ return other == self or other in self.parents def full_match(self, pattern): """ Return True if this path matches the given glob-style pattern. The pattern is matched against the entire path. """ case_sensitive = self.parser.normcase('Aa') == 'Aa' globber = _PathGlobber(self.parser.sep, case_sensitive, recursive=True) match = globber.compile(pattern, altsep=self.parser.altsep) return match(vfspath(self)) is not None class ReadablePath(JoinablePath): """Abstract base class for readable path objects. The Path class implements this ABC for local filesystem paths. Users may create subclasses to implement readable virtual filesystem paths, such as paths in archive files or on remote storage systems. """ __slots__ = () @property @abstractmethod def info(self): """ A PathInfo object that exposes the file type and other file attributes of this path. """ raise NotImplementedError @abstractmethod def __open_reader__(self): """ Open the file pointed to by this path for reading in binary mode and return a file object. """ raise NotImplementedError def read_bytes(self): """ Open the file in bytes mode, read it, and close the file. """ with vfsopen(self, mode='rb') as f: return f.read() def read_text(self, encoding=None, errors=None, newline=None): """ Open the file in text mode, read it, and close the file. """ # Call io.text_encoding() here to ensure any warning is raised at an # appropriate stack level. encoding = text_encoding(encoding) with vfsopen(self, mode='r', encoding=encoding, errors=errors, newline=newline) as f: return f.read() @abstractmethod def iterdir(self): """Yield path objects of the directory contents. The children are yielded in arbitrary order, and the special entries '.' and '..' are not included. """ raise NotImplementedError def glob(self, pattern, *, recurse_symlinks=True): """Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given relative pattern. """ anchor, parts = _explode_path(pattern, self.parser.split) if anchor: raise NotImplementedError("Non-relative patterns are unsupported") elif not parts: raise ValueError(f"Unacceptable pattern: {pattern!r}") elif not recurse_symlinks: raise NotImplementedError("recurse_symlinks=False is unsupported") case_sensitive = self.parser.normcase('Aa') == 'Aa' globber = _PathGlobber(self.parser.sep, case_sensitive, recursive=True) select = globber.selector(parts) return select(self.joinpath('')) def walk(self, top_down=True, on_error=None, follow_symlinks=False): """Walk the directory tree from this directory, similar to os.walk().""" paths = [self] while paths: path = paths.pop() if isinstance(path, tuple): yield path continue dirnames = [] filenames = [] if not top_down: paths.append((path, dirnames, filenames)) try: for child in path.iterdir(): if child.info.is_dir(follow_symlinks=follow_symlinks): if not top_down: paths.append(child) dirnames.append(child.name) else: filenames.append(child.name) except OSError as error: if on_error is not None: on_error(error) if not top_down: while not isinstance(paths.pop(), tuple): pass continue if top_down: yield path, dirnames, filenames paths += [path.joinpath(d) for d in reversed(dirnames)] @abstractmethod def readlink(self): """ Return the path to which the symbolic link points. """ raise NotImplementedError def copy(self, target, **kwargs): """ Recursively copy this file or directory tree to the given destination. """ ensure_distinct_paths(self, target) target._copy_from(self, **kwargs) return target.joinpath() # Empty join to ensure fresh metadata. def copy_into(self, target_dir, **kwargs): """ Copy this file or directory tree into the given existing directory. """ name = self.name if not name: raise ValueError(f"{self!r} has an empty name") return self.copy(target_dir / name, **kwargs) class WritablePath(JoinablePath): """Abstract base class for writable path objects. The Path class implements this ABC for local filesystem paths. Users may create subclasses to implement writable virtual filesystem paths, such as paths in archive files or on remote storage systems. """ __slots__ = () @abstractmethod def symlink_to(self, target, target_is_directory=False): """ Make this path a symlink pointing to the target path. Note the order of arguments (link, target) is the reverse of os.symlink. """ raise NotImplementedError @abstractmethod def mkdir(self): """ Create a new directory at this given path. """ raise NotImplementedError @abstractmethod def __open_writer__(self, mode): """ Open the file pointed to by this path for writing in binary mode and return a file object. """ raise NotImplementedError def write_bytes(self, data): """ Open the file in bytes mode, write to it, and close the file. """ # type-check for the buffer interface before truncating the file view = memoryview(data) with vfsopen(self, mode='wb') as f: return f.write(view) def write_text(self, data, encoding=None, errors=None, newline=None): """ Open the file in text mode, write to it, and close the file. """ # Call io.text_encoding() here to ensure any warning is raised at an # appropriate stack level. encoding = text_encoding(encoding) if not isinstance(data, str): raise TypeError('data must be str, not %s' % data.__class__.__name__) with vfsopen(self, mode='w', encoding=encoding, errors=errors, newline=newline) as f: return f.write(data) def _copy_from(self, source, follow_symlinks=True): """ Recursively copy the given path to this path. """ stack = [(source, self)] while stack: src, dst = stack.pop() if not follow_symlinks and src.info.is_symlink(): dst.symlink_to(vfspath(src.readlink()), src.info.is_dir()) elif src.info.is_dir(): children = src.iterdir() dst.mkdir() for child in children: stack.append((child, dst.joinpath(child.name))) else: ensure_different_files(src, dst) with vfsopen(src, 'rb') as source_f: with vfsopen(dst, 'wb') as target_f: copyfileobj(source_f, target_f) # For tests. _PathParser = PathParser _JoinablePath = JoinablePath _ReadablePath = ReadablePath _WritablePath = WritablePath pathlib-abc-0.5.2/pathlib_abc/_fnmatch.py000066400000000000000000000152071507225105000202470ustar00rootroot00000000000000"""Filename matching with shell patterns. fnmatch(FILENAME, PATTERN) matches according to the local convention. fnmatchcase(FILENAME, PATTERN) always takes case in account. The functions operate by translating the pattern into a regular expression. They cache the compiled regular expressions for speed. The function translate(PATTERN) returns a regular expression corresponding to PATTERN. (It does not compile it.) """ import functools import itertools import os import posixpath import re __all__ = ["filter", "filterfalse", "fnmatch", "fnmatchcase", "translate"] def fnmatch(name, pat): """Test whether FILENAME matches PATTERN. Patterns are Unix shell style: * matches everything ? matches any single character [seq] matches any character in seq [!seq] matches any char not in seq An initial period in FILENAME is not special. Both FILENAME and PATTERN are first case-normalized if the operating system requires it. If you don't want this, use fnmatchcase(FILENAME, PATTERN). """ name = os.path.normcase(name) pat = os.path.normcase(pat) return fnmatchcase(name, pat) @functools.lru_cache(maxsize=32768, typed=True) def _compile_pattern(pat): if isinstance(pat, bytes): pat_str = str(pat, 'ISO-8859-1') res_str = translate(pat_str) res = bytes(res_str, 'ISO-8859-1') else: res = translate(pat) return re.compile(res).match def filter(names, pat): """Construct a list from those elements of the iterable NAMES that match PAT.""" result = [] pat = os.path.normcase(pat) match = _compile_pattern(pat) if os.path is posixpath: # normcase on posix is NOP. Optimize it away from the loop. for name in names: if match(name): result.append(name) else: for name in names: if match(os.path.normcase(name)): result.append(name) return result def filterfalse(names, pat): """Construct a list from those elements of the iterable NAMES that do not match PAT.""" pat = os.path.normcase(pat) match = _compile_pattern(pat) if os.path is posixpath: # normcase on posix is NOP. Optimize it away from the loop. return list(itertools.filterfalse(match, names)) result = [] for name in names: if match(os.path.normcase(name)) is None: result.append(name) return result def fnmatchcase(name, pat): """Test whether FILENAME matches PATTERN, including case. This is a version of fnmatch() which doesn't case-normalize its arguments. """ match = _compile_pattern(pat) return match(name) is not None def translate(pat): """Translate a shell PATTERN to a regular expression. There is no way to quote meta-characters. """ parts, star_indices = _translate(pat, '*', '.') return _join_translated_parts(parts, star_indices) _re_setops_sub = re.compile(r'([&~|])').sub _re_escape = functools.lru_cache(maxsize=512)(re.escape) def _translate(pat, star, question_mark): res = [] add = res.append star_indices = [] i, n = 0, len(pat) while i < n: c = pat[i] i = i+1 if c == '*': # store the position of the wildcard star_indices.append(len(res)) add(star) # compress consecutive `*` into one while i < n and pat[i] == '*': i += 1 elif c == '?': add(question_mark) elif c == '[': j = i if j < n and pat[j] == '!': j = j+1 if j < n and pat[j] == ']': j = j+1 while j < n and pat[j] != ']': j = j+1 if j >= n: add('\\[') else: stuff = pat[i:j] if '-' not in stuff: stuff = stuff.replace('\\', r'\\') else: chunks = [] k = i+2 if pat[i] == '!' else i+1 while True: k = pat.find('-', k, j) if k < 0: break chunks.append(pat[i:k]) i = k+1 k = k+3 chunk = pat[i:j] if chunk: chunks.append(chunk) else: chunks[-1] += '-' # Remove empty ranges -- invalid in RE. for k in range(len(chunks)-1, 0, -1): if chunks[k-1][-1] > chunks[k][0]: chunks[k-1] = chunks[k-1][:-1] + chunks[k][1:] del chunks[k] # Escape backslashes and hyphens for set difference (--). # Hyphens that create ranges shouldn't be escaped. stuff = '-'.join(s.replace('\\', r'\\').replace('-', r'\-') for s in chunks) i = j+1 if not stuff: # Empty range: never match. add('(?!)') elif stuff == '!': # Negated empty range: match any character. add('.') else: # Escape set operations (&&, ~~ and ||). stuff = _re_setops_sub(r'\\\1', stuff) if stuff[0] == '!': stuff = '^' + stuff[1:] elif stuff[0] in ('^', '['): stuff = '\\' + stuff add(f'[{stuff}]') else: add(_re_escape(c)) assert i == n return res, star_indices def _join_translated_parts(parts, star_indices): if not star_indices: return fr'(?s:{"".join(parts)})\Z' iter_star_indices = iter(star_indices) j = next(iter_star_indices) buffer = parts[:j] # fixed pieces at the start append, extend = buffer.append, buffer.extend i = j + 1 for j in iter_star_indices: # Now deal with STAR fixed STAR fixed ... # For an interior `STAR fixed` pairing, we want to do a minimal # .*? match followed by `fixed`, with no possibility of backtracking. # Atomic groups ("(?>...)") allow us to spell that directly. # Note: people rely on the undocumented ability to join multiple # translate() results together via "|" to build large regexps matching # "one of many" shell patterns. append('(?>.*?') extend(parts[i:j]) append(')') i = j + 1 append('.*') extend(parts[i:]) res = ''.join(buffer) return fr'(?s:{res})\Z' pathlib-abc-0.5.2/pathlib_abc/_glob.py000066400000000000000000000447201507225105000175540ustar00rootroot00000000000000"""Filename globbing utility.""" import contextlib import os import re from pathlib_abc import _fnmatch as fnmatch import functools import itertools import operator import stat import sys __all__ = ["glob", "iglob", "escape", "translate"] def glob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False): """Return a list of paths matching a pathname pattern. The pattern may contain simple shell-style wildcards a la fnmatch. Unlike fnmatch, filenames starting with a dot are special cases that are not matched by '*' and '?' patterns by default. If `include_hidden` is true, the patterns '*', '?', '**' will match hidden directories. If `recursive` is true, the pattern '**' will match any files and zero or more directories and subdirectories. """ return list(iglob(pathname, root_dir=root_dir, dir_fd=dir_fd, recursive=recursive, include_hidden=include_hidden)) def iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False, include_hidden=False): """Return an iterator which yields the paths matching a pathname pattern. The pattern may contain simple shell-style wildcards a la fnmatch. However, unlike fnmatch, filenames starting with a dot are special cases that are not matched by '*' and '?' patterns. If recursive is true, the pattern '**' will match any files and zero or more directories and subdirectories. """ sys.audit("glob.glob", pathname, recursive) sys.audit("glob.glob/2", pathname, recursive, root_dir, dir_fd) if root_dir is not None: root_dir = os.fspath(root_dir) else: root_dir = pathname[:0] it = _iglob(pathname, root_dir, dir_fd, recursive, False, include_hidden=include_hidden) if not pathname or recursive and _isrecursive(pathname[:2]): try: s = next(it) # skip empty string if s: it = itertools.chain((s,), it) except StopIteration: pass return it def _iglob(pathname, root_dir, dir_fd, recursive, dironly, include_hidden=False): dirname, basename = os.path.split(pathname) if not has_magic(pathname): assert not dironly if basename: if _lexists(_join(root_dir, pathname), dir_fd): yield pathname else: # Patterns ending with a slash should match only directories if _isdir(_join(root_dir, dirname), dir_fd): yield pathname return if not dirname: if recursive and _isrecursive(basename): yield from _glob2(root_dir, basename, dir_fd, dironly, include_hidden=include_hidden) else: yield from _glob1(root_dir, basename, dir_fd, dironly, include_hidden=include_hidden) return # `os.path.split()` returns the argument itself as a dirname if it is a # drive or UNC path. Prevent an infinite recursion if a drive or UNC path # contains magic characters (i.e. r'\\?\C:'). if dirname != pathname and has_magic(dirname): dirs = _iglob(dirname, root_dir, dir_fd, recursive, True, include_hidden=include_hidden) else: dirs = [dirname] if has_magic(basename): if recursive and _isrecursive(basename): glob_in_dir = _glob2 else: glob_in_dir = _glob1 else: glob_in_dir = _glob0 for dirname in dirs: for name in glob_in_dir(_join(root_dir, dirname), basename, dir_fd, dironly, include_hidden=include_hidden): yield os.path.join(dirname, name) # These 2 helper functions non-recursively glob inside a literal directory. # They return a list of basenames. _glob1 accepts a pattern while _glob0 # takes a literal basename (so it only has to check for its existence). def _glob1(dirname, pattern, dir_fd, dironly, include_hidden=False): names = _listdir(dirname, dir_fd, dironly) if not (include_hidden or _ishidden(pattern)): names = (x for x in names if not _ishidden(x)) return fnmatch.filter(names, pattern) def _glob0(dirname, basename, dir_fd, dironly, include_hidden=False): if basename: if _lexists(_join(dirname, basename), dir_fd): return [basename] else: # `os.path.split()` returns an empty basename for paths ending with a # directory separator. 'q*x/' should match only directories. if _isdir(dirname, dir_fd): return [basename] return [] # This helper function recursively yields relative pathnames inside a literal # directory. def _glob2(dirname, pattern, dir_fd, dironly, include_hidden=False): assert _isrecursive(pattern) if not dirname or _isdir(dirname, dir_fd): yield pattern[:0] yield from _rlistdir(dirname, dir_fd, dironly, include_hidden=include_hidden) # If dironly is false, yields all file names inside a directory. # If dironly is true, yields only directory names. def _iterdir(dirname, dir_fd, dironly): try: fd = None fsencode = None if dir_fd is not None: if dirname: fd = arg = os.open(dirname, _dir_open_flags, dir_fd=dir_fd) else: arg = dir_fd if isinstance(dirname, bytes): fsencode = os.fsencode elif dirname: arg = dirname elif isinstance(dirname, bytes): arg = bytes(os.curdir, 'ASCII') else: arg = os.curdir try: with os.scandir(arg) as it: for entry in it: try: if not dironly or entry.is_dir(): if fsencode is not None: yield fsencode(entry.name) else: yield entry.name except OSError: pass finally: if fd is not None: os.close(fd) except OSError: return def _listdir(dirname, dir_fd, dironly): with contextlib.closing(_iterdir(dirname, dir_fd, dironly)) as it: return list(it) # Recursively yields relative pathnames inside a literal directory. def _rlistdir(dirname, dir_fd, dironly, include_hidden=False): names = _listdir(dirname, dir_fd, dironly) for x in names: if include_hidden or not _ishidden(x): yield x path = _join(dirname, x) if dirname else x for y in _rlistdir(path, dir_fd, dironly, include_hidden=include_hidden): yield _join(x, y) def _lexists(pathname, dir_fd): # Same as os.path.lexists(), but with dir_fd if dir_fd is None: return os.path.lexists(pathname) try: os.lstat(pathname, dir_fd=dir_fd) except (OSError, ValueError): return False else: return True def _isdir(pathname, dir_fd): # Same as os.path.isdir(), but with dir_fd if dir_fd is None: return os.path.isdir(pathname) try: st = os.stat(pathname, dir_fd=dir_fd) except (OSError, ValueError): return False else: return stat.S_ISDIR(st.st_mode) def _join(dirname, basename): # It is common if dirname or basename is empty if not dirname or not basename: return dirname or basename return os.path.join(dirname, basename) magic_check = re.compile('([*?[])') magic_check_bytes = re.compile(b'([*?[])') def has_magic(s): if isinstance(s, bytes): match = magic_check_bytes.search(s) else: match = magic_check.search(s) return match is not None def _ishidden(path): return path[0] in ('.', b'.'[0]) def _isrecursive(pattern): if isinstance(pattern, bytes): return pattern == b'**' else: return pattern == '**' def escape(pathname): """Escape all special characters. """ # Escaping is done by wrapping any of "*?[" between square brackets. # Metacharacters do not work in the drive part and shouldn't be escaped. drive, pathname = os.path.splitdrive(pathname) if isinstance(pathname, bytes): pathname = magic_check_bytes.sub(br'[\1]', pathname) else: pathname = magic_check.sub(r'[\1]', pathname) return drive + pathname _special_parts = ('', '.', '..') _dir_open_flags = os.O_RDONLY | getattr(os, 'O_DIRECTORY', 0) _no_recurse_symlinks = object() def translate(pat, *, recursive=False, include_hidden=False, seps=None): """Translate a pathname with shell wildcards to a regular expression. If `recursive` is true, the pattern segment '**' will match any number of path segments. If `include_hidden` is true, wildcards can match path segments beginning with a dot ('.'). If a sequence of separator characters is given to `seps`, they will be used to split the pattern into segments and match path separators. If not given, os.path.sep and os.path.altsep (where available) are used. """ if not seps: if os.path.altsep: seps = (os.path.sep, os.path.altsep) else: seps = os.path.sep escaped_seps = ''.join(map(re.escape, seps)) any_sep = f'[{escaped_seps}]' if len(seps) > 1 else escaped_seps not_sep = f'[^{escaped_seps}]' if include_hidden: one_last_segment = f'{not_sep}+' one_segment = f'{one_last_segment}{any_sep}' any_segments = f'(?:.+{any_sep})?' any_last_segments = '.*' else: one_last_segment = f'[^{escaped_seps}.]{not_sep}*' one_segment = f'{one_last_segment}{any_sep}' any_segments = f'(?:{one_segment})*' any_last_segments = f'{any_segments}(?:{one_last_segment})?' results = [] parts = re.split(any_sep, pat) last_part_idx = len(parts) - 1 for idx, part in enumerate(parts): if part == '*': results.append(one_segment if idx < last_part_idx else one_last_segment) elif recursive and part == '**': if idx < last_part_idx: if parts[idx + 1] != '**': results.append(any_segments) else: results.append(any_last_segments) else: if part: if not include_hidden and part[0] in '*?': results.append(r'(?!\.)') results.extend(fnmatch._translate(part, f'{not_sep}*', not_sep)[0]) if idx < last_part_idx: results.append(any_sep) res = ''.join(results) return fr'(?s:{res})\Z' @functools.lru_cache(maxsize=512) def _compile_pattern(pat, seps, case_sensitive, recursive=True): """Compile given glob pattern to a re.Pattern object (observing case sensitivity).""" flags = 0 if case_sensitive else re.IGNORECASE regex = translate(pat, recursive=recursive, include_hidden=True, seps=seps) return re.compile(regex, flags=flags).match class _GlobberBase: """Abstract class providing shell-style pattern matching and globbing. """ def __init__(self, sep, case_sensitive, case_pedantic=False, recursive=False): self.sep = sep self.case_sensitive = case_sensitive self.case_pedantic = case_pedantic self.recursive = recursive # Abstract methods @staticmethod def lexists(path): """Implements os.path.lexists(). """ raise NotImplementedError @staticmethod def scandir(path): """Like os.scandir(), but generates (entry, name, path) tuples. """ raise NotImplementedError @staticmethod def concat_path(path, text): """Implements path concatenation. """ raise NotImplementedError @staticmethod def stringify_path(path): """Converts the path to a string object """ raise NotImplementedError # High-level methods def compile(self, pat, altsep=None): seps = (self.sep, altsep) if altsep else self.sep return _compile_pattern(pat, seps, self.case_sensitive, self.recursive) def selector(self, parts): """Returns a function that selects from a given path, walking and filtering according to the glob-style pattern parts in *parts*. """ if not parts: return self.select_exists part = parts.pop() if self.recursive and part == '**': selector = self.recursive_selector elif part in _special_parts: selector = self.special_selector elif not self.case_pedantic and magic_check.search(part) is None: selector = self.literal_selector else: selector = self.wildcard_selector return selector(part, parts) def special_selector(self, part, parts): """Returns a function that selects special children of the given path. """ if parts: part += self.sep select_next = self.selector(parts) def select_special(path, exists=False): path = self.concat_path(path, part) return select_next(path, exists) return select_special def literal_selector(self, part, parts): """Returns a function that selects a literal descendant of a path. """ # Optimization: consume and join any subsequent literal parts here, # rather than leaving them for the next selector. This reduces the # number of string concatenation operations. while parts and magic_check.search(parts[-1]) is None: part += self.sep + parts.pop() if parts: part += self.sep select_next = self.selector(parts) def select_literal(path, exists=False): path = self.concat_path(path, part) return select_next(path, exists=False) return select_literal def wildcard_selector(self, part, parts): """Returns a function that selects direct children of a given path, filtering by pattern. """ match = None if part == '*' else self.compile(part) dir_only = bool(parts) if dir_only: select_next = self.selector(parts) def select_wildcard(path, exists=False): try: entries = self.scandir(path) except OSError: pass else: for entry, entry_name, entry_path in entries: if match is None or match(entry_name): if dir_only: try: if not entry.is_dir(): continue except OSError: continue entry_path = self.concat_path(entry_path, self.sep) yield from select_next(entry_path, exists=True) else: yield entry_path return select_wildcard def recursive_selector(self, part, parts): """Returns a function that selects a given path and all its children, recursively, filtering by pattern. """ # Optimization: consume following '**' parts, which have no effect. while parts and parts[-1] == '**': parts.pop() # Optimization: consume and join any following non-special parts here, # rather than leaving them for the next selector. They're used to # build a regular expression, which we use to filter the results of # the recursive walk. As a result, non-special pattern segments # following a '**' wildcard don't require additional filesystem access # to expand. follow_symlinks = self.recursive is not _no_recurse_symlinks if follow_symlinks: while parts and parts[-1] not in _special_parts: part += self.sep + parts.pop() match = None if part == '**' else self.compile(part) dir_only = bool(parts) select_next = self.selector(parts) def select_recursive(path, exists=False): path_str = self.stringify_path(path) match_pos = len(path_str) if match is None or match(path_str, match_pos): yield from select_next(path, exists) stack = [path] while stack: yield from select_recursive_step(stack, match_pos) def select_recursive_step(stack, match_pos): path = stack.pop() try: entries = self.scandir(path) except OSError: pass else: for entry, _entry_name, entry_path in entries: is_dir = False try: if entry.is_dir(follow_symlinks=follow_symlinks): is_dir = True except OSError: pass if is_dir or not dir_only: entry_path_str = self.stringify_path(entry_path) if dir_only: entry_path = self.concat_path(entry_path, self.sep) if match is None or match(entry_path_str, match_pos): if dir_only: yield from select_next(entry_path, exists=True) else: # Optimization: directly yield the path if this is # last pattern part. yield entry_path if is_dir: stack.append(entry_path) return select_recursive def select_exists(self, path, exists=False): """Yields the given path, if it exists. """ if exists: # Optimization: this path is already known to exist, e.g. because # it was returned from os.scandir(), so we skip calling lstat(). yield path elif self.lexists(path): yield path class _StringGlobber(_GlobberBase): """Provides shell-style pattern matching and globbing for string paths. """ lexists = staticmethod(os.path.lexists) concat_path = operator.add @staticmethod def scandir(path): # We must close the scandir() object before proceeding to # avoid exhausting file descriptors when globbing deep trees. with os.scandir(path) as scandir_it: entries = list(scandir_it) return ((entry, entry.name, entry.path) for entry in entries) @staticmethod def stringify_path(path): return path # Already a string. pathlib-abc-0.5.2/pathlib_abc/_os.py000066400000000000000000000226211507225105000172460ustar00rootroot00000000000000""" Low-level OS functionality wrappers used by pathlib. """ from errno import * from io import TextIOWrapper import os import sys try: from io import text_encoding except ImportError: def text_encoding(encoding): return encoding try: import fcntl except ImportError: fcntl = None try: import posix except ImportError: posix = None try: import _winapi except ImportError: _winapi = None def _get_copy_blocksize(infd): """Determine blocksize for fastcopying on Linux. Hopefully the whole file will be copied in a single call. The copying itself should be performed in a loop 'till EOF is reached (0 return) so a blocksize smaller or bigger than the actual file size should not make any difference, also in case the file content changes while being copied. """ try: blocksize = max(os.fstat(infd).st_size, 2 ** 23) # min 8 MiB except OSError: blocksize = 2 ** 27 # 128 MiB # On 32-bit architectures truncate to 1 GiB to avoid OverflowError, # see gh-82500. if sys.maxsize < 2 ** 32: blocksize = min(blocksize, 2 ** 30) return blocksize if fcntl and hasattr(fcntl, 'FICLONE'): def _ficlone(source_fd, target_fd): """ Perform a lightweight copy of two files, where the data blocks are copied only when modified. This is known as Copy on Write (CoW), instantaneous copy or reflink. """ fcntl.ioctl(target_fd, fcntl.FICLONE, source_fd) else: _ficlone = None if posix and hasattr(posix, '_fcopyfile'): def _fcopyfile(source_fd, target_fd): """ Copy a regular file content using high-performance fcopyfile(3) syscall (macOS). """ posix._fcopyfile(source_fd, target_fd, posix._COPYFILE_DATA) else: _fcopyfile = None if hasattr(os, 'copy_file_range'): def _copy_file_range(source_fd, target_fd): """ Copy data from one regular mmap-like fd to another by using a high-performance copy_file_range(2) syscall that gives filesystems an opportunity to implement the use of reflinks or server-side copy. This should work on Linux >= 4.5 only. """ blocksize = _get_copy_blocksize(source_fd) offset = 0 while True: sent = os.copy_file_range(source_fd, target_fd, blocksize, offset_dst=offset) if sent == 0: break # EOF offset += sent else: _copy_file_range = None if hasattr(os, 'sendfile'): def _sendfile(source_fd, target_fd): """Copy data from one regular mmap-like fd to another by using high-performance sendfile(2) syscall. This should work on Linux >= 2.6.33 only. """ blocksize = _get_copy_blocksize(source_fd) offset = 0 while True: sent = os.sendfile(target_fd, source_fd, offset, blocksize) if sent == 0: break # EOF offset += sent else: _sendfile = None if _winapi and hasattr(_winapi, 'CopyFile2'): def copyfile2(source, target): """ Copy from one file to another using CopyFile2 (Windows only). """ _winapi.CopyFile2(source, target, 0) else: copyfile2 = None def copyfileobj(source_f, target_f): """ Copy data from file-like object source_f to file-like object target_f. """ try: source_fd = source_f.fileno() target_fd = target_f.fileno() except Exception: pass # Fall through to generic code. else: try: # Use OS copy-on-write where available. if _ficlone: try: _ficlone(source_fd, target_fd) return except OSError as err: if err.errno not in (EBADF, EOPNOTSUPP, ETXTBSY, EXDEV): raise err # Use OS copy where available. if _fcopyfile: try: _fcopyfile(source_fd, target_fd) return except OSError as err: if err.errno not in (EINVAL, ENOTSUP): raise err if _copy_file_range: try: _copy_file_range(source_fd, target_fd) return except OSError as err: if err.errno not in (ETXTBSY, EXDEV): raise err if _sendfile: try: _sendfile(source_fd, target_fd) return except OSError as err: if err.errno != ENOTSOCK: raise err except OSError as err: # Produce more useful error messages. err.filename = source_f.name err.filename2 = target_f.name raise err # Last resort: copy with fileobj read() and write(). read_source = source_f.read write_target = target_f.write while buf := read_source(1024 * 1024): write_target(buf) def _open_reader(obj): cls = type(obj) try: open_reader = cls.__open_reader__ except AttributeError: cls_name = cls.__name__ raise TypeError(f"{cls_name} can't be opened for reading") from None else: return open_reader(obj) def _open_writer(obj, mode): cls = type(obj) try: open_writer = cls.__open_writer__ except AttributeError: cls_name = cls.__name__ raise TypeError(f"{cls_name} can't be opened for writing") from None else: return open_writer(obj, mode) def _open_updater(obj, mode): cls = type(obj) try: open_updater = cls.__open_updater__ except AttributeError: cls_name = cls.__name__ raise TypeError(f"{cls_name} can't be opened for updating") from None else: return open_updater(obj, mode) def vfsopen(obj, mode='r', buffering=-1, encoding=None, errors=None, newline=None): """ Open the file pointed to by this path and return a file object, as the built-in open() function does. Unlike the built-in open() function, this function additionally accepts 'openable' objects, which are objects with any of these special methods: __open_reader__() __open_writer__(mode) __open_updater__(mode) '__open_reader__' is called for 'r' mode; '__open_writer__' for 'a', 'w' and 'x' modes; and '__open_updater__' for 'r+' and 'w+' modes. If text mode is requested, the result is wrapped in an io.TextIOWrapper object. """ if buffering != -1: raise ValueError("buffer size can't be customized") text = 'b' not in mode if text: # Call io.text_encoding() here to ensure any warning is raised at an # appropriate stack level. encoding = text_encoding(encoding) try: return open(obj, mode, buffering, encoding, errors, newline) except TypeError: pass if not text: if encoding is not None: raise ValueError("binary mode doesn't take an encoding argument") if errors is not None: raise ValueError("binary mode doesn't take an errors argument") if newline is not None: raise ValueError("binary mode doesn't take a newline argument") mode = ''.join(sorted(c for c in mode if c not in 'bt')) if mode == 'r': stream = _open_reader(obj) elif mode in ('a', 'w', 'x'): stream = _open_writer(obj, mode) elif mode in ('+r', '+w'): stream = _open_updater(obj, mode[1]) else: raise ValueError(f'invalid mode: {mode}') if text: stream = TextIOWrapper(stream, encoding, errors, newline) return stream def vfspath(obj): """ Return the string representation of a virtual path object. """ cls = type(obj) try: vfspath_method = cls.__vfspath__ except AttributeError: cls_name = cls.__name__ raise TypeError(f"expected JoinablePath object, not {cls_name}") from None else: return vfspath_method(obj) def ensure_distinct_paths(source, target): """ Raise OSError(EINVAL) if the other path is within this path. """ # Note: there is no straightforward, foolproof algorithm to determine # if one directory is within another (a particularly perverse example # would be a single network share mounted in one location via NFS, and # in another location via CIFS), so we simply checks whether the # other path is lexically equal to, or within, this path. if source == target: err = OSError(EINVAL, "Source and target are the same path") elif source in target.parents: err = OSError(EINVAL, "Source path is a parent of target path") else: return err.filename = vfspath(source) err.filename2 = vfspath(target) raise err def ensure_different_files(source, target): """ Raise OSError(EINVAL) if both paths refer to the same file. """ try: source_file_id = source.info._file_id target_file_id = target.info._file_id except AttributeError: if source != target: return else: try: if source_file_id() != target_file_id(): return except (OSError, ValueError): return err = OSError(EINVAL, "Source and target are the same file") err.filename = vfspath(source) err.filename2 = vfspath(target) raise err pathlib-abc-0.5.2/pyproject.toml000066400000000000000000000012221507225105000165720ustar00rootroot00000000000000[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "pathlib_abc" version = "0.5.2" authors = [ { name="Barney Gale", email="barney.gale@gmail.com" }, ] description = "Backport of pathlib ABCs" readme = "README.rst" license = {file = "LICENSE.txt"} requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: Python Software Foundation License", "Operating System :: OS Independent", "Intended Audience :: Developers" ] [project.urls] Homepage = "https://github.com/barneygale/pathlib-abc" Issues = "https://github.com/barneygale/pathlib-abc/issues" pathlib-abc-0.5.2/tests/000077500000000000000000000000001507225105000150235ustar00rootroot00000000000000pathlib-abc-0.5.2/tests/__init__.py000066400000000000000000000002161507225105000171330ustar00rootroot00000000000000import os from test.support import load_package_tests def load_tests(*args): return load_package_tests(os.path.dirname(__file__), *args) pathlib-abc-0.5.2/tests/support/000077500000000000000000000000001507225105000165375ustar00rootroot00000000000000pathlib-abc-0.5.2/tests/support/__init__.py000066400000000000000000000000171507225105000206460ustar00rootroot00000000000000is_pypi = True pathlib-abc-0.5.2/tests/support/lexical_path.py000066400000000000000000000021621507225105000215470ustar00rootroot00000000000000""" Simple implementation of JoinablePath, for use in pathlib tests. """ import ntpath import os.path import posixpath from . import is_pypi if is_pypi: from pathlib_abc import vfspath, _JoinablePath else: from pathlib.types import _JoinablePath from pathlib._os import vfspath class LexicalPath(_JoinablePath): __slots__ = ('_segments',) parser = os.path def __init__(self, *pathsegments): self._segments = pathsegments def __hash__(self): return hash(vfspath(self)) def __eq__(self, other): if not isinstance(other, LexicalPath): return NotImplemented return vfspath(self) == vfspath(other) def __vfspath__(self): if not self._segments: return '' return self.parser.join(*self._segments) def __repr__(self): return f'{type(self).__name__}({vfspath(self)!r})' def with_segments(self, *pathsegments): return type(self)(*pathsegments) class LexicalPosixPath(LexicalPath): __slots__ = () parser = posixpath class LexicalWindowsPath(LexicalPath): __slots__ = () parser = ntpath pathlib-abc-0.5.2/tests/support/local_path.py000066400000000000000000000123441507225105000212230ustar00rootroot00000000000000""" Implementations of ReadablePath and WritablePath for local paths, for use in pathlib tests. LocalPathGround is also defined here. It helps establish the "ground truth" about local paths in tests. """ import os from . import is_pypi from .lexical_path import LexicalPath if is_pypi: from shutil import rmtree from pathlib_abc import PathInfo, _ReadablePath, _WritablePath can_symlink = True testfn = "TESTFN" else: from pathlib.types import PathInfo, _ReadablePath, _WritablePath from test.support import os_helper can_symlink = os_helper.can_symlink() testfn = os_helper.TESTFN rmtree = os_helper.rmtree class LocalPathGround: can_symlink = can_symlink def __init__(self, path_cls): self.path_cls = path_cls def setup(self, local_suffix=""): root = self.path_cls(testfn + local_suffix) os.mkdir(root) return root def teardown(self, root): rmtree(root) def create_file(self, p, data=b''): with open(p, 'wb') as f: f.write(data) def create_dir(self, p): os.mkdir(p) def create_symlink(self, p, target): os.symlink(target, p) def create_hierarchy(self, p): os.mkdir(os.path.join(p, 'dirA')) os.mkdir(os.path.join(p, 'dirB')) os.mkdir(os.path.join(p, 'dirC')) os.mkdir(os.path.join(p, 'dirC', 'dirD')) with open(os.path.join(p, 'fileA'), 'wb') as f: f.write(b"this is file A\n") with open(os.path.join(p, 'dirB', 'fileB'), 'wb') as f: f.write(b"this is file B\n") with open(os.path.join(p, 'dirC', 'fileC'), 'wb') as f: f.write(b"this is file C\n") with open(os.path.join(p, 'dirC', 'novel.txt'), 'wb') as f: f.write(b"this is a novel\n") with open(os.path.join(p, 'dirC', 'dirD', 'fileD'), 'wb') as f: f.write(b"this is file D\n") if self.can_symlink: # Relative symlinks. os.symlink('fileA', os.path.join(p, 'linkA')) os.symlink('non-existing', os.path.join(p, 'brokenLink')) os.symlink('dirB', os.path.join(p, 'linkB'), target_is_directory=True) os.symlink(os.path.join('..', 'dirB'), os.path.join(p, 'dirA', 'linkC'), target_is_directory=True) # Broken symlink (pointing to itself). os.symlink('brokenLinkLoop', os.path.join(p, 'brokenLinkLoop')) isdir = staticmethod(os.path.isdir) isfile = staticmethod(os.path.isfile) islink = staticmethod(os.path.islink) readlink = staticmethod(os.readlink) def readtext(self, p): with open(p, 'r', encoding='utf-8') as f: return f.read() def readbytes(self, p): with open(p, 'rb') as f: return f.read() class LocalPathInfo(PathInfo): """ Simple implementation of PathInfo for a local path """ __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink') def __init__(self, path): self._path = os.fspath(path) self._exists = None self._is_dir = None self._is_file = None self._is_symlink = None def exists(self, *, follow_symlinks=True): """Whether this path exists.""" if not follow_symlinks and self.is_symlink(): return True if self._exists is None: self._exists = os.path.exists(self._path) return self._exists def is_dir(self, *, follow_symlinks=True): """Whether this path is a directory.""" if not follow_symlinks and self.is_symlink(): return False if self._is_dir is None: self._is_dir = os.path.isdir(self._path) return self._is_dir def is_file(self, *, follow_symlinks=True): """Whether this path is a regular file.""" if not follow_symlinks and self.is_symlink(): return False if self._is_file is None: self._is_file = os.path.isfile(self._path) return self._is_file def is_symlink(self): """Whether this path is a symbolic link.""" if self._is_symlink is None: self._is_symlink = os.path.islink(self._path) return self._is_symlink class ReadableLocalPath(_ReadablePath, LexicalPath): """ Simple implementation of a ReadablePath class for local filesystem paths. """ __slots__ = ('info',) __fspath__ = LexicalPath.__vfspath__ def __init__(self, *pathsegments): super().__init__(*pathsegments) self.info = LocalPathInfo(self) def __open_reader__(self): return open(self, 'rb') def iterdir(self): return (self / name for name in os.listdir(self)) def readlink(self): return self.with_segments(os.readlink(self)) class WritableLocalPath(_WritablePath, LexicalPath): """ Simple implementation of a WritablePath class for local filesystem paths. """ __slots__ = () __fspath__ = LexicalPath.__vfspath__ def __open_writer__(self, mode): return open(self, f'{mode}b') def mkdir(self, mode=0o777): os.mkdir(self, mode) def symlink_to(self, target, target_is_directory=False): os.symlink(target, self, target_is_directory) pathlib-abc-0.5.2/tests/support/zip_path.py000066400000000000000000000252621507225105000207360ustar00rootroot00000000000000""" Implementations of ReadablePath and WritablePath for zip file members, for use in pathlib tests. ZipPathGround is also defined here. It helps establish the "ground truth" about zip file members in tests. """ import errno import io import posixpath import stat import zipfile from stat import S_IFMT, S_ISDIR, S_ISREG, S_ISLNK from . import is_pypi if is_pypi: from pathlib_abc import vfspath, PathInfo, _ReadablePath, _WritablePath else: from pathlib.types import PathInfo, _ReadablePath, _WritablePath from pathlib._os import vfspath class ZipPathGround: can_symlink = True def __init__(self, path_cls): self.path_cls = path_cls def setup(self, local_suffix=""): return self.path_cls(zip_file=zipfile.ZipFile(io.BytesIO(), "w")) def teardown(self, root): root.zip_file.close() def create_file(self, path, data=b''): path.zip_file.writestr(vfspath(path), data) def create_dir(self, path): zip_info = zipfile.ZipInfo(vfspath(path) + '/') zip_info.external_attr |= stat.S_IFDIR << 16 zip_info.external_attr |= stat.FILE_ATTRIBUTE_DIRECTORY path.zip_file.writestr(zip_info, '') def create_symlink(self, path, target): zip_info = zipfile.ZipInfo(vfspath(path)) zip_info.external_attr = stat.S_IFLNK << 16 path.zip_file.writestr(zip_info, target.encode()) def create_hierarchy(self, p): # Add regular files self.create_file(p.joinpath('fileA'), b'this is file A\n') self.create_file(p.joinpath('dirB/fileB'), b'this is file B\n') self.create_file(p.joinpath('dirC/fileC'), b'this is file C\n') self.create_file(p.joinpath('dirC/dirD/fileD'), b'this is file D\n') self.create_file(p.joinpath('dirC/novel.txt'), b'this is a novel\n') # Add symlinks self.create_symlink(p.joinpath('linkA'), 'fileA') self.create_symlink(p.joinpath('linkB'), 'dirB') self.create_symlink(p.joinpath('dirA/linkC'), '../dirB') self.create_symlink(p.joinpath('brokenLink'), 'non-existing') self.create_symlink(p.joinpath('brokenLinkLoop'), 'brokenLinkLoop') def readtext(self, p): with p.zip_file.open(vfspath(p), 'r') as f: f = io.TextIOWrapper(f, encoding='utf-8') return f.read() def readbytes(self, p): with p.zip_file.open(vfspath(p), 'r') as f: return f.read() readlink = readtext def isdir(self, p): path_str = vfspath(p) + "/" return path_str in p.zip_file.NameToInfo def isfile(self, p): info = p.zip_file.NameToInfo.get(vfspath(p)) if info is None: return False return not stat.S_ISLNK(info.external_attr >> 16) def islink(self, p): info = p.zip_file.NameToInfo.get(vfspath(p)) if info is None: return False return stat.S_ISLNK(info.external_attr >> 16) class MissingZipPathInfo(PathInfo): """ PathInfo implementation that is used when a zip file member is missing. """ __slots__ = () def exists(self, follow_symlinks=True): return False def is_dir(self, follow_symlinks=True): return False def is_file(self, follow_symlinks=True): return False def is_symlink(self): return False def resolve(self): return self missing_zip_path_info = MissingZipPathInfo() class ZipPathInfo(PathInfo): """ PathInfo implementation for an existing zip file member. """ __slots__ = ('zip_file', 'zip_info', 'parent', 'children') def __init__(self, zip_file, parent=None): self.zip_file = zip_file self.zip_info = None self.parent = parent or self self.children = {} def exists(self, follow_symlinks=True): if follow_symlinks and self.is_symlink(): return self.resolve().exists() return True def is_dir(self, follow_symlinks=True): if follow_symlinks and self.is_symlink(): return self.resolve().is_dir() elif self.zip_info is None: return True elif fmt := S_IFMT(self.zip_info.external_attr >> 16): return S_ISDIR(fmt) else: return self.zip_info.filename.endswith('/') def is_file(self, follow_symlinks=True): if follow_symlinks and self.is_symlink(): return self.resolve().is_file() elif self.zip_info is None: return False elif fmt := S_IFMT(self.zip_info.external_attr >> 16): return S_ISREG(fmt) else: return not self.zip_info.filename.endswith('/') def is_symlink(self): if self.zip_info is None: return False elif fmt := S_IFMT(self.zip_info.external_attr >> 16): return S_ISLNK(fmt) else: return False def resolve(self, path=None, create=False, follow_symlinks=True): """ Traverse zip hierarchy (parents, children and symlinks) starting from this PathInfo. This is called from three places: - When a zip file member is added to ZipFile.filelist, this method populates the ZipPathInfo tree (using create=True). - When ReadableZipPath.info is accessed, this method is finds a ZipPathInfo entry for the path without resolving any final symlink (using follow_symlinks=False) - When ZipPathInfo methods are called with follow_symlinks=True, this method resolves any symlink in the final path position. """ link_count = 0 stack = path.split('/')[::-1] if path else [] info = self while True: if info.is_symlink() and (follow_symlinks or stack): link_count += 1 if link_count >= 40: return missing_zip_path_info # Symlink loop! path = info.zip_file.read(info.zip_info).decode() stack += path.split('/')[::-1] if path else [] info = info.parent if stack: name = stack.pop() else: return info if name == '..': info = info.parent elif name and name != '.': if name not in info.children: if create: info.children[name] = ZipPathInfo(info.zip_file, info) else: return missing_zip_path_info # No such child! info = info.children[name] class ZipFileList: """ `list`-like object that we inject as `ZipFile.filelist`. We maintain a tree of `ZipPathInfo` objects representing the zip file members. """ __slots__ = ('tree', '_items') def __init__(self, zip_file): self.tree = ZipPathInfo(zip_file) self._items = [] for item in zip_file.filelist: self.append(item) def __len__(self): return len(self._items) def __iter__(self): return iter(self._items) def append(self, item): self._items.append(item) self.tree.resolve(item.filename, create=True).zip_info = item class ReadableZipPath(_ReadablePath): """ Simple implementation of a ReadablePath class for .zip files. """ __slots__ = ('_segments', 'zip_file') parser = posixpath def __init__(self, *pathsegments, zip_file): self._segments = pathsegments self.zip_file = zip_file if not isinstance(zip_file.filelist, ZipFileList): zip_file.filelist = ZipFileList(zip_file) def __hash__(self): return hash((vfspath(self), self.zip_file)) def __eq__(self, other): if not isinstance(other, ReadableZipPath): return NotImplemented return vfspath(self) == vfspath(other) and self.zip_file is other.zip_file def __vfspath__(self): if not self._segments: return '' return self.parser.join(*self._segments) def __repr__(self): return f'{type(self).__name__}({vfspath(self)!r}, zip_file={self.zip_file!r})' def with_segments(self, *pathsegments): return type(self)(*pathsegments, zip_file=self.zip_file) @property def info(self): tree = self.zip_file.filelist.tree return tree.resolve(vfspath(self), follow_symlinks=False) def __open_reader__(self): info = self.info.resolve() if not info.exists(): raise FileNotFoundError(errno.ENOENT, "File not found", self) elif info.is_dir(): raise IsADirectoryError(errno.EISDIR, "Is a directory", self) return self.zip_file.open(info.zip_info) def iterdir(self): info = self.info.resolve() if not info.exists(): raise FileNotFoundError(errno.ENOENT, "File not found", self) elif not info.is_dir(): raise NotADirectoryError(errno.ENOTDIR, "Not a directory", self) return (self / name for name in info.children) def readlink(self): info = self.info if not info.exists(): raise FileNotFoundError(errno.ENOENT, "File not found", self) elif not info.is_symlink(): raise OSError(errno.EINVAL, "Not a symlink", self) return self.with_segments(self.zip_file.read(info.zip_info).decode()) class WritableZipPath(_WritablePath): """ Simple implementation of a WritablePath class for .zip files. """ __slots__ = ('_segments', 'zip_file') parser = posixpath def __init__(self, *pathsegments, zip_file): self._segments = pathsegments self.zip_file = zip_file def __hash__(self): return hash((vfspath(self), self.zip_file)) def __eq__(self, other): if not isinstance(other, WritableZipPath): return NotImplemented return vfspath(self) == vfspath(other) and self.zip_file is other.zip_file def __vfspath__(self): if not self._segments: return '' return self.parser.join(*self._segments) def __repr__(self): return f'{type(self).__name__}({vfspath(self)!r}, zip_file={self.zip_file!r})' def with_segments(self, *pathsegments): return type(self)(*pathsegments, zip_file=self.zip_file) def __open_writer__(self, mode): return self.zip_file.open(vfspath(self), mode) def mkdir(self, mode=0o777): zinfo = zipfile.ZipInfo(vfspath(self) + '/') zinfo.external_attr |= stat.S_IFDIR << 16 zinfo.external_attr |= stat.FILE_ATTRIBUTE_DIRECTORY self.zip_file.writestr(zinfo, '') def symlink_to(self, target, target_is_directory=False): zinfo = zipfile.ZipInfo(vfspath(self)) zinfo.external_attr = stat.S_IFLNK << 16 if target_is_directory: zinfo.external_attr |= 0x10 self.zip_file.writestr(zinfo, target) pathlib-abc-0.5.2/tests/test_copy.py000066400000000000000000000166701507225105000174200ustar00rootroot00000000000000""" Tests for copying from pathlib.types._ReadablePath to _WritablePath. """ import contextlib import unittest from .support import is_pypi from .support.local_path import LocalPathGround from .support.zip_path import ZipPathGround, ReadableZipPath, WritableZipPath class CopyTestBase: def setUp(self): self.source_root = self.source_ground.setup() self.source_ground.create_hierarchy(self.source_root) self.target_root = self.target_ground.setup(local_suffix="_target") def tearDown(self): self.source_ground.teardown(self.source_root) self.target_ground.teardown(self.target_root) def test_copy_file(self): source = self.source_root / 'fileA' target = self.target_root / 'copyA' result = source.copy(target) self.assertEqual(result, target) self.assertTrue(self.target_ground.isfile(target)) self.assertEqual(self.source_ground.readbytes(source), self.target_ground.readbytes(result)) def test_copy_file_empty(self): source = self.source_root / 'empty' target = self.target_root / 'copyA' self.source_ground.create_file(source, b'') result = source.copy(target) self.assertEqual(result, target) self.assertTrue(self.target_ground.isfile(target)) self.assertEqual(self.target_ground.readbytes(result), b'') def test_copy_file_to_existing_file(self): source = self.source_root / 'fileA' target = self.target_root / 'copyA' self.target_ground.create_file(target, b'this is a copy\n') with contextlib.ExitStack() as stack: if isinstance(target, WritableZipPath): stack.enter_context(self.assertWarns(UserWarning)) result = source.copy(target) self.assertEqual(result, target) self.assertTrue(self.target_ground.isfile(target)) self.assertEqual(self.source_ground.readbytes(source), self.target_ground.readbytes(result)) def test_copy_file_to_directory(self): if isinstance(self.target_root, WritableZipPath): self.skipTest('needs local target') source = self.source_root / 'fileA' target = self.target_root / 'copyA' self.target_ground.create_dir(target) self.assertRaises(OSError, source.copy, target) def test_copy_file_to_itself(self): source = self.source_root / 'fileA' self.assertRaises(OSError, source.copy, source) self.assertRaises(OSError, source.copy, source, follow_symlinks=False) def test_copy_dir(self): source = self.source_root / 'dirC' target = self.target_root / 'copyC' result = source.copy(target) self.assertEqual(result, target) self.assertTrue(self.target_ground.isdir(target)) self.assertTrue(self.target_ground.isfile(target / 'fileC')) self.assertEqual(self.target_ground.readtext(target / 'fileC'), 'this is file C\n') self.assertTrue(self.target_ground.isdir(target / 'dirD')) self.assertTrue(self.target_ground.isfile(target / 'dirD' / 'fileD')) self.assertEqual(self.target_ground.readtext(target / 'dirD' / 'fileD'), 'this is file D\n') def test_copy_dir_follow_symlinks_true(self): if not self.source_ground.can_symlink: self.skipTest('needs symlink support on source') source = self.source_root / 'dirC' target = self.target_root / 'copyC' self.source_ground.create_symlink(source / 'linkC', 'fileC') self.source_ground.create_symlink(source / 'linkD', 'dirD') result = source.copy(target) self.assertEqual(result, target) self.assertTrue(self.target_ground.isdir(target)) self.assertFalse(self.target_ground.islink(target / 'linkC')) self.assertTrue(self.target_ground.isfile(target / 'linkC')) self.assertEqual(self.target_ground.readtext(target / 'linkC'), 'this is file C\n') self.assertFalse(self.target_ground.islink(target / 'linkD')) self.assertTrue(self.target_ground.isdir(target / 'linkD')) self.assertTrue(self.target_ground.isfile(target / 'linkD' / 'fileD')) self.assertEqual(self.target_ground.readtext(target / 'linkD' / 'fileD'), 'this is file D\n') def test_copy_dir_follow_symlinks_false(self): if not self.source_ground.can_symlink: self.skipTest('needs symlink support on source') if not self.target_ground.can_symlink: self.skipTest('needs symlink support on target') source = self.source_root / 'dirC' target = self.target_root / 'copyC' self.source_ground.create_symlink(source / 'linkC', 'fileC') self.source_ground.create_symlink(source / 'linkD', 'dirD') result = source.copy(target, follow_symlinks=False) self.assertEqual(result, target) self.assertTrue(self.target_ground.isdir(target)) self.assertTrue(self.target_ground.islink(target / 'linkC')) self.assertEqual(self.target_ground.readlink(target / 'linkC'), 'fileC') self.assertTrue(self.target_ground.islink(target / 'linkD')) self.assertEqual(self.target_ground.readlink(target / 'linkD'), 'dirD') def test_copy_dir_to_existing_directory(self): if isinstance(self.target_root, WritableZipPath): self.skipTest('needs local target') source = self.source_root / 'dirC' target = self.target_root / 'copyC' self.target_ground.create_dir(target) self.assertRaises(FileExistsError, source.copy, target) def test_copy_dir_to_itself(self): source = self.source_root / 'dirC' self.assertRaises(OSError, source.copy, source) self.assertRaises(OSError, source.copy, source, follow_symlinks=False) def test_copy_dir_into_itself(self): source = self.source_root / 'dirC' target = self.source_root / 'dirC' / 'dirD' / 'copyC' self.assertRaises(OSError, source.copy, target) self.assertRaises(OSError, source.copy, target, follow_symlinks=False) def test_copy_into(self): source = self.source_root / 'fileA' target_dir = self.target_root / 'dirA' self.target_ground.create_dir(target_dir) result = source.copy_into(target_dir) self.assertEqual(result, target_dir / 'fileA') self.assertTrue(self.target_ground.isfile(result)) self.assertEqual(self.source_ground.readbytes(source), self.target_ground.readbytes(result)) def test_copy_into_empty_name(self): source = self.source_root.with_segments() target_dir = self.target_root / 'dirA' self.target_ground.create_dir(target_dir) self.assertRaises(ValueError, source.copy_into, target_dir) class ZipToZipPathCopyTest(CopyTestBase, unittest.TestCase): source_ground = ZipPathGround(ReadableZipPath) target_ground = ZipPathGround(WritableZipPath) if not is_pypi: from pathlib import Path class ZipToLocalPathCopyTest(CopyTestBase, unittest.TestCase): source_ground = ZipPathGround(ReadableZipPath) target_ground = LocalPathGround(Path) class LocalToZipPathCopyTest(CopyTestBase, unittest.TestCase): source_ground = LocalPathGround(Path) target_ground = ZipPathGround(WritableZipPath) class LocalToLocalPathCopyTest(CopyTestBase, unittest.TestCase): source_ground = LocalPathGround(Path) target_ground = LocalPathGround(Path) if __name__ == "__main__": unittest.main() pathlib-abc-0.5.2/tests/test_join.py000066400000000000000000000446541507225105000174100ustar00rootroot00000000000000""" Tests for pathlib.types._JoinablePath """ import unittest from .support import is_pypi from .support.lexical_path import LexicalPath if is_pypi: from pathlib_abc import _PathParser, _JoinablePath else: from pathlib.types import _PathParser, _JoinablePath class JoinTestBase: def test_is_joinable(self): p = self.cls() self.assertIsInstance(p, _JoinablePath) def test_parser(self): self.assertIsInstance(self.cls.parser, _PathParser) def test_constructor(self): P = self.cls p = P('a') self.assertIsInstance(p, P) P() P('a', 'b', 'c') P('/a', 'b', 'c') P('a/b/c') P('/a/b/c') def test_with_segments(self): class P(self.cls): def __init__(self, *pathsegments, session_id): super().__init__(*pathsegments) self.session_id = session_id def with_segments(self, *pathsegments): return type(self)(*pathsegments, session_id=self.session_id) p = P('foo', 'bar', session_id=42) self.assertEqual(42, (p / 'foo').session_id) self.assertEqual(42, ('foo' / p).session_id) self.assertEqual(42, p.joinpath('foo').session_id) self.assertEqual(42, p.with_name('foo').session_id) self.assertEqual(42, p.with_stem('foo').session_id) self.assertEqual(42, p.with_suffix('.foo').session_id) self.assertEqual(42, p.with_segments('foo').session_id) self.assertEqual(42, p.parent.session_id) for parent in p.parents: self.assertEqual(42, parent.session_id) def test_join(self): P = self.cls sep = self.cls.parser.sep p = P(f'a{sep}b') pp = p.joinpath('c') self.assertEqual(pp, P(f'a{sep}b{sep}c')) self.assertIs(type(pp), type(p)) pp = p.joinpath('c', 'd') self.assertEqual(pp, P(f'a{sep}b{sep}c{sep}d')) pp = p.joinpath(f'{sep}c') self.assertEqual(pp, P(f'{sep}c')) def test_div(self): # Basically the same as joinpath(). P = self.cls sep = self.cls.parser.sep p = P(f'a{sep}b') pp = p / 'c' self.assertEqual(pp, P(f'a{sep}b{sep}c')) self.assertIs(type(pp), type(p)) pp = p / f'c{sep}d' self.assertEqual(pp, P(f'a{sep}b{sep}c{sep}d')) pp = p / 'c' / 'd' self.assertEqual(pp, P(f'a{sep}b{sep}c{sep}d')) pp = 'c' / p / 'd' self.assertEqual(pp, P(f'c{sep}a{sep}b{sep}d')) pp = p/ f'{sep}c' self.assertEqual(pp, P(f'{sep}c')) def test_full_match(self): P = self.cls # Simple relative pattern. self.assertTrue(P('b.py').full_match('b.py')) self.assertFalse(P('a/b.py').full_match('b.py')) self.assertFalse(P('/a/b.py').full_match('b.py')) self.assertFalse(P('a.py').full_match('b.py')) self.assertFalse(P('b/py').full_match('b.py')) self.assertFalse(P('/a.py').full_match('b.py')) self.assertFalse(P('b.py/c').full_match('b.py')) # Wildcard relative pattern. self.assertTrue(P('b.py').full_match('*.py')) self.assertFalse(P('a/b.py').full_match('*.py')) self.assertFalse(P('/a/b.py').full_match('*.py')) self.assertFalse(P('b.pyc').full_match('*.py')) self.assertFalse(P('b./py').full_match('*.py')) self.assertFalse(P('b.py/c').full_match('*.py')) # Multi-part relative pattern. self.assertTrue(P('ab/c.py').full_match('a*/*.py')) self.assertFalse(P('/d/ab/c.py').full_match('a*/*.py')) self.assertFalse(P('a.py').full_match('a*/*.py')) self.assertFalse(P('/dab/c.py').full_match('a*/*.py')) self.assertFalse(P('ab/c.py/d').full_match('a*/*.py')) # Absolute pattern. self.assertTrue(P('/b.py').full_match('/*.py')) self.assertFalse(P('b.py').full_match('/*.py')) self.assertFalse(P('a/b.py').full_match('/*.py')) self.assertFalse(P('/a/b.py').full_match('/*.py')) # Multi-part absolute pattern. self.assertTrue(P('/a/b.py').full_match('/a/*.py')) self.assertFalse(P('/ab.py').full_match('/a/*.py')) self.assertFalse(P('/a/b/c.py').full_match('/a/*.py')) # Multi-part glob-style pattern. self.assertTrue(P('a').full_match('**')) self.assertTrue(P('c.py').full_match('**')) self.assertTrue(P('a/b/c.py').full_match('**')) self.assertTrue(P('/a/b/c.py').full_match('**')) self.assertTrue(P('/a/b/c.py').full_match('/**')) self.assertTrue(P('/a/b/c.py').full_match('/a/**')) self.assertTrue(P('/a/b/c.py').full_match('**/*.py')) self.assertTrue(P('/a/b/c.py').full_match('/**/*.py')) self.assertTrue(P('/a/b/c.py').full_match('/a/**/*.py')) self.assertTrue(P('/a/b/c.py').full_match('/a/b/**/*.py')) self.assertTrue(P('/a/b/c.py').full_match('/**/**/**/**/*.py')) self.assertFalse(P('c.py').full_match('**/a.py')) self.assertFalse(P('c.py').full_match('c/**')) self.assertFalse(P('a/b/c.py').full_match('**/a')) self.assertFalse(P('a/b/c.py').full_match('**/a/b')) self.assertFalse(P('a/b/c.py').full_match('**/a/b/c')) self.assertFalse(P('a/b/c.py').full_match('**/a/b/c.')) self.assertFalse(P('a/b/c.py').full_match('**/a/b/c./**')) self.assertFalse(P('a/b/c.py').full_match('**/a/b/c./**')) self.assertFalse(P('a/b/c.py').full_match('/a/b/c.py/**')) self.assertFalse(P('a/b/c.py').full_match('/**/a/b/c.py')) # Matching against empty path self.assertFalse(P('').full_match('*')) self.assertTrue(P('').full_match('**')) self.assertFalse(P('').full_match('**/*')) # Matching with empty pattern self.assertTrue(P('').full_match('')) self.assertTrue(P('.').full_match('.')) self.assertFalse(P('/').full_match('')) self.assertFalse(P('/').full_match('.')) self.assertFalse(P('foo').full_match('')) self.assertFalse(P('foo').full_match('.')) def test_parts(self): # `parts` returns a tuple. sep = self.cls.parser.sep P = self.cls p = P(f'a{sep}b') parts = p.parts self.assertEqual(parts, ('a', 'b')) # When the path is absolute, the anchor is a separate part. p = P(f'{sep}a{sep}b') parts = p.parts self.assertEqual(parts, (sep, 'a', 'b')) def test_parent(self): # Relative P = self.cls p = P('a/b/c') self.assertEqual(p.parent, P('a/b')) self.assertEqual(p.parent.parent, P('a')) self.assertEqual(p.parent.parent.parent, P('')) self.assertEqual(p.parent.parent.parent.parent, P('')) # Anchored p = P('/a/b/c') self.assertEqual(p.parent, P('/a/b')) self.assertEqual(p.parent.parent, P('/a')) self.assertEqual(p.parent.parent.parent, P('/')) self.assertEqual(p.parent.parent.parent.parent, P('/')) def test_parents(self): # Relative P = self.cls p = P('a/b/c') par = p.parents self.assertEqual(len(par), 3) self.assertEqual(par[0], P('a/b')) self.assertEqual(par[1], P('a')) self.assertEqual(par[2], P('')) self.assertEqual(par[-1], P('')) self.assertEqual(par[-2], P('a')) self.assertEqual(par[-3], P('a/b')) self.assertEqual(par[0:1], (P('a/b'),)) self.assertEqual(par[:2], (P('a/b'), P('a'))) self.assertEqual(par[:-1], (P('a/b'), P('a'))) self.assertEqual(par[1:], (P('a'), P(''))) self.assertEqual(par[::2], (P('a/b'), P(''))) self.assertEqual(par[::-1], (P(''), P('a'), P('a/b'))) self.assertEqual(list(par), [P('a/b'), P('a'), P('')]) with self.assertRaises(IndexError): par[-4] with self.assertRaises(IndexError): par[3] with self.assertRaises(TypeError): par[0] = p # Anchored p = P('/a/b/c') par = p.parents self.assertEqual(len(par), 3) self.assertEqual(par[0], P('/a/b')) self.assertEqual(par[1], P('/a')) self.assertEqual(par[2], P('/')) self.assertEqual(par[-1], P('/')) self.assertEqual(par[-2], P('/a')) self.assertEqual(par[-3], P('/a/b')) self.assertEqual(par[0:1], (P('/a/b'),)) self.assertEqual(par[:2], (P('/a/b'), P('/a'))) self.assertEqual(par[:-1], (P('/a/b'), P('/a'))) self.assertEqual(par[1:], (P('/a'), P('/'))) self.assertEqual(par[::2], (P('/a/b'), P('/'))) self.assertEqual(par[::-1], (P('/'), P('/a'), P('/a/b'))) self.assertEqual(list(par), [P('/a/b'), P('/a'), P('/')]) with self.assertRaises(IndexError): par[-4] with self.assertRaises(IndexError): par[3] def test_anchor(self): P = self.cls sep = self.cls.parser.sep self.assertEqual(P('').anchor, '') self.assertEqual(P(f'a{sep}b').anchor, '') self.assertEqual(P(sep).anchor, sep) self.assertEqual(P(f'{sep}a{sep}b').anchor, sep) def test_name(self): P = self.cls self.assertEqual(P('').name, '') self.assertEqual(P('/').name, '') self.assertEqual(P('a/b').name, 'b') self.assertEqual(P('/a/b').name, 'b') self.assertEqual(P('a/b.py').name, 'b.py') self.assertEqual(P('/a/b.py').name, 'b.py') def test_suffix(self): P = self.cls self.assertEqual(P('').suffix, '') self.assertEqual(P('.').suffix, '') self.assertEqual(P('..').suffix, '') self.assertEqual(P('/').suffix, '') self.assertEqual(P('a/b').suffix, '') self.assertEqual(P('/a/b').suffix, '') self.assertEqual(P('/a/b/.').suffix, '') self.assertEqual(P('a/b.py').suffix, '.py') self.assertEqual(P('/a/b.py').suffix, '.py') self.assertEqual(P('a/.hgrc').suffix, '') self.assertEqual(P('/a/.hgrc').suffix, '') self.assertEqual(P('a/.hg.rc').suffix, '.rc') self.assertEqual(P('/a/.hg.rc').suffix, '.rc') self.assertEqual(P('a/b.tar.gz').suffix, '.gz') self.assertEqual(P('/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('a/trailing.dot.').suffix, '.') self.assertEqual(P('/a/trailing.dot.').suffix, '.') self.assertEqual(P('a/..d.o.t..').suffix, '.') self.assertEqual(P('a/inn.er..dots').suffix, '.dots') self.assertEqual(P('photo').suffix, '') self.assertEqual(P('photo.jpg').suffix, '.jpg') def test_suffixes(self): P = self.cls self.assertEqual(P('').suffixes, []) self.assertEqual(P('.').suffixes, []) self.assertEqual(P('/').suffixes, []) self.assertEqual(P('a/b').suffixes, []) self.assertEqual(P('/a/b').suffixes, []) self.assertEqual(P('/a/b/.').suffixes, []) self.assertEqual(P('a/b.py').suffixes, ['.py']) self.assertEqual(P('/a/b.py').suffixes, ['.py']) self.assertEqual(P('a/.hgrc').suffixes, []) self.assertEqual(P('/a/.hgrc').suffixes, []) self.assertEqual(P('a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('/a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('a/trailing.dot.').suffixes, ['.dot', '.']) self.assertEqual(P('/a/trailing.dot.').suffixes, ['.dot', '.']) self.assertEqual(P('a/..d.o.t..').suffixes, ['.o', '.t', '.', '.']) self.assertEqual(P('a/inn.er..dots').suffixes, ['.er', '.', '.dots']) self.assertEqual(P('photo').suffixes, []) self.assertEqual(P('photo.jpg').suffixes, ['.jpg']) def test_stem(self): P = self.cls self.assertEqual(P('..').stem, '..') self.assertEqual(P('').stem, '') self.assertEqual(P('/').stem, '') self.assertEqual(P('a/b').stem, 'b') self.assertEqual(P('a/b.py').stem, 'b') self.assertEqual(P('a/.hgrc').stem, '.hgrc') self.assertEqual(P('a/.hg.rc').stem, '.hg') self.assertEqual(P('a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('a/trailing.dot.').stem, 'trailing.dot') self.assertEqual(P('a/..d.o.t..').stem, '..d.o.t.') self.assertEqual(P('a/inn.er..dots').stem, 'inn.er.') self.assertEqual(P('photo').stem, 'photo') self.assertEqual(P('photo.jpg').stem, 'photo') def test_with_name(self): P = self.cls self.assertEqual(P('a/b').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/b').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/b.py').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/b.py').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/Dot ending.').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/Dot ending.').with_name('d.xml'), P('/a/d.xml')) self.assertRaises(ValueError, P('a/b').with_name, '/c') self.assertRaises(ValueError, P('a/b').with_name, 'c/') self.assertRaises(ValueError, P('a/b').with_name, 'c/d') def test_with_stem(self): P = self.cls self.assertEqual(P('a/b').with_stem('d'), P('a/d')) self.assertEqual(P('/a/b').with_stem('d'), P('/a/d')) self.assertEqual(P('a/b.py').with_stem('d'), P('a/d.py')) self.assertEqual(P('/a/b.py').with_stem('d'), P('/a/d.py')) self.assertEqual(P('/a/b.tar.gz').with_stem('d'), P('/a/d.gz')) self.assertEqual(P('a/Dot ending.').with_stem('d'), P('a/d.')) self.assertEqual(P('/a/Dot ending.').with_stem('d'), P('/a/d.')) self.assertRaises(ValueError, P('foo.gz').with_stem, '') self.assertRaises(ValueError, P('/a/b/foo.gz').with_stem, '') self.assertRaises(ValueError, P('a/b').with_stem, '/c') self.assertRaises(ValueError, P('a/b').with_stem, 'c/') self.assertRaises(ValueError, P('a/b').with_stem, 'c/d') def test_with_suffix(self): P = self.cls self.assertEqual(P('a/b').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b').with_suffix('.gz'), P('/a/b.gz')) self.assertEqual(P('a/b.py').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b.py').with_suffix('.gz'), P('/a/b.gz')) # Stripping suffix. self.assertEqual(P('a/b.py').with_suffix(''), P('a/b')) self.assertEqual(P('/a/b').with_suffix(''), P('/a/b')) # Single dot self.assertEqual(P('a/b').with_suffix('.'), P('a/b.')) self.assertEqual(P('/a/b').with_suffix('.'), P('/a/b.')) self.assertEqual(P('a/b.py').with_suffix('.'), P('a/b.')) self.assertEqual(P('/a/b.py').with_suffix('.'), P('/a/b.')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('/').with_suffix, '.gz') # Invalid suffix. self.assertRaises(ValueError, P('a/b').with_suffix, 'gz') self.assertRaises(ValueError, P('a/b').with_suffix, '/') self.assertRaises(ValueError, P('a/b').with_suffix, '/.gz') self.assertRaises(ValueError, P('a/b').with_suffix, 'c/d') self.assertRaises(ValueError, P('a/b').with_suffix, '.c/.d') self.assertRaises(ValueError, P('a/b').with_suffix, './.d') self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.') self.assertRaises(TypeError, P('a/b').with_suffix, None) def test_relative_to(self): P = self.cls p = P('a/b') self.assertEqual(p.relative_to(P('')), P('a', 'b')) self.assertEqual(p.relative_to(P('a')), P('b')) self.assertEqual(p.relative_to(P('a/b')), P('')) self.assertEqual(p.relative_to(P(''), walk_up=True), P('a', 'b')) self.assertEqual(p.relative_to(P('a'), walk_up=True), P('b')) self.assertEqual(p.relative_to(P('a/b'), walk_up=True), P('')) self.assertEqual(p.relative_to(P('a/c'), walk_up=True), P('..', 'b')) self.assertEqual(p.relative_to(P('a/b/c'), walk_up=True), P('..')) self.assertEqual(p.relative_to(P('c'), walk_up=True), P('..', 'a', 'b')) self.assertRaises(ValueError, p.relative_to, P('c')) self.assertRaises(ValueError, p.relative_to, P('a/b/c')) self.assertRaises(ValueError, p.relative_to, P('a/c')) self.assertRaises(ValueError, p.relative_to, P('/a')) self.assertRaises(ValueError, p.relative_to, P('../a')) self.assertRaises(ValueError, p.relative_to, P('a/..')) self.assertRaises(ValueError, p.relative_to, P('/a/..')) self.assertRaises(ValueError, p.relative_to, P('/'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('/a'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('../a'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('a/..'), walk_up=True) self.assertRaises(ValueError, p.relative_to, P('/a/..'), walk_up=True) class Q(self.cls): __eq__ = object.__eq__ __hash__ = object.__hash__ q = Q('a/b') self.assertTrue(q.relative_to(q)) self.assertRaises(ValueError, q.relative_to, Q('')) self.assertRaises(ValueError, q.relative_to, Q('a')) self.assertRaises(ValueError, q.relative_to, Q('a'), walk_up=True) self.assertRaises(ValueError, q.relative_to, Q('a/b')) self.assertRaises(ValueError, q.relative_to, Q('c')) def test_is_relative_to(self): P = self.cls p = P('a/b') self.assertTrue(p.is_relative_to(P(''))) self.assertTrue(p.is_relative_to(P('a'))) self.assertTrue(p.is_relative_to(P('a/b'))) self.assertFalse(p.is_relative_to(P('c'))) self.assertFalse(p.is_relative_to(P('a/b/c'))) self.assertFalse(p.is_relative_to(P('a/c'))) self.assertFalse(p.is_relative_to(P('/a'))) class Q(self.cls): __eq__ = object.__eq__ __hash__ = object.__hash__ q = Q('a/b') self.assertTrue(q.is_relative_to(q)) self.assertFalse(q.is_relative_to(Q(''))) self.assertFalse(q.is_relative_to(Q('a'))) self.assertFalse(q.is_relative_to(Q('a/b'))) self.assertFalse(q.is_relative_to(Q('c'))) class LexicalPathJoinTest(JoinTestBase, unittest.TestCase): cls = LexicalPath if not is_pypi: from pathlib import PurePath, Path class PurePathJoinTest(JoinTestBase, unittest.TestCase): cls = PurePath class PathJoinTest(JoinTestBase, unittest.TestCase): cls = Path if __name__ == "__main__": unittest.main() pathlib-abc-0.5.2/tests/test_join_posix.py000066400000000000000000000022741507225105000206220ustar00rootroot00000000000000""" Tests for Posix-flavoured pathlib.types._JoinablePath """ import os import unittest from .support import is_pypi from .support.lexical_path import LexicalPosixPath class JoinTestBase: def test_join(self): P = self.cls p = P('//a') pp = p.joinpath('b') self.assertEqual(pp, P('//a/b')) pp = P('/a').joinpath('//c') self.assertEqual(pp, P('//c')) pp = P('//a').joinpath('/c') self.assertEqual(pp, P('/c')) def test_div(self): # Basically the same as joinpath(). P = self.cls p = P('//a') pp = p / 'b' self.assertEqual(pp, P('//a/b')) pp = P('/a') / '//c' self.assertEqual(pp, P('//c')) pp = P('//a') / '/c' self.assertEqual(pp, P('/c')) class LexicalPosixPathJoinTest(JoinTestBase, unittest.TestCase): cls = LexicalPosixPath if not is_pypi: from pathlib import PurePosixPath, PosixPath class PurePosixPathJoinTest(JoinTestBase, unittest.TestCase): cls = PurePosixPath if os.name != 'nt': class PosixPathJoinTest(JoinTestBase, unittest.TestCase): cls = PosixPath if __name__ == "__main__": unittest.main() pathlib-abc-0.5.2/tests/test_join_windows.py000066400000000000000000000306011507225105000211450ustar00rootroot00000000000000""" Tests for Windows-flavoured pathlib.types._JoinablePath """ import os import unittest from .support import is_pypi from .support.lexical_path import LexicalWindowsPath if is_pypi: from pathlib_abc import vfspath else: from pathlib._os import vfspath class JoinTestBase: def test_join(self): P = self.cls p = P('C:/a/b') pp = p.joinpath('x/y') self.assertEqual(pp, P(r'C:/a/b\x/y')) pp = p.joinpath('/x/y') self.assertEqual(pp, P('C:/x/y')) # Joining with a different drive => the first path is ignored, even # if the second path is relative. pp = p.joinpath('D:x/y') self.assertEqual(pp, P('D:x/y')) pp = p.joinpath('D:/x/y') self.assertEqual(pp, P('D:/x/y')) pp = p.joinpath('//host/share/x/y') self.assertEqual(pp, P('//host/share/x/y')) # Joining with the same drive => the first path is appended to if # the second path is relative. pp = p.joinpath('c:x/y') self.assertEqual(pp, P(r'c:/a/b\x/y')) pp = p.joinpath('c:/x/y') self.assertEqual(pp, P('c:/x/y')) # Joining with files with NTFS data streams => the filename should # not be parsed as a drive letter pp = p.joinpath('./d:s') self.assertEqual(pp, P(r'C:/a/b\./d:s')) pp = p.joinpath('./dd:s') self.assertEqual(pp, P(r'C:/a/b\./dd:s')) pp = p.joinpath('E:d:s') self.assertEqual(pp, P('E:d:s')) # Joining onto a UNC path with no root pp = P('//server').joinpath('share') self.assertEqual(pp, P(r'//server\share')) pp = P('//./BootPartition').joinpath('Windows') self.assertEqual(pp, P(r'//./BootPartition\Windows')) def test_div(self): # Basically the same as joinpath(). P = self.cls p = P('C:/a/b') self.assertEqual(p / 'x/y', P(r'C:/a/b\x/y')) self.assertEqual(p / 'x' / 'y', P(r'C:/a/b\x\y')) self.assertEqual(p / '/x/y', P('C:/x/y')) self.assertEqual(p / '/x' / 'y', P(r'C:/x\y')) # Joining with a different drive => the first path is ignored, even # if the second path is relative. self.assertEqual(p / 'D:x/y', P('D:x/y')) self.assertEqual(p / 'D:' / 'x/y', P('D:x/y')) self.assertEqual(p / 'D:/x/y', P('D:/x/y')) self.assertEqual(p / 'D:' / '/x/y', P('D:/x/y')) self.assertEqual(p / '//host/share/x/y', P('//host/share/x/y')) # Joining with the same drive => the first path is appended to if # the second path is relative. self.assertEqual(p / 'c:x/y', P(r'c:/a/b\x/y')) self.assertEqual(p / 'c:/x/y', P('c:/x/y')) # Joining with files with NTFS data streams => the filename should # not be parsed as a drive letter self.assertEqual(p / './d:s', P(r'C:/a/b\./d:s')) self.assertEqual(p / './dd:s', P(r'C:/a/b\./dd:s')) self.assertEqual(p / 'E:d:s', P('E:d:s')) def test_vfspath(self): p = self.cls(r'a\b\c') self.assertEqual(vfspath(p), 'a\\b\\c') p = self.cls(r'c:\a\b\c') self.assertEqual(vfspath(p), 'c:\\a\\b\\c') p = self.cls('\\\\a\\b\\') self.assertEqual(vfspath(p), '\\\\a\\b\\') p = self.cls(r'\\a\b\c') self.assertEqual(vfspath(p), '\\\\a\\b\\c') p = self.cls(r'\\a\b\c\d') self.assertEqual(vfspath(p), '\\\\a\\b\\c\\d') def test_parts(self): P = self.cls p = P(r'c:a\b') parts = p.parts self.assertEqual(parts, ('c:', 'a', 'b')) p = P(r'c:\a\b') parts = p.parts self.assertEqual(parts, ('c:\\', 'a', 'b')) p = P(r'\\a\b\c\d') parts = p.parts self.assertEqual(parts, ('\\\\a\\b\\', 'c', 'd')) def test_parent(self): # Anchored P = self.cls p = P('z:a/b/c') self.assertEqual(p.parent, P('z:a/b')) self.assertEqual(p.parent.parent, P('z:a')) self.assertEqual(p.parent.parent.parent, P('z:')) self.assertEqual(p.parent.parent.parent.parent, P('z:')) p = P('z:/a/b/c') self.assertEqual(p.parent, P('z:/a/b')) self.assertEqual(p.parent.parent, P('z:/a')) self.assertEqual(p.parent.parent.parent, P('z:/')) self.assertEqual(p.parent.parent.parent.parent, P('z:/')) p = P('//a/b/c/d') self.assertEqual(p.parent, P('//a/b/c')) self.assertEqual(p.parent.parent, P('//a/b/')) self.assertEqual(p.parent.parent.parent, P('//a/b/')) def test_parents(self): # Anchored P = self.cls p = P('z:a/b') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('z:a')) self.assertEqual(par[1], P('z:')) self.assertEqual(par[0:1], (P('z:a'),)) self.assertEqual(par[:-1], (P('z:a'),)) self.assertEqual(par[:2], (P('z:a'), P('z:'))) self.assertEqual(par[1:], (P('z:'),)) self.assertEqual(par[::2], (P('z:a'),)) self.assertEqual(par[::-1], (P('z:'), P('z:a'))) self.assertEqual(list(par), [P('z:a'), P('z:')]) with self.assertRaises(IndexError): par[2] p = P('z:/a/b') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('z:/a')) self.assertEqual(par[1], P('z:/')) self.assertEqual(par[0:1], (P('z:/a'),)) self.assertEqual(par[0:-1], (P('z:/a'),)) self.assertEqual(par[:2], (P('z:/a'), P('z:/'))) self.assertEqual(par[1:], (P('z:/'),)) self.assertEqual(par[::2], (P('z:/a'),)) self.assertEqual(par[::-1], (P('z:/'), P('z:/a'),)) self.assertEqual(list(par), [P('z:/a'), P('z:/')]) with self.assertRaises(IndexError): par[2] p = P('//a/b/c/d') par = p.parents self.assertEqual(len(par), 2) self.assertEqual(par[0], P('//a/b/c')) self.assertEqual(par[1], P('//a/b/')) self.assertEqual(par[0:1], (P('//a/b/c'),)) self.assertEqual(par[0:-1], (P('//a/b/c'),)) self.assertEqual(par[:2], (P('//a/b/c'), P('//a/b/'))) self.assertEqual(par[1:], (P('//a/b/'),)) self.assertEqual(par[::2], (P('//a/b/c'),)) self.assertEqual(par[::-1], (P('//a/b/'), P('//a/b/c'))) self.assertEqual(list(par), [P('//a/b/c'), P('//a/b/')]) with self.assertRaises(IndexError): par[2] def test_anchor(self): P = self.cls self.assertEqual(P('c:').anchor, 'c:') self.assertEqual(P('c:a/b').anchor, 'c:') self.assertEqual(P('c:\\').anchor, 'c:\\') self.assertEqual(P('c:\\a\\b\\').anchor, 'c:\\') self.assertEqual(P('\\\\a\\b\\').anchor, '\\\\a\\b\\') self.assertEqual(P('\\\\a\\b\\c\\d').anchor, '\\\\a\\b\\') def test_name(self): P = self.cls self.assertEqual(P('c:').name, '') self.assertEqual(P('c:/').name, '') self.assertEqual(P('c:a/b').name, 'b') self.assertEqual(P('c:/a/b').name, 'b') self.assertEqual(P('c:a/b.py').name, 'b.py') self.assertEqual(P('c:/a/b.py').name, 'b.py') self.assertEqual(P('//My.py/Share.php').name, '') self.assertEqual(P('//My.py/Share.php/a/b').name, 'b') def test_stem(self): P = self.cls self.assertEqual(P('c:').stem, '') self.assertEqual(P('c:..').stem, '..') self.assertEqual(P('c:/').stem, '') self.assertEqual(P('c:a/b').stem, 'b') self.assertEqual(P('c:a/b.py').stem, 'b') self.assertEqual(P('c:a/.hgrc').stem, '.hgrc') self.assertEqual(P('c:a/.hg.rc').stem, '.hg') self.assertEqual(P('c:a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('c:a/trailing.dot.').stem, 'trailing.dot') def test_suffix(self): P = self.cls self.assertEqual(P('c:').suffix, '') self.assertEqual(P('c:/').suffix, '') self.assertEqual(P('c:a/b').suffix, '') self.assertEqual(P('c:/a/b').suffix, '') self.assertEqual(P('c:a/b.py').suffix, '.py') self.assertEqual(P('c:/a/b.py').suffix, '.py') self.assertEqual(P('c:a/.hgrc').suffix, '') self.assertEqual(P('c:/a/.hgrc').suffix, '') self.assertEqual(P('c:a/.hg.rc').suffix, '.rc') self.assertEqual(P('c:/a/.hg.rc').suffix, '.rc') self.assertEqual(P('c:a/b.tar.gz').suffix, '.gz') self.assertEqual(P('c:/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('c:a/trailing.dot.').suffix, '.') self.assertEqual(P('c:/a/trailing.dot.').suffix, '.') self.assertEqual(P('//My.py/Share.php').suffix, '') self.assertEqual(P('//My.py/Share.php/a/b').suffix, '') def test_suffixes(self): P = self.cls self.assertEqual(P('c:').suffixes, []) self.assertEqual(P('c:/').suffixes, []) self.assertEqual(P('c:a/b').suffixes, []) self.assertEqual(P('c:/a/b').suffixes, []) self.assertEqual(P('c:a/b.py').suffixes, ['.py']) self.assertEqual(P('c:/a/b.py').suffixes, ['.py']) self.assertEqual(P('c:a/.hgrc').suffixes, []) self.assertEqual(P('c:/a/.hgrc').suffixes, []) self.assertEqual(P('c:a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('c:/a/.hg.rc').suffixes, ['.rc']) self.assertEqual(P('c:a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('c:/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('//My.py/Share.php').suffixes, []) self.assertEqual(P('//My.py/Share.php/a/b').suffixes, []) self.assertEqual(P('c:a/trailing.dot.').suffixes, ['.dot', '.']) self.assertEqual(P('c:/a/trailing.dot.').suffixes, ['.dot', '.']) def test_with_name(self): P = self.cls self.assertEqual(P(r'c:a\b').with_name('d.xml'), P(r'c:a\d.xml')) self.assertEqual(P(r'c:\a\b').with_name('d.xml'), P(r'c:\a\d.xml')) self.assertEqual(P(r'c:a\Dot ending.').with_name('d.xml'), P(r'c:a\d.xml')) self.assertEqual(P(r'c:\a\Dot ending.').with_name('d.xml'), P(r'c:\a\d.xml')) self.assertRaises(ValueError, P(r'c:a\b').with_name, r'd:\e') self.assertRaises(ValueError, P(r'c:a\b').with_name, r'\\My\Share') def test_with_stem(self): P = self.cls self.assertEqual(P('c:a/b').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/b').with_stem('d'), P('c:/a/d')) self.assertEqual(P('c:a/Dot ending.').with_stem('d'), P('c:a/d.')) self.assertEqual(P('c:/a/Dot ending.').with_stem('d'), P('c:/a/d.')) self.assertRaises(ValueError, P('c:a/b').with_stem, 'd:/e') self.assertRaises(ValueError, P('c:a/b').with_stem, '//My/Share') def test_with_suffix(self): P = self.cls self.assertEqual(P('c:a/b').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b').with_suffix('.gz'), P('c:/a/b.gz')) self.assertEqual(P('c:a/b.py').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b.py').with_suffix('.gz'), P('c:/a/b.gz')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('/').with_suffix, '.gz') self.assertRaises(ValueError, P('//My/Share').with_suffix, '.gz') # Invalid suffix. self.assertRaises(ValueError, P('c:a/b').with_suffix, 'gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, '/') self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:') self.assertRaises(ValueError, P('c:a/b').with_suffix, '/.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:.gz') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c/d') self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c\\d') self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c/d') self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c\\d') self.assertRaises(TypeError, P('c:a/b').with_suffix, None) class LexicalWindowsPathJoinTest(JoinTestBase, unittest.TestCase): cls = LexicalWindowsPath if not is_pypi: from pathlib import PureWindowsPath, WindowsPath class PureWindowsPathJoinTest(JoinTestBase, unittest.TestCase): cls = PureWindowsPath if os.name == 'nt': class WindowsPathJoinTest(JoinTestBase, unittest.TestCase): cls = WindowsPath if __name__ == "__main__": unittest.main() pathlib-abc-0.5.2/tests/test_read.py000066400000000000000000000366631507225105000173650ustar00rootroot00000000000000""" Tests for pathlib.types._ReadablePath """ import collections.abc import io import sys import unittest from .support import is_pypi from .support.local_path import ReadableLocalPath, LocalPathGround from .support.zip_path import ReadableZipPath, ZipPathGround if is_pypi: from pathlib_abc import PathInfo, _ReadablePath from pathlib_abc._os import vfsopen else: from pathlib.types import PathInfo, _ReadablePath from pathlib._os import vfsopen class ReadTestBase: def setUp(self): self.root = self.ground.setup() self.ground.create_hierarchy(self.root) def tearDown(self): self.ground.teardown(self.root) def test_is_readable(self): self.assertIsInstance(self.root, _ReadablePath) def test_open_r(self): p = self.root / 'fileA' with vfsopen(p, 'r', encoding='utf-8') as f: self.assertIsInstance(f, io.TextIOBase) self.assertEqual(f.read(), 'this is file A\n') def test_open_r_buffering_error(self): p = self.root / 'fileA' self.assertRaises(ValueError, vfsopen, p, 'r', buffering=0) self.assertRaises(ValueError, vfsopen, p, 'r', buffering=1) self.assertRaises(ValueError, vfsopen, p, 'r', buffering=1024) @unittest.skipIf( not getattr(sys.flags, 'warn_default_encoding', 0), "Requires warn_default_encoding", ) def test_open_r_encoding_warning(self): p = self.root / 'fileA' with self.assertWarns(EncodingWarning) as wc: with vfsopen(p, 'r'): pass self.assertEqual(wc.filename, __file__) def test_open_rb(self): p = self.root / 'fileA' with vfsopen(p, 'rb') as f: self.assertEqual(f.read(), b'this is file A\n') self.assertRaises(ValueError, vfsopen, p, 'rb', encoding='utf8') self.assertRaises(ValueError, vfsopen, p, 'rb', errors='strict') self.assertRaises(ValueError, vfsopen, p, 'rb', newline='') def test_read_bytes(self): p = self.root / 'fileA' self.assertEqual(p.read_bytes(), b'this is file A\n') def test_read_text(self): p = self.root / 'fileA' self.assertEqual(p.read_text(encoding='utf-8'), 'this is file A\n') q = self.root / 'abc' self.ground.create_file(q, b'\xe4bcdefg') self.assertEqual(q.read_text(encoding='latin-1'), 'äbcdefg') self.assertEqual(q.read_text(encoding='utf-8', errors='ignore'), 'bcdefg') @unittest.skipIf( not getattr(sys.flags, 'warn_default_encoding', 0), "Requires warn_default_encoding", ) def test_read_text_encoding_warning(self): p = self.root / 'fileA' with self.assertWarns(EncodingWarning) as wc: p.read_text() self.assertEqual(wc.filename, __file__) def test_read_text_with_newlines(self): p = self.root / 'abc' self.ground.create_file(p, b'abcde\r\nfghlk\n\rmnopq') # Check that `\n` character change nothing self.assertEqual(p.read_text(encoding='utf-8', newline='\n'), 'abcde\r\nfghlk\n\rmnopq') # Check that `\r` character replaces `\n` self.assertEqual(p.read_text(encoding='utf-8', newline='\r'), 'abcde\r\nfghlk\n\rmnopq') # Check that `\r\n` character replaces `\n` self.assertEqual(p.read_text(encoding='utf-8', newline='\r\n'), 'abcde\r\nfghlk\n\rmnopq') def test_iterdir(self): expected = ['dirA', 'dirB', 'dirC', 'fileA'] if self.ground.can_symlink: expected += ['linkA', 'linkB', 'brokenLink', 'brokenLinkLoop'] expected = {self.root.joinpath(name) for name in expected} actual = set(self.root.iterdir()) self.assertEqual(actual, expected) def test_iterdir_nodir(self): p = self.root / 'fileA' self.assertRaises(OSError, p.iterdir) def test_iterdir_info(self): for child in self.root.iterdir(): self.assertIsInstance(child.info, PathInfo) self.assertTrue(child.info.exists(follow_symlinks=False)) def test_glob(self): if not self.ground.can_symlink: self.skipTest("requires symlinks") p = self.root sep = self.root.parser.sep altsep = self.root.parser.altsep def check(pattern, expected): if altsep: expected = {name.replace(altsep, sep) for name in expected} expected = {p.joinpath(name) for name in expected} actual = set(p.glob(pattern, recurse_symlinks=True)) self.assertEqual(actual, expected) it = p.glob("fileA") self.assertIsInstance(it, collections.abc.Iterator) self.assertEqual(list(it), [p.joinpath("fileA")]) check("*A", ["dirA", "fileA", "linkA"]) check("*A", ['dirA', 'fileA', 'linkA']) check("*B/*", ["dirB/fileB", "linkB/fileB"]) check("*B/*", ['dirB/fileB', 'linkB/fileB']) check("brokenLink", ['brokenLink']) check("brokenLinkLoop", ['brokenLinkLoop']) check("**/", ["", "dirA/", "dirA/linkC/", "dirB/", "dirC/", "dirC/dirD/", "linkB/"]) check("**/*/", ["dirA/", "dirA/linkC/", "dirB/", "dirC/", "dirC/dirD/", "linkB/"]) check("*/", ["dirA/", "dirB/", "dirC/", "linkB/"]) check("*/dirD/**/", ["dirC/dirD/"]) check("*/dirD/**", ["dirC/dirD/", "dirC/dirD/fileD"]) check("dir*/**", ["dirA/", "dirA/linkC", "dirA/linkC/fileB", "dirB/", "dirB/fileB", "dirC/", "dirC/fileC", "dirC/dirD", "dirC/dirD/fileD", "dirC/novel.txt"]) check("dir*/**/", ["dirA/", "dirA/linkC/", "dirB/", "dirC/", "dirC/dirD/"]) check("dir*/**/..", ["dirA/..", "dirA/linkC/..", "dirB/..", "dirC/..", "dirC/dirD/.."]) check("dir*/*/**", ["dirA/linkC/", "dirA/linkC/fileB", "dirC/dirD/", "dirC/dirD/fileD"]) check("dir*/*/**/", ["dirA/linkC/", "dirC/dirD/"]) check("dir*/*/**/..", ["dirA/linkC/..", "dirC/dirD/.."]) check("dir*/*/..", ["dirC/dirD/..", "dirA/linkC/.."]) check("dir*/*/../dirD/**/", ["dirC/dirD/../dirD/"]) check("dir*/**/fileC", ["dirC/fileC"]) check("dir*/file*", ["dirB/fileB", "dirC/fileC"]) check("**/*/fileA", []) check("fileB", []) check("**/*/fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB"]) check("**/fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB"]) check("*/fileB", ["dirB/fileB", "linkB/fileB"]) check("*/fileB", ['dirB/fileB', 'linkB/fileB']) check("**/file*", ["fileA", "dirA/linkC/fileB", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD", "linkB/fileB"]) with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'): list(p.glob('')) def test_walk_top_down(self): it = self.root.walk() path, dirnames, filenames = next(it) dirnames.sort() filenames.sort() self.assertEqual(path, self.root) self.assertEqual(dirnames, ['dirA', 'dirB', 'dirC']) self.assertEqual(filenames, ['brokenLink', 'brokenLinkLoop', 'fileA', 'linkA', 'linkB'] if self.ground.can_symlink else ['fileA']) path, dirnames, filenames = next(it) self.assertEqual(path, self.root / 'dirA') self.assertEqual(dirnames, []) self.assertEqual(filenames, ['linkC'] if self.ground.can_symlink else []) path, dirnames, filenames = next(it) self.assertEqual(path, self.root / 'dirB') self.assertEqual(dirnames, []) self.assertEqual(filenames, ['fileB']) path, dirnames, filenames = next(it) filenames.sort() self.assertEqual(path, self.root / 'dirC') self.assertEqual(dirnames, ['dirD']) self.assertEqual(filenames, ['fileC', 'novel.txt']) path, dirnames, filenames = next(it) self.assertEqual(path, self.root / 'dirC' / 'dirD') self.assertEqual(dirnames, []) self.assertEqual(filenames, ['fileD']) self.assertRaises(StopIteration, next, it) def test_walk_prune(self): expected = {self.root, self.root / 'dirA', self.root / 'dirC', self.root / 'dirC' / 'dirD'} actual = set() for path, dirnames, filenames in self.root.walk(): actual.add(path) if path == self.root: dirnames.remove('dirB') self.assertEqual(actual, expected) def test_walk_bottom_up(self): seen_root = seen_dira = seen_dirb = seen_dirc = seen_dird = False for path, dirnames, filenames in self.root.walk(top_down=False): if path == self.root: self.assertFalse(seen_root) self.assertTrue(seen_dira) self.assertTrue(seen_dirb) self.assertTrue(seen_dirc) self.assertEqual(sorted(dirnames), ['dirA', 'dirB', 'dirC']) self.assertEqual(sorted(filenames), ['brokenLink', 'brokenLinkLoop', 'fileA', 'linkA', 'linkB'] if self.ground.can_symlink else ['fileA']) seen_root = True elif path == self.root / 'dirA': self.assertFalse(seen_root) self.assertFalse(seen_dira) self.assertEqual(dirnames, []) self.assertEqual(filenames, ['linkC'] if self.ground.can_symlink else []) seen_dira = True elif path == self.root / 'dirB': self.assertFalse(seen_root) self.assertFalse(seen_dirb) self.assertEqual(dirnames, []) self.assertEqual(filenames, ['fileB']) seen_dirb = True elif path == self.root / 'dirC': self.assertFalse(seen_root) self.assertFalse(seen_dirc) self.assertTrue(seen_dird) self.assertEqual(dirnames, ['dirD']) self.assertEqual(sorted(filenames), ['fileC', 'novel.txt']) seen_dirc = True elif path == self.root / 'dirC' / 'dirD': self.assertFalse(seen_root) self.assertFalse(seen_dirc) self.assertFalse(seen_dird) self.assertEqual(dirnames, []) self.assertEqual(filenames, ['fileD']) seen_dird = True else: raise AssertionError(f"Unexpected path: {path}") self.assertTrue(seen_root) def test_info_exists(self): p = self.root self.assertTrue(p.info.exists()) self.assertTrue((p / 'dirA').info.exists()) self.assertTrue((p / 'dirA').info.exists(follow_symlinks=False)) self.assertTrue((p / 'fileA').info.exists()) self.assertTrue((p / 'fileA').info.exists(follow_symlinks=False)) self.assertFalse((p / 'non-existing').info.exists()) self.assertFalse((p / 'non-existing').info.exists(follow_symlinks=False)) if self.ground.can_symlink: self.assertTrue((p / 'linkA').info.exists()) self.assertTrue((p / 'linkA').info.exists(follow_symlinks=False)) self.assertTrue((p / 'linkB').info.exists()) self.assertTrue((p / 'linkB').info.exists(follow_symlinks=True)) self.assertFalse((p / 'brokenLink').info.exists()) self.assertTrue((p / 'brokenLink').info.exists(follow_symlinks=False)) self.assertFalse((p / 'brokenLinkLoop').info.exists()) self.assertTrue((p / 'brokenLinkLoop').info.exists(follow_symlinks=False)) self.assertFalse((p / 'fileA\udfff').info.exists()) self.assertFalse((p / 'fileA\udfff').info.exists(follow_symlinks=False)) self.assertFalse((p / 'fileA\x00').info.exists()) self.assertFalse((p / 'fileA\x00').info.exists(follow_symlinks=False)) def test_info_is_dir(self): p = self.root self.assertTrue((p / 'dirA').info.is_dir()) self.assertTrue((p / 'dirA').info.is_dir(follow_symlinks=False)) self.assertFalse((p / 'fileA').info.is_dir()) self.assertFalse((p / 'fileA').info.is_dir(follow_symlinks=False)) self.assertFalse((p / 'non-existing').info.is_dir()) self.assertFalse((p / 'non-existing').info.is_dir(follow_symlinks=False)) if self.ground.can_symlink: self.assertFalse((p / 'linkA').info.is_dir()) self.assertFalse((p / 'linkA').info.is_dir(follow_symlinks=False)) self.assertTrue((p / 'linkB').info.is_dir()) self.assertFalse((p / 'linkB').info.is_dir(follow_symlinks=False)) self.assertFalse((p / 'brokenLink').info.is_dir()) self.assertFalse((p / 'brokenLink').info.is_dir(follow_symlinks=False)) self.assertFalse((p / 'brokenLinkLoop').info.is_dir()) self.assertFalse((p / 'brokenLinkLoop').info.is_dir(follow_symlinks=False)) self.assertFalse((p / 'dirA\udfff').info.is_dir()) self.assertFalse((p / 'dirA\udfff').info.is_dir(follow_symlinks=False)) self.assertFalse((p / 'dirA\x00').info.is_dir()) self.assertFalse((p / 'dirA\x00').info.is_dir(follow_symlinks=False)) def test_info_is_file(self): p = self.root self.assertTrue((p / 'fileA').info.is_file()) self.assertTrue((p / 'fileA').info.is_file(follow_symlinks=False)) self.assertFalse((p / 'dirA').info.is_file()) self.assertFalse((p / 'dirA').info.is_file(follow_symlinks=False)) self.assertFalse((p / 'non-existing').info.is_file()) self.assertFalse((p / 'non-existing').info.is_file(follow_symlinks=False)) if self.ground.can_symlink: self.assertTrue((p / 'linkA').info.is_file()) self.assertFalse((p / 'linkA').info.is_file(follow_symlinks=False)) self.assertFalse((p / 'linkB').info.is_file()) self.assertFalse((p / 'linkB').info.is_file(follow_symlinks=False)) self.assertFalse((p / 'brokenLink').info.is_file()) self.assertFalse((p / 'brokenLink').info.is_file(follow_symlinks=False)) self.assertFalse((p / 'brokenLinkLoop').info.is_file()) self.assertFalse((p / 'brokenLinkLoop').info.is_file(follow_symlinks=False)) self.assertFalse((p / 'fileA\udfff').info.is_file()) self.assertFalse((p / 'fileA\udfff').info.is_file(follow_symlinks=False)) self.assertFalse((p / 'fileA\x00').info.is_file()) self.assertFalse((p / 'fileA\x00').info.is_file(follow_symlinks=False)) def test_info_is_symlink(self): p = self.root self.assertFalse((p / 'fileA').info.is_symlink()) self.assertFalse((p / 'dirA').info.is_symlink()) self.assertFalse((p / 'non-existing').info.is_symlink()) if self.ground.can_symlink: self.assertTrue((p / 'linkA').info.is_symlink()) self.assertTrue((p / 'linkB').info.is_symlink()) self.assertTrue((p / 'brokenLink').info.is_symlink()) self.assertFalse((p / 'linkA\udfff').info.is_symlink()) self.assertFalse((p / 'linkA\x00').info.is_symlink()) self.assertTrue((p / 'brokenLinkLoop').info.is_symlink()) self.assertFalse((p / 'fileA\udfff').info.is_symlink()) self.assertFalse((p / 'fileA\x00').info.is_symlink()) class ZipPathReadTest(ReadTestBase, unittest.TestCase): ground = ZipPathGround(ReadableZipPath) class LocalPathReadTest(ReadTestBase, unittest.TestCase): ground = LocalPathGround(ReadableLocalPath) if not is_pypi: from pathlib import Path class PathReadTest(ReadTestBase, unittest.TestCase): ground = LocalPathGround(Path) if __name__ == "__main__": unittest.main() pathlib-abc-0.5.2/tests/test_write.py000066400000000000000000000122031507225105000175640ustar00rootroot00000000000000""" Tests for pathlib.types._WritablePath """ import io import os import sys import unittest from .support import is_pypi from .support.local_path import WritableLocalPath, LocalPathGround from .support.zip_path import WritableZipPath, ZipPathGround if is_pypi: from pathlib_abc import _WritablePath from pathlib_abc._os import vfsopen else: from pathlib.types import _WritablePath from pathlib._os import vfsopen class WriteTestBase: def setUp(self): self.root = self.ground.setup() def tearDown(self): self.ground.teardown(self.root) def test_is_writable(self): self.assertIsInstance(self.root, _WritablePath) def test_open_w(self): p = self.root / 'fileA' with vfsopen(p, 'w', encoding='utf-8') as f: self.assertIsInstance(f, io.TextIOBase) f.write('this is file A\n') self.assertEqual(self.ground.readtext(p), 'this is file A\n') def test_open_w_buffering_error(self): p = self.root / 'fileA' self.assertRaises(ValueError, vfsopen, p, 'w', buffering=0) self.assertRaises(ValueError, vfsopen, p, 'w', buffering=1) self.assertRaises(ValueError, vfsopen, p, 'w', buffering=1024) @unittest.skipIf( not getattr(sys.flags, 'warn_default_encoding', 0), "Requires warn_default_encoding", ) def test_open_w_encoding_warning(self): p = self.root / 'fileA' with self.assertWarns(EncodingWarning) as wc: with vfsopen(p, 'w'): pass self.assertEqual(wc.filename, __file__) def test_open_wb(self): p = self.root / 'fileA' with vfsopen(p, 'wb') as f: #self.assertIsInstance(f, io.BufferedWriter) f.write(b'this is file A\n') self.assertEqual(self.ground.readbytes(p), b'this is file A\n') self.assertRaises(ValueError, vfsopen, p, 'wb', encoding='utf8') self.assertRaises(ValueError, vfsopen, p, 'wb', errors='strict') self.assertRaises(ValueError, vfsopen, p, 'wb', newline='') def test_write_bytes(self): p = self.root / 'fileA' p.write_bytes(b'abcdefg') self.assertEqual(self.ground.readbytes(p), b'abcdefg') # Check that trying to write str does not truncate the file. self.assertRaises(TypeError, p.write_bytes, 'somestr') self.assertEqual(self.ground.readbytes(p), b'abcdefg') def test_write_text(self): p = self.root / 'fileA' p.write_text('äbcdefg', encoding='latin-1') self.assertEqual(self.ground.readbytes(p), b'\xe4bcdefg') # Check that trying to write bytes does not truncate the file. self.assertRaises(TypeError, p.write_text, b'somebytes', encoding='utf-8') self.assertEqual(self.ground.readbytes(p), b'\xe4bcdefg') @unittest.skipIf( not getattr(sys.flags, 'warn_default_encoding', 0), "Requires warn_default_encoding", ) def test_write_text_encoding_warning(self): p = self.root / 'fileA' with self.assertWarns(EncodingWarning) as wc: p.write_text('abcdefg') self.assertEqual(wc.filename, __file__) def test_write_text_with_newlines(self): # Check that `\n` character change nothing p = self.root / 'fileA' p.write_text('abcde\r\nfghlk\n\rmnopq', encoding='utf-8', newline='\n') self.assertEqual(self.ground.readbytes(p), b'abcde\r\nfghlk\n\rmnopq') # Check that `\r` character replaces `\n` p = self.root / 'fileB' p.write_text('abcde\r\nfghlk\n\rmnopq', encoding='utf-8', newline='\r') self.assertEqual(self.ground.readbytes(p), b'abcde\r\rfghlk\r\rmnopq') # Check that `\r\n` character replaces `\n` p = self.root / 'fileC' p.write_text('abcde\r\nfghlk\n\rmnopq', encoding='utf-8', newline='\r\n') self.assertEqual(self.ground.readbytes(p), b'abcde\r\r\nfghlk\r\n\rmnopq') # Check that no argument passed will change `\n` to `os.linesep` os_linesep_byte = bytes(os.linesep, encoding='ascii') p = self.root / 'fileD' p.write_text('abcde\nfghlk\n\rmnopq', encoding='utf-8') self.assertEqual(self.ground.readbytes(p), b'abcde' + os_linesep_byte + b'fghlk' + os_linesep_byte + b'\rmnopq') def test_mkdir(self): p = self.root / 'newdirA' self.assertFalse(self.ground.isdir(p)) p.mkdir() self.assertTrue(self.ground.isdir(p)) def test_symlink_to(self): if not self.ground.can_symlink: self.skipTest('needs symlinks') link = self.root.joinpath('linkA') link.symlink_to('fileA') self.assertTrue(self.ground.islink(link)) self.assertEqual(self.ground.readlink(link), 'fileA') class ZipPathWriteTest(WriteTestBase, unittest.TestCase): ground = ZipPathGround(WritableZipPath) class LocalPathWriteTest(WriteTestBase, unittest.TestCase): ground = LocalPathGround(WritableLocalPath) if not is_pypi: from pathlib import Path class PathWriteTest(WriteTestBase, unittest.TestCase): ground = LocalPathGround(Path) if __name__ == "__main__": unittest.main() pathlib-abc-0.5.2/tox.ini000066400000000000000000000005331507225105000151750ustar00rootroot00000000000000[tox] isolated_build = True envlist = py{37,38,39,310,311,312,313},sphinx [testenv] pass_env = FORCE_COLOR NO_COLOR set_env = PYTHONWARNDEFAULTENCODING = 1 deps = pytest commands = pytest tests [testenv:sphinx] usedevelop = True deps = -rdocs/requirements.txt commands = sphinx-build -n -W --keep-going -b html docs docs/_build/html