aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/path
diff options
context:
space:
mode:
authorvitalyisaev <vitalyisaev@ydb.tech>2023-11-30 13:26:22 +0300
committervitalyisaev <vitalyisaev@ydb.tech>2023-11-30 15:44:45 +0300
commit0a98fece5a9b54f16afeb3a94b3eb3105e9c3962 (patch)
tree291d72dbd7e9865399f668c84d11ed86fb190bbf /contrib/python/path
parentcb2c8d75065e5b3c47094067cb4aa407d4813298 (diff)
downloadydb-0a98fece5a9b54f16afeb3a94b3eb3105e9c3962.tar.gz
YQ Connector:Use docker-compose in integrational tests
Diffstat (limited to 'contrib/python/path')
-rw-r--r--contrib/python/path/.dist-info/METADATA201
-rw-r--r--contrib/python/path/.dist-info/top_level.txt1
-rw-r--r--contrib/python/path/LICENSE17
-rw-r--r--contrib/python/path/README.rst163
-rw-r--r--contrib/python/path/path/__init__.py1665
-rw-r--r--contrib/python/path/path/classes.py27
-rw-r--r--contrib/python/path/path/masks.py159
-rw-r--r--contrib/python/path/path/matchers.py59
-rw-r--r--contrib/python/path/path/py.typed0
-rw-r--r--contrib/python/path/ya.make30
10 files changed, 2322 insertions, 0 deletions
diff --git a/contrib/python/path/.dist-info/METADATA b/contrib/python/path/.dist-info/METADATA
new file mode 100644
index 0000000000..7cddb38723
--- /dev/null
+++ b/contrib/python/path/.dist-info/METADATA
@@ -0,0 +1,201 @@
+Metadata-Version: 2.1
+Name: path
+Version: 16.7.1
+Summary: A module wrapper for os.path
+Home-page: https://github.com/jaraco/path
+Author: Jason Orendorff
+Author-email: jason.orendorff@gmail.com
+Maintainer: Jason R. Coombs
+Maintainer-email: jaraco@jaraco.com
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Requires-Python: >=3.8
+License-File: LICENSE
+Provides-Extra: docs
+Requires-Dist: sphinx (>=3.5) ; extra == 'docs'
+Requires-Dist: jaraco.packaging (>=9.3) ; extra == 'docs'
+Requires-Dist: rst.linker (>=1.9) ; extra == 'docs'
+Requires-Dist: furo ; extra == 'docs'
+Requires-Dist: sphinx-lint ; extra == 'docs'
+Requires-Dist: jaraco.tidelift (>=1.4) ; extra == 'docs'
+Provides-Extra: testing
+Requires-Dist: pytest (>=6) ; extra == 'testing'
+Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing'
+Requires-Dist: pytest-cov ; extra == 'testing'
+Requires-Dist: pytest-enabler (>=2.2) ; extra == 'testing'
+Requires-Dist: pytest-ruff ; extra == 'testing'
+Requires-Dist: appdirs ; extra == 'testing'
+Requires-Dist: packaging ; extra == 'testing'
+Requires-Dist: pygments ; extra == 'testing'
+Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing'
+Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing'
+Requires-Dist: pywin32 ; (platform_system == "Windows" and python_version < "3.12") and extra == 'testing'
+
+.. image:: https://img.shields.io/pypi/v/path.svg
+ :target: https://pypi.org/project/path
+
+.. image:: https://img.shields.io/pypi/pyversions/path.svg
+
+.. image:: https://github.com/jaraco/path/workflows/tests/badge.svg
+ :target: https://github.com/jaraco/path/actions?query=workflow%3A%22tests%22
+ :alt: tests
+
+.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
+ :target: https://github.com/astral-sh/ruff
+ :alt: Ruff
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/psf/black
+ :alt: Code style: Black
+
+.. image:: https://readthedocs.org/projects/path/badge/?version=latest
+ :target: https://path.readthedocs.io/en/latest/?badge=latest
+
+.. image:: https://img.shields.io/badge/skeleton-2023-informational
+ :target: https://blog.jaraco.com/skeleton
+
+.. image:: https://tidelift.com/badges/package/pypi/path
+ :target: https://tidelift.com/subscription/pkg/pypi-path?utm_source=pypi-path&utm_medium=readme
+
+
+``path`` (aka path pie, formerly ``path.py``) implements path
+objects as first-class entities, allowing common operations on
+files to be invoked on those path objects directly. For example:
+
+.. code-block:: python
+
+ from path import Path
+
+ d = Path("/home/guido/bin")
+ for f in d.files("*.py"):
+ f.chmod(0o755)
+
+ # Globbing
+ for f in d.files("*.py"):
+ f.chmod("u+rwx")
+
+ # Changing the working directory:
+ with Path("somewhere"):
+ # cwd in now `somewhere`
+ ...
+
+ # Concatenate paths with /
+ foo_txt = Path("bar") / "foo.txt"
+
+Path pie is `hosted at Github <https://github.com/jaraco/path>`_.
+
+Find `the documentation here <https://path.readthedocs.io>`_.
+
+Guides and Testimonials
+=======================
+
+Yasoob wrote the Python 101 `Writing a Cleanup Script
+<http://freepythontips.wordpress.com/2014/01/23/python-101-writing-a-cleanup-script/>`_
+based on ``path``.
+
+Advantages
+==========
+
+Python 3.4 introduced
+`pathlib <https://docs.python.org/3/library/pathlib.html>`_,
+which shares many characteristics with ``path``. In particular,
+it provides an object encapsulation for representing filesystem paths.
+One may have imagined ``pathlib`` would supersede ``path``.
+
+But the implementation and the usage quickly diverge, and ``path``
+has several advantages over ``pathlib``:
+
+- ``path`` implements ``Path`` objects as a subclass of
+ ``str``, and as a result these ``Path``
+ objects may be passed directly to other APIs that expect simple
+ text representations of paths, whereas with ``pathlib``, one
+ must first cast values to strings before passing them to
+ APIs unaware of ``pathlib``. This shortcoming was `addressed
+ by PEP 519 <https://www.python.org/dev/peps/pep-0519/>`_,
+ in Python 3.6.
+- ``path`` goes beyond exposing basic functionality of a path
+ and exposes commonly-used behaviors on a path, providing
+ methods like ``rmtree`` (from shlib) and ``remove_p`` (remove
+ a file if it exists).
+- As a PyPI-hosted package, ``path`` is free to iterate
+ faster than a stdlib package. Contributions are welcome
+ and encouraged.
+- ``path`` provides a uniform abstraction over its Path object,
+ freeing the implementer to subclass it readily. One cannot
+ subclass a ``pathlib.Path`` to add functionality, but must
+ subclass ``Path``, ``PosixPath``, and ``WindowsPath``, even
+ if one only wishes to add a ``__dict__`` to the subclass
+ instances. ``path`` instead allows the ``Path.module``
+ object to be overridden by subclasses, defaulting to the
+ ``os.path``. Even advanced uses of ``path.Path`` that
+ subclass the model do not need to be concerned with
+ OS-specific nuances.
+
+This path project has the explicit aim to provide compatibility
+with ``pathlib`` objects where possible, such that a ``path.Path``
+object is a drop-in replacement for ``pathlib.Path*`` objects.
+This project welcomes contributions to improve that compatibility
+where it's lacking.
+
+Alternatives
+============
+
+In addition to
+`pathlib <https://docs.python.org/3/library/pathlib.html>`_, the
+`pylib project <https://pypi.org/project/py/>`_ implements a
+`LocalPath <https://github.com/pytest-dev/py/blob/72601dc8bbb5e11298bf9775bb23b0a395deb09b/py/_path/local.py#L106>`_
+class, which shares some behaviors and interfaces with ``path``.
+
+Development
+===========
+
+To install a development version, use the Github links to clone or
+download a snapshot of the latest code. Alternatively, if you have git
+installed, you may be able to use ``pip`` to install directly from
+the repository::
+
+ pip install git+https://github.com/jaraco/path.git
+
+Testing
+=======
+
+Tests are invoked with `tox <https://pypi.org/project/tox>`_. After
+having installed tox, simply invoke ``tox`` in a checkout of the repo
+to invoke the tests.
+
+Tests are also run in continuous integration. See the badges above
+for links to the CI runs.
+
+Releasing
+=========
+
+Tagged releases are automatically published to PyPI by Azure
+Pipelines, assuming the tests pass.
+
+Origins
+=======
+
+The ``path.py`` project was initially released in 2003 by Jason Orendorff
+and has been continuously developed and supported by several maintainers
+over the years.
+
+For Enterprise
+==============
+
+Available as part of the Tidelift Subscription.
+
+This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
+
+`Learn more <https://tidelift.com/subscription/pkg/pypi-path?utm_source=pypi-path&utm_medium=referral&utm_campaign=github>`_.
+
+Security Contact
+================
+
+To report a security vulnerability, please use the
+`Tidelift security contact <https://tidelift.com/security>`_.
+Tidelift will coordinate the fix and disclosure.
diff --git a/contrib/python/path/.dist-info/top_level.txt b/contrib/python/path/.dist-info/top_level.txt
new file mode 100644
index 0000000000..e7a8fd4d0a
--- /dev/null
+++ b/contrib/python/path/.dist-info/top_level.txt
@@ -0,0 +1 @@
+path
diff --git a/contrib/python/path/LICENSE b/contrib/python/path/LICENSE
new file mode 100644
index 0000000000..1bb5a44356
--- /dev/null
+++ b/contrib/python/path/LICENSE
@@ -0,0 +1,17 @@
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/contrib/python/path/README.rst b/contrib/python/path/README.rst
new file mode 100644
index 0000000000..69aa8737d6
--- /dev/null
+++ b/contrib/python/path/README.rst
@@ -0,0 +1,163 @@
+.. image:: https://img.shields.io/pypi/v/path.svg
+ :target: https://pypi.org/project/path
+
+.. image:: https://img.shields.io/pypi/pyversions/path.svg
+
+.. image:: https://github.com/jaraco/path/workflows/tests/badge.svg
+ :target: https://github.com/jaraco/path/actions?query=workflow%3A%22tests%22
+ :alt: tests
+
+.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
+ :target: https://github.com/astral-sh/ruff
+ :alt: Ruff
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/psf/black
+ :alt: Code style: Black
+
+.. image:: https://readthedocs.org/projects/path/badge/?version=latest
+ :target: https://path.readthedocs.io/en/latest/?badge=latest
+
+.. image:: https://img.shields.io/badge/skeleton-2023-informational
+ :target: https://blog.jaraco.com/skeleton
+
+.. image:: https://tidelift.com/badges/package/pypi/path
+ :target: https://tidelift.com/subscription/pkg/pypi-path?utm_source=pypi-path&utm_medium=readme
+
+
+``path`` (aka path pie, formerly ``path.py``) implements path
+objects as first-class entities, allowing common operations on
+files to be invoked on those path objects directly. For example:
+
+.. code-block:: python
+
+ from path import Path
+
+ d = Path("/home/guido/bin")
+ for f in d.files("*.py"):
+ f.chmod(0o755)
+
+ # Globbing
+ for f in d.files("*.py"):
+ f.chmod("u+rwx")
+
+ # Changing the working directory:
+ with Path("somewhere"):
+ # cwd in now `somewhere`
+ ...
+
+ # Concatenate paths with /
+ foo_txt = Path("bar") / "foo.txt"
+
+Path pie is `hosted at Github <https://github.com/jaraco/path>`_.
+
+Find `the documentation here <https://path.readthedocs.io>`_.
+
+Guides and Testimonials
+=======================
+
+Yasoob wrote the Python 101 `Writing a Cleanup Script
+<http://freepythontips.wordpress.com/2014/01/23/python-101-writing-a-cleanup-script/>`_
+based on ``path``.
+
+Advantages
+==========
+
+Python 3.4 introduced
+`pathlib <https://docs.python.org/3/library/pathlib.html>`_,
+which shares many characteristics with ``path``. In particular,
+it provides an object encapsulation for representing filesystem paths.
+One may have imagined ``pathlib`` would supersede ``path``.
+
+But the implementation and the usage quickly diverge, and ``path``
+has several advantages over ``pathlib``:
+
+- ``path`` implements ``Path`` objects as a subclass of
+ ``str``, and as a result these ``Path``
+ objects may be passed directly to other APIs that expect simple
+ text representations of paths, whereas with ``pathlib``, one
+ must first cast values to strings before passing them to
+ APIs unaware of ``pathlib``. This shortcoming was `addressed
+ by PEP 519 <https://www.python.org/dev/peps/pep-0519/>`_,
+ in Python 3.6.
+- ``path`` goes beyond exposing basic functionality of a path
+ and exposes commonly-used behaviors on a path, providing
+ methods like ``rmtree`` (from shlib) and ``remove_p`` (remove
+ a file if it exists).
+- As a PyPI-hosted package, ``path`` is free to iterate
+ faster than a stdlib package. Contributions are welcome
+ and encouraged.
+- ``path`` provides a uniform abstraction over its Path object,
+ freeing the implementer to subclass it readily. One cannot
+ subclass a ``pathlib.Path`` to add functionality, but must
+ subclass ``Path``, ``PosixPath``, and ``WindowsPath``, even
+ if one only wishes to add a ``__dict__`` to the subclass
+ instances. ``path`` instead allows the ``Path.module``
+ object to be overridden by subclasses, defaulting to the
+ ``os.path``. Even advanced uses of ``path.Path`` that
+ subclass the model do not need to be concerned with
+ OS-specific nuances.
+
+This path project has the explicit aim to provide compatibility
+with ``pathlib`` objects where possible, such that a ``path.Path``
+object is a drop-in replacement for ``pathlib.Path*`` objects.
+This project welcomes contributions to improve that compatibility
+where it's lacking.
+
+Alternatives
+============
+
+In addition to
+`pathlib <https://docs.python.org/3/library/pathlib.html>`_, the
+`pylib project <https://pypi.org/project/py/>`_ implements a
+`LocalPath <https://github.com/pytest-dev/py/blob/72601dc8bbb5e11298bf9775bb23b0a395deb09b/py/_path/local.py#L106>`_
+class, which shares some behaviors and interfaces with ``path``.
+
+Development
+===========
+
+To install a development version, use the Github links to clone or
+download a snapshot of the latest code. Alternatively, if you have git
+installed, you may be able to use ``pip`` to install directly from
+the repository::
+
+ pip install git+https://github.com/jaraco/path.git
+
+Testing
+=======
+
+Tests are invoked with `tox <https://pypi.org/project/tox>`_. After
+having installed tox, simply invoke ``tox`` in a checkout of the repo
+to invoke the tests.
+
+Tests are also run in continuous integration. See the badges above
+for links to the CI runs.
+
+Releasing
+=========
+
+Tagged releases are automatically published to PyPI by Azure
+Pipelines, assuming the tests pass.
+
+Origins
+=======
+
+The ``path.py`` project was initially released in 2003 by Jason Orendorff
+and has been continuously developed and supported by several maintainers
+over the years.
+
+For Enterprise
+==============
+
+Available as part of the Tidelift Subscription.
+
+This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.
+
+`Learn more <https://tidelift.com/subscription/pkg/pypi-path?utm_source=pypi-path&utm_medium=referral&utm_campaign=github>`_.
+
+Security Contact
+================
+
+To report a security vulnerability, please use the
+`Tidelift security contact <https://tidelift.com/security>`_.
+Tidelift will coordinate the fix and disclosure.
diff --git a/contrib/python/path/path/__init__.py b/contrib/python/path/path/__init__.py
new file mode 100644
index 0000000000..eebdc3a0b8
--- /dev/null
+++ b/contrib/python/path/path/__init__.py
@@ -0,0 +1,1665 @@
+"""
+Path Pie
+
+Implements ``path.Path`` - An object representing a
+path to a file or directory.
+
+Example::
+
+ from path import Path
+ d = Path('/home/guido/bin')
+
+ # Globbing
+ for f in d.files('*.py'):
+ f.chmod(0o755)
+
+ # Changing the working directory:
+ with Path("somewhere"):
+ # cwd in now `somewhere`
+ ...
+
+ # Concatenate paths with /
+ foo_txt = Path("bar") / "foo.txt"
+"""
+
+import sys
+import warnings
+import os
+import fnmatch
+import glob
+import shutil
+import hashlib
+import errno
+import tempfile
+import functools
+import re
+import contextlib
+import importlib
+import itertools
+import datetime
+from numbers import Number
+from typing import Union
+
+with contextlib.suppress(ImportError):
+ import win32security
+
+with contextlib.suppress(ImportError):
+ import pwd
+
+with contextlib.suppress(ImportError):
+ import grp
+
+from . import matchers
+from . import masks
+from . import classes
+
+
+__all__ = ['Path', 'TempDir']
+
+
+LINESEPS = ['\r\n', '\r', '\n']
+U_LINESEPS = LINESEPS + ['\u0085', '\u2028', '\u2029']
+B_NEWLINE = re.compile('|'.join(LINESEPS).encode())
+U_NEWLINE = re.compile('|'.join(U_LINESEPS))
+B_NL_END = re.compile(B_NEWLINE.pattern + b'$')
+U_NL_END = re.compile(U_NEWLINE.pattern + '$')
+
+_default_linesep = object()
+
+
+def _make_timestamp_ns(value: Union[Number, datetime.datetime]) -> Number:
+ timestamp_s = value if isinstance(value, Number) else value.timestamp()
+ return int(timestamp_s * 10**9)
+
+
+class TreeWalkWarning(Warning):
+ pass
+
+
+class Traversal:
+ """
+ Wrap a walk result to customize the traversal.
+
+ `follow` is a function that takes an item and returns
+ True if that item should be followed and False otherwise.
+
+ For example, to avoid traversing into directories that
+ begin with `.`:
+
+ >>> traverse = Traversal(lambda dir: not dir.startswith('.'))
+ >>> items = list(traverse(Path('.').walk()))
+
+ Directories beginning with `.` will appear in the results, but
+ their children will not.
+
+ >>> dot_dir = next(item for item in items if item.isdir() and item.startswith('.'))
+ >>> any(item.parent == dot_dir for item in items)
+ False
+ """
+
+ def __init__(self, follow):
+ self.follow = follow
+
+ def __call__(self, walker):
+ traverse = None
+ while True:
+ try:
+ item = walker.send(traverse)
+ except StopIteration:
+ return
+ yield item
+
+ traverse = functools.partial(self.follow, item)
+
+
+def _strip_newlines(lines):
+ r"""
+ >>> list(_strip_newlines(['Hello World\r\n', 'foo']))
+ ['Hello World', 'foo']
+ """
+ return (U_NL_END.sub('', line) for line in lines)
+
+
+class Path(str):
+ """
+ Represents a filesystem path.
+
+ For documentation on individual methods, consult their
+ counterparts in :mod:`os.path`.
+
+ Some methods are additionally included from :mod:`shutil`.
+ The functions are linked directly into the class namespace
+ such that they will be bound to the Path instance. For example,
+ ``Path(src).copy(target)`` is equivalent to
+ ``shutil.copy(src, target)``. Therefore, when referencing
+ the docs for these methods, assume `src` references `self`,
+ the Path instance.
+ """
+
+ module = os.path
+ """ The path module to use for path operations.
+
+ .. seealso:: :mod:`os.path`
+ """
+
+ def __init__(self, other=''):
+ if other is None:
+ raise TypeError("Invalid initial value for path: None")
+ with contextlib.suppress(AttributeError):
+ self._validate()
+
+ @classmethod
+ @functools.lru_cache
+ def using_module(cls, module):
+ subclass_name = cls.__name__ + '_' + module.__name__
+ bases = (cls,)
+ ns = {'module': module}
+ return type(subclass_name, bases, ns)
+
+ @classes.ClassProperty
+ @classmethod
+ def _next_class(cls):
+ """
+ What class should be used to construct new instances from this class
+ """
+ return cls
+
+ # --- Special Python methods.
+
+ def __repr__(self):
+ return '{}({})'.format(type(self).__name__, super().__repr__())
+
+ # Adding a Path and a string yields a Path.
+ def __add__(self, more):
+ return self._next_class(super().__add__(more))
+
+ def __radd__(self, other):
+ return self._next_class(other.__add__(self))
+
+ # The / operator joins Paths.
+ def __div__(self, rel):
+ """fp.__div__(rel) == fp / rel == fp.joinpath(rel)
+
+ Join two path components, adding a separator character if
+ needed.
+
+ .. seealso:: :func:`os.path.join`
+ """
+ return self._next_class(self.module.join(self, rel))
+
+ # Make the / operator work even when true division is enabled.
+ __truediv__ = __div__
+
+ # The / operator joins Paths the other way around
+ def __rdiv__(self, rel):
+ """fp.__rdiv__(rel) == rel / fp
+
+ Join two path components, adding a separator character if
+ needed.
+
+ .. seealso:: :func:`os.path.join`
+ """
+ return self._next_class(self.module.join(rel, self))
+
+ # Make the / operator work even when true division is enabled.
+ __rtruediv__ = __rdiv__
+
+ def __enter__(self):
+ self._old_dir = self.getcwd()
+ os.chdir(self)
+ return self
+
+ def __exit__(self, *_):
+ os.chdir(self._old_dir)
+
+ @classmethod
+ def getcwd(cls):
+ """Return the current working directory as a path object.
+
+ .. seealso:: :func:`os.getcwd`
+ """
+ return cls(os.getcwd())
+
+ #
+ # --- Operations on Path strings.
+
+ def abspath(self):
+ """.. seealso:: :func:`os.path.abspath`"""
+ return self._next_class(self.module.abspath(self))
+
+ def normcase(self):
+ """.. seealso:: :func:`os.path.normcase`"""
+ return self._next_class(self.module.normcase(self))
+
+ def normpath(self):
+ """.. seealso:: :func:`os.path.normpath`"""
+ return self._next_class(self.module.normpath(self))
+
+ def realpath(self):
+ """.. seealso:: :func:`os.path.realpath`"""
+ return self._next_class(self.module.realpath(self))
+
+ def expanduser(self):
+ """.. seealso:: :func:`os.path.expanduser`"""
+ return self._next_class(self.module.expanduser(self))
+
+ def expandvars(self):
+ """.. seealso:: :func:`os.path.expandvars`"""
+ return self._next_class(self.module.expandvars(self))
+
+ def dirname(self):
+ """.. seealso:: :attr:`parent`, :func:`os.path.dirname`"""
+ return self._next_class(self.module.dirname(self))
+
+ def basename(self):
+ """.. seealso:: :attr:`name`, :func:`os.path.basename`"""
+ return self._next_class(self.module.basename(self))
+
+ def expand(self):
+ """Clean up a filename by calling :meth:`expandvars()`,
+ :meth:`expanduser()`, and :meth:`normpath()` on it.
+
+ This is commonly everything needed to clean up a filename
+ read from a configuration file, for example.
+ """
+ return self.expandvars().expanduser().normpath()
+
+ @property
+ def stem(self):
+ """The same as :meth:`name`, but with one file extension stripped off.
+
+ >>> Path('/home/guido/python.tar.gz').stem
+ 'python.tar'
+ """
+ base, ext = self.module.splitext(self.name)
+ return base
+
+ @property
+ def ext(self):
+ """The file extension, for example ``'.py'``."""
+ f, ext = self.module.splitext(self)
+ return ext
+
+ def with_suffix(self, suffix):
+ """Return a new path with the file suffix changed (or added, if none)
+
+ >>> Path('/home/guido/python.tar.gz').with_suffix(".foo")
+ Path('/home/guido/python.tar.foo')
+
+ >>> Path('python').with_suffix('.zip')
+ Path('python.zip')
+
+ >>> Path('filename.ext').with_suffix('zip')
+ Traceback (most recent call last):
+ ...
+ ValueError: Invalid suffix 'zip'
+ """
+ if not suffix.startswith('.'):
+ raise ValueError(f"Invalid suffix {suffix!r}")
+
+ return self.stripext() + suffix
+
+ @property
+ def drive(self):
+ """The drive specifier, for example ``'C:'``.
+
+ This is always empty on systems that don't use drive specifiers.
+ """
+ drive, r = self.module.splitdrive(self)
+ return self._next_class(drive)
+
+ parent = property(
+ dirname,
+ None,
+ None,
+ """ This path's parent directory, as a new Path object.
+
+ For example,
+ ``Path('/usr/local/lib/libpython.so').parent ==
+ Path('/usr/local/lib')``
+
+ .. seealso:: :meth:`dirname`, :func:`os.path.dirname`
+ """,
+ )
+
+ name = property(
+ basename,
+ None,
+ None,
+ """ The name of this file or directory without the full path.
+
+ For example,
+ ``Path('/usr/local/lib/libpython.so').name == 'libpython.so'``
+
+ .. seealso:: :meth:`basename`, :func:`os.path.basename`
+ """,
+ )
+
+ def splitpath(self):
+ """Return two-tuple of ``.parent``, ``.name``.
+
+ .. seealso:: :attr:`parent`, :attr:`name`, :func:`os.path.split`
+ """
+ parent, child = self.module.split(self)
+ return self._next_class(parent), child
+
+ def splitdrive(self):
+ """Return two-tuple of ``.drive`` and rest without drive.
+
+ Split the drive specifier from this path. If there is
+ no drive specifier, :samp:`{p.drive}` is empty, so the return value
+ is simply ``(Path(''), p)``. This is always the case on Unix.
+
+ .. seealso:: :func:`os.path.splitdrive`
+ """
+ drive, rel = self.module.splitdrive(self)
+ return self._next_class(drive), self._next_class(rel)
+
+ def splitext(self):
+ """Return two-tuple of ``.stripext()`` and ``.ext``.
+
+ Split the filename extension from this path and return
+ the two parts. Either part may be empty.
+
+ The extension is everything from ``'.'`` to the end of the
+ last path segment. This has the property that if
+ ``(a, b) == p.splitext()``, then ``a + b == p``.
+
+ .. seealso:: :func:`os.path.splitext`
+ """
+ filename, ext = self.module.splitext(self)
+ return self._next_class(filename), ext
+
+ def stripext(self):
+ """Remove one file extension from the path.
+
+ For example, ``Path('/home/guido/python.tar.gz').stripext()``
+ returns ``Path('/home/guido/python.tar')``.
+ """
+ return self.splitext()[0]
+
+ @classes.multimethod
+ def joinpath(cls, first, *others):
+ """
+ Join first to zero or more :class:`Path` components,
+ adding a separator character (:samp:`{first}.module.sep`)
+ if needed. Returns a new instance of
+ :samp:`{first}._next_class`.
+
+ .. seealso:: :func:`os.path.join`
+ """
+ return cls._next_class(cls.module.join(first, *others))
+
+ def splitall(self):
+ r"""Return a list of the path components in this path.
+
+ The first item in the list will be a Path. Its value will be
+ either :data:`os.curdir`, :data:`os.pardir`, empty, or the root
+ directory of this path (for example, ``'/'`` or ``'C:\\'``). The
+ other items in the list will be strings.
+
+ ``Path.joinpath(*result)`` will yield the original path.
+
+ >>> Path('/foo/bar/baz').splitall()
+ [Path('/'), 'foo', 'bar', 'baz']
+ """
+ return list(self._parts())
+
+ def parts(self):
+ """
+ >>> Path('/foo/bar/baz').parts()
+ (Path('/'), 'foo', 'bar', 'baz')
+ """
+ return tuple(self._parts())
+
+ def _parts(self):
+ return reversed(tuple(self._parts_iter()))
+
+ def _parts_iter(self):
+ loc = self
+ while loc != os.curdir and loc != os.pardir:
+ prev = loc
+ loc, child = prev.splitpath()
+ if loc == prev:
+ break
+ yield child
+ yield loc
+
+ def relpath(self, start='.'):
+ """Return this path as a relative path,
+ based from `start`, which defaults to the current working directory.
+ """
+ cwd = self._next_class(start)
+ return cwd.relpathto(self)
+
+ def relpathto(self, dest):
+ """Return a relative path from `self` to `dest`.
+
+ If there is no relative path from `self` to `dest`, for example if
+ they reside on different drives in Windows, then this returns
+ ``dest.abspath()``.
+ """
+ origin = self.abspath()
+ dest = self._next_class(dest).abspath()
+
+ orig_list = origin.normcase().splitall()
+ # Don't normcase dest! We want to preserve the case.
+ dest_list = dest.splitall()
+
+ if orig_list[0] != self.module.normcase(dest_list[0]):
+ # Can't get here from there.
+ return dest
+
+ # Find the location where the two paths start to differ.
+ i = 0
+ for start_seg, dest_seg in zip(orig_list, dest_list):
+ if start_seg != self.module.normcase(dest_seg):
+ break
+ i += 1
+
+ # Now i is the point where the two paths diverge.
+ # Need a certain number of "os.pardir"s to work up
+ # from the origin to the point of divergence.
+ segments = [os.pardir] * (len(orig_list) - i)
+ # Need to add the diverging part of dest_list.
+ segments += dest_list[i:]
+ if len(segments) == 0:
+ # If they happen to be identical, use os.curdir.
+ relpath = os.curdir
+ else:
+ relpath = self.module.join(*segments)
+ return self._next_class(relpath)
+
+ # --- Listing, searching, walking, and matching
+
+ def listdir(self, match=None):
+ """List of items in this directory.
+
+ Use :meth:`files` or :meth:`dirs` instead if you want a listing
+ of just files or just subdirectories.
+
+ The elements of the list are Path objects.
+
+ With the optional `match` argument, a callable,
+ only return items whose names match the given pattern.
+
+ .. seealso:: :meth:`files`, :meth:`dirs`
+ """
+ match = matchers.load(match)
+ return list(filter(match, (self / child for child in os.listdir(self))))
+
+ def dirs(self, *args, **kwargs):
+ """List of this directory's subdirectories.
+
+ The elements of the list are Path objects.
+ This does not walk recursively into subdirectories
+ (but see :meth:`walkdirs`).
+
+ Accepts parameters to :meth:`listdir`.
+ """
+ return [p for p in self.listdir(*args, **kwargs) if p.isdir()]
+
+ def files(self, *args, **kwargs):
+ """List of the files in self.
+
+ The elements of the list are Path objects.
+ This does not walk into subdirectories (see :meth:`walkfiles`).
+
+ Accepts parameters to :meth:`listdir`.
+ """
+
+ return [p for p in self.listdir(*args, **kwargs) if p.isfile()]
+
+ def walk(self, match=None, errors='strict'):
+ """Iterator over files and subdirs, recursively.
+
+ The iterator yields Path objects naming each child item of
+ this directory and its descendants. This requires that
+ ``D.isdir()``.
+
+ This performs a depth-first traversal of the directory tree.
+ Each directory is returned just before all its children.
+
+ The `errors=` keyword argument controls behavior when an
+ error occurs. The default is ``'strict'``, which causes an
+ exception. Other allowed values are ``'warn'`` (which
+ reports the error via :func:`warnings.warn()`), and ``'ignore'``.
+ `errors` may also be an arbitrary callable taking a msg parameter.
+ """
+
+ errors = Handlers._resolve(errors)
+ match = matchers.load(match)
+
+ try:
+ childList = self.listdir()
+ except Exception as exc:
+ errors(f"Unable to list directory '{self}': {exc}")
+ return
+
+ for child in childList:
+ traverse = None
+ if match(child):
+ traverse = yield child
+ traverse = traverse or child.isdir
+ try:
+ do_traverse = traverse()
+ except Exception as exc:
+ errors(f"Unable to access '{child}': {exc}")
+ continue
+
+ if do_traverse:
+ yield from child.walk(errors=errors, match=match)
+
+ def walkdirs(self, *args, **kwargs):
+ """Iterator over subdirs, recursively."""
+ return (item for item in self.walk(*args, **kwargs) if item.isdir())
+
+ def walkfiles(self, *args, **kwargs):
+ """Iterator over files, recursively."""
+ return (item for item in self.walk(*args, **kwargs) if item.isfile())
+
+ def fnmatch(self, pattern, normcase=None):
+ """Return ``True`` if `self.name` matches the given `pattern`.
+
+ `pattern` - A filename pattern with wildcards,
+ for example ``'*.py'``. If the pattern contains a `normcase`
+ attribute, it is applied to the name and path prior to comparison.
+
+ `normcase` - (optional) A function used to normalize the pattern and
+ filename before matching. Defaults to normcase from
+ ``self.module``, :func:`os.path.normcase`.
+
+ .. seealso:: :func:`fnmatch.fnmatch`
+ """
+ default_normcase = getattr(pattern, 'normcase', self.module.normcase)
+ normcase = normcase or default_normcase
+ name = normcase(self.name)
+ pattern = normcase(pattern)
+ return fnmatch.fnmatchcase(name, pattern)
+
+ def glob(self, pattern):
+ """Return a list of Path objects that match the pattern.
+
+ `pattern` - a path relative to this directory, with wildcards.
+
+ For example, ``Path('/users').glob('*/bin/*')`` returns a list
+ of all the files users have in their :file:`bin` directories.
+
+ .. seealso:: :func:`glob.glob`
+
+ .. note:: Glob is **not** recursive, even when using ``**``.
+ To do recursive globbing see :func:`walk`,
+ :func:`walkdirs` or :func:`walkfiles`.
+ """
+ cls = self._next_class
+ return [cls(s) for s in glob.glob(self / pattern)]
+
+ def iglob(self, pattern):
+ """Return an iterator of Path objects that match the pattern.
+
+ `pattern` - a path relative to this directory, with wildcards.
+
+ For example, ``Path('/users').iglob('*/bin/*')`` returns an
+ iterator of all the files users have in their :file:`bin`
+ directories.
+
+ .. seealso:: :func:`glob.iglob`
+
+ .. note:: Glob is **not** recursive, even when using ``**``.
+ To do recursive globbing see :func:`walk`,
+ :func:`walkdirs` or :func:`walkfiles`.
+ """
+ cls = self._next_class
+ return (cls(s) for s in glob.iglob(self / pattern))
+
+ #
+ # --- Reading or writing an entire file at once.
+
+ def open(self, *args, **kwargs):
+ """Open this file and return a corresponding file object.
+
+ Keyword arguments work as in :func:`io.open`. If the file cannot be
+ opened, an :class:`OSError` is raised.
+ """
+ return open(self, *args, **kwargs)
+
+ def bytes(self):
+ """Open this file, read all bytes, return them as a string."""
+ with self.open('rb') as f:
+ return f.read()
+
+ def chunks(self, size, *args, **kwargs):
+ """Returns a generator yielding chunks of the file, so it can
+ be read piece by piece with a simple for loop.
+
+ Any argument you pass after `size` will be passed to :meth:`open`.
+
+ :example:
+
+ >>> hash = hashlib.md5()
+ >>> for chunk in Path("NEWS.rst").chunks(8192, mode='rb'):
+ ... hash.update(chunk)
+
+ This will read the file by chunks of 8192 bytes.
+ """
+ with self.open(*args, **kwargs) as f:
+ yield from iter(lambda: f.read(size) or None, None)
+
+ def write_bytes(self, bytes, append=False):
+ """Open this file and write the given bytes to it.
+
+ Default behavior is to overwrite any existing file.
+ Call ``p.write_bytes(bytes, append=True)`` to append instead.
+ """
+ with self.open('ab' if append else 'wb') as f:
+ f.write(bytes)
+
+ def read_text(self, encoding=None, errors=None):
+ r"""Open this file, read it in, return the content as a string.
+
+ Optional parameters are passed to :meth:`open`.
+
+ .. seealso:: :meth:`lines`
+ """
+ with self.open(encoding=encoding, errors=errors) as f:
+ return f.read()
+
+ def read_bytes(self):
+ r"""Return the contents of this file as bytes."""
+ with self.open(mode='rb') as f:
+ return f.read()
+
+ def text(self, encoding=None, errors='strict'):
+ r"""Legacy function to read text.
+
+ Converts all newline sequences to ``\n``.
+ """
+ warnings.warn(
+ ".text is deprecated; use read_text",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return U_NEWLINE.sub('\n', self.read_text(encoding, errors))
+
+ def write_text(
+ self, text, encoding=None, errors='strict', linesep=os.linesep, append=False
+ ):
+ r"""Write the given text to this file.
+
+ The default behavior is to overwrite any existing file;
+ to append instead, use the `append=True` keyword argument.
+
+ There are two differences between :meth:`write_text` and
+ :meth:`write_bytes`: newline handling and Unicode handling.
+ See below.
+
+ Parameters:
+
+ `text` - str/bytes - The text to be written.
+
+ `encoding` - str - The text encoding used.
+
+ `errors` - str - How to handle Unicode encoding errors.
+ Default is ``'strict'``. See ``help(unicode.encode)`` for the
+ options. Ignored if `text` isn't a Unicode string.
+
+ `linesep` - keyword argument - str/unicode - The sequence of
+ characters to be used to mark end-of-line. The default is
+ :data:`os.linesep`. Specify ``None`` to
+ use newlines unmodified.
+
+ `append` - keyword argument - bool - Specifies what to do if
+ the file already exists (``True``: append to the end of it;
+ ``False``: overwrite it). The default is ``False``.
+
+
+ --- Newline handling.
+
+ ``write_text()`` converts all standard end-of-line sequences
+ (``'\n'``, ``'\r'``, and ``'\r\n'``) to your platform's default
+ end-of-line sequence (see :data:`os.linesep`; on Windows, for example,
+ the end-of-line marker is ``'\r\n'``).
+
+ To override the platform's default, pass the `linesep=`
+ keyword argument. To preserve the newlines as-is, pass
+ ``linesep=None``.
+
+ This handling applies to Unicode text and bytes, except
+ with Unicode, additional non-ASCII newlines are recognized:
+ ``\x85``, ``\r\x85``, and ``\u2028``.
+
+ --- Unicode
+
+ If `text` isn't Unicode, then apart from newline handling, the
+ bytes are written verbatim to the file. The `encoding` and
+ `errors` arguments are not used and must be omitted.
+
+ If `text` is Unicode, it is first converted to :func:`bytes` using the
+ specified `encoding` (or the default encoding if `encoding`
+ isn't specified). The `errors` argument applies only to this
+ conversion.
+ """
+ if isinstance(text, str):
+ if linesep is not None:
+ text = U_NEWLINE.sub(linesep, text)
+ bytes = text.encode(encoding or sys.getdefaultencoding(), errors)
+ else:
+ warnings.warn(
+ "Writing bytes in write_text is deprecated",
+ DeprecationWarning,
+ stacklevel=1,
+ )
+ assert encoding is None
+ if linesep is not None:
+ text = B_NEWLINE.sub(linesep.encode(), text)
+ bytes = text
+ self.write_bytes(bytes, append=append)
+
+ def lines(self, encoding=None, errors=None, retain=True):
+ r"""Open this file, read all lines, return them in a list.
+
+ Optional arguments:
+ `encoding` - The Unicode encoding (or character set) of
+ the file. The default is ``None``, meaning use
+ ``locale.getpreferredencoding()``.
+ `errors` - How to handle Unicode errors; see
+ `open <https://docs.python.org/3/library/functions.html#open>`_
+ for the options. Default is ``None`` meaning "strict".
+ `retain` - If ``True`` (default), retain newline characters,
+ but translate all newline
+ characters to ``\n``. If ``False``, newline characters are
+ omitted.
+
+ .. seealso:: :meth:`text`
+ """
+ text = U_NEWLINE.sub('\n', self.read_text(encoding, errors))
+ return text.splitlines(retain)
+
+ def write_lines(
+ self,
+ lines,
+ encoding=None,
+ errors='strict',
+ linesep=_default_linesep,
+ append=False,
+ ):
+ r"""Write the given lines of text to this file.
+
+ By default this overwrites any existing file at this path.
+
+ This puts a platform-specific newline sequence on every line.
+ See `linesep` below.
+
+ `lines` - A list of strings.
+
+ `encoding` - A Unicode encoding to use. This applies only if
+ `lines` contains any Unicode strings.
+
+ `errors` - How to handle errors in Unicode encoding. This
+ also applies only to Unicode strings.
+
+ linesep - (deprecated) The desired line-ending. This line-ending is
+ applied to every line. If a line already has any
+ standard line ending (``'\r'``, ``'\n'``, ``'\r\n'``,
+ ``u'\x85'``, ``u'\r\x85'``, ``u'\u2028'``), that will
+ be stripped off and this will be used instead. The
+ default is os.linesep, which is platform-dependent
+ (``'\r\n'`` on Windows, ``'\n'`` on Unix, etc.).
+ Specify ``None`` to write the lines as-is, like
+ ``.writelines`` on a file object.
+
+ Use the keyword argument ``append=True`` to append lines to the
+ file. The default is to overwrite the file.
+ """
+ mode = 'a' if append else 'w'
+ with self.open(mode, encoding=encoding, errors=errors, newline='') as f:
+ f.writelines(self._replace_linesep(lines, linesep))
+
+ @staticmethod
+ def _replace_linesep(lines, linesep):
+ if linesep != _default_linesep:
+ warnings.warn("linesep is deprecated", DeprecationWarning, stacklevel=3)
+ else:
+ linesep = os.linesep
+ if linesep is None:
+ return lines
+
+ return (line + linesep for line in _strip_newlines(lines))
+
+ def read_md5(self):
+ """Calculate the md5 hash for this file.
+
+ This reads through the entire file.
+
+ .. seealso:: :meth:`read_hash`
+ """
+ return self.read_hash('md5')
+
+ def _hash(self, hash_name):
+ """Returns a hash object for the file at the current path.
+
+ `hash_name` should be a hash algo name (such as ``'md5'``
+ or ``'sha1'``) that's available in the :mod:`hashlib` module.
+ """
+ m = hashlib.new(hash_name)
+ for chunk in self.chunks(8192, mode="rb"):
+ m.update(chunk)
+ return m
+
+ def read_hash(self, hash_name):
+ """Calculate given hash for this file.
+
+ List of supported hashes can be obtained from :mod:`hashlib` package.
+ This reads the entire file.
+
+ .. seealso:: :meth:`hashlib.hash.digest`
+ """
+ return self._hash(hash_name).digest()
+
+ def read_hexhash(self, hash_name):
+ """Calculate given hash for this file, returning hexdigest.
+
+ List of supported hashes can be obtained from :mod:`hashlib` package.
+ This reads the entire file.
+
+ .. seealso:: :meth:`hashlib.hash.hexdigest`
+ """
+ return self._hash(hash_name).hexdigest()
+
+ # --- Methods for querying the filesystem.
+ # N.B. On some platforms, the os.path functions may be implemented in C
+ # (e.g. isdir on Windows, Python 3.2.2), and compiled functions don't get
+ # bound. Playing it safe and wrapping them all in method calls.
+
+ def isabs(self):
+ """
+ >>> Path('.').isabs()
+ False
+
+ .. seealso:: :func:`os.path.isabs`
+ """
+ return self.module.isabs(self)
+
+ def exists(self):
+ """.. seealso:: :func:`os.path.exists`"""
+ return self.module.exists(self)
+
+ def isdir(self):
+ """.. seealso:: :func:`os.path.isdir`"""
+ return self.module.isdir(self)
+
+ def isfile(self):
+ """.. seealso:: :func:`os.path.isfile`"""
+ return self.module.isfile(self)
+
+ def islink(self):
+ """.. seealso:: :func:`os.path.islink`"""
+ return self.module.islink(self)
+
+ def ismount(self):
+ """
+ >>> Path('.').ismount()
+ False
+
+ .. seealso:: :func:`os.path.ismount`
+ """
+ return self.module.ismount(self)
+
+ def samefile(self, other):
+ """.. seealso:: :func:`os.path.samefile`"""
+ return self.module.samefile(self, other)
+
+ def getatime(self):
+ """.. seealso:: :attr:`atime`, :func:`os.path.getatime`"""
+ return self.module.getatime(self)
+
+ def set_atime(self, value):
+ mtime_ns = self.stat().st_atime_ns
+ self.utime(ns=(_make_timestamp_ns(value), mtime_ns))
+
+ atime = property(
+ getatime,
+ set_atime,
+ None,
+ """
+ Last access time of the file.
+
+ >>> Path('.').atime > 0
+ True
+
+ Allows setting:
+
+ >>> some_file = Path(getfixture('tmp_path')).joinpath('file.txt').touch()
+ >>> MST = datetime.timezone(datetime.timedelta(hours=-7))
+ >>> some_file.atime = datetime.datetime(1976, 5, 7, 10, tzinfo=MST)
+ >>> some_file.atime
+ 200336400.0
+
+ .. seealso:: :meth:`getatime`, :func:`os.path.getatime`
+ """,
+ )
+
+ def getmtime(self):
+ """.. seealso:: :attr:`mtime`, :func:`os.path.getmtime`"""
+ return self.module.getmtime(self)
+
+ def set_mtime(self, value):
+ atime_ns = self.stat().st_atime_ns
+ self.utime(ns=(atime_ns, _make_timestamp_ns(value)))
+
+ mtime = property(
+ getmtime,
+ set_mtime,
+ None,
+ """
+ Last modified time of the file.
+
+ Allows setting:
+
+ >>> some_file = Path(getfixture('tmp_path')).joinpath('file.txt').touch()
+ >>> MST = datetime.timezone(datetime.timedelta(hours=-7))
+ >>> some_file.mtime = datetime.datetime(1976, 5, 7, 10, tzinfo=MST)
+ >>> some_file.mtime
+ 200336400.0
+
+ .. seealso:: :meth:`getmtime`, :func:`os.path.getmtime`
+ """,
+ )
+
+ def getctime(self):
+ """.. seealso:: :attr:`ctime`, :func:`os.path.getctime`"""
+ return self.module.getctime(self)
+
+ ctime = property(
+ getctime,
+ None,
+ None,
+ """ Creation time of the file.
+
+ .. seealso:: :meth:`getctime`, :func:`os.path.getctime`
+ """,
+ )
+
+ def getsize(self):
+ """.. seealso:: :attr:`size`, :func:`os.path.getsize`"""
+ return self.module.getsize(self)
+
+ size = property(
+ getsize,
+ None,
+ None,
+ """ Size of the file, in bytes.
+
+ .. seealso:: :meth:`getsize`, :func:`os.path.getsize`
+ """,
+ )
+
+ @property
+ def permissions(self) -> masks.Permissions:
+ """
+ Permissions.
+
+ >>> perms = Path('.').permissions
+ >>> isinstance(perms, int)
+ True
+ >>> set(perms.symbolic) <= set('rwx-')
+ True
+ >>> perms.symbolic
+ 'r...'
+ """
+ return masks.Permissions(self.stat().st_mode)
+
+ def access(self, *args, **kwargs):
+ """
+ Return does the real user have access to this path.
+
+ >>> Path('.').access(os.F_OK)
+ True
+
+ .. seealso:: :func:`os.access`
+ """
+ return os.access(self, *args, **kwargs)
+
+ def stat(self):
+ """
+ Perform a ``stat()`` system call on this path.
+
+ >>> Path('.').stat()
+ os.stat_result(...)
+
+ .. seealso:: :meth:`lstat`, :func:`os.stat`
+ """
+ return os.stat(self)
+
+ def lstat(self):
+ """
+ Like :meth:`stat`, but do not follow symbolic links.
+
+ >>> Path('.').lstat() == Path('.').stat()
+ True
+
+ .. seealso:: :meth:`stat`, :func:`os.lstat`
+ """
+ return os.lstat(self)
+
+ def __get_owner_windows(self): # pragma: nocover
+ r"""
+ Return the name of the owner of this file or directory. Follow
+ symbolic links.
+
+ Return a name of the form ``DOMAIN\User Name``; may be a group.
+
+ .. seealso:: :attr:`owner`
+ """
+ desc = win32security.GetFileSecurity(
+ self, win32security.OWNER_SECURITY_INFORMATION
+ )
+ sid = desc.GetSecurityDescriptorOwner()
+ account, domain, typecode = win32security.LookupAccountSid(None, sid)
+ return domain + '\\' + account
+
+ def __get_owner_unix(self): # pragma: nocover
+ """
+ Return the name of the owner of this file or directory. Follow
+ symbolic links.
+
+ .. seealso:: :attr:`owner`
+ """
+ st = self.stat()
+ return pwd.getpwuid(st.st_uid).pw_name
+
+ def __get_owner_not_implemented(self): # pragma: nocover
+ raise NotImplementedError("Ownership not available on this platform.")
+
+ get_owner = (
+ __get_owner_windows
+ if 'win32security' in globals()
+ else __get_owner_unix
+ if 'pwd' in globals()
+ else __get_owner_not_implemented
+ )
+
+ owner = property(
+ get_owner,
+ None,
+ None,
+ """ Name of the owner of this file or directory.
+
+ .. seealso:: :meth:`get_owner`""",
+ )
+
+ if hasattr(os, 'statvfs'):
+
+ def statvfs(self):
+ """Perform a ``statvfs()`` system call on this path.
+
+ .. seealso:: :func:`os.statvfs`
+ """
+ return os.statvfs(self)
+
+ if hasattr(os, 'pathconf'):
+
+ def pathconf(self, name):
+ """.. seealso:: :func:`os.pathconf`"""
+ return os.pathconf(self, name)
+
+ #
+ # --- Modifying operations on files and directories
+
+ def utime(self, *args, **kwargs):
+ """Set the access and modified times of this file.
+
+ .. seealso:: :func:`os.utime`
+ """
+ os.utime(self, *args, **kwargs)
+ return self
+
+ def chmod(self, mode):
+ """
+ Set the mode. May be the new mode (os.chmod behavior) or a `symbolic
+ mode <http://en.wikipedia.org/wiki/Chmod#Symbolic_modes>`_.
+
+ >>> a_file = Path(getfixture('tmp_path')).joinpath('afile.txt').touch()
+ >>> a_file.chmod(0o700)
+ Path(...
+ >>> a_file.chmod('u+x')
+ Path(...
+
+ .. seealso:: :func:`os.chmod`
+ """
+ if isinstance(mode, str):
+ mask = masks.compound(mode)
+ mode = mask(self.stat().st_mode)
+ os.chmod(self, mode)
+ return self
+
+ if hasattr(os, 'chown'):
+
+ def chown(self, uid=-1, gid=-1):
+ """
+ Change the owner and group by names or numbers.
+
+ .. seealso:: :func:`os.chown`
+ """
+
+ def resolve_uid(uid):
+ return uid if isinstance(uid, int) else pwd.getpwnam(uid).pw_uid
+
+ def resolve_gid(gid):
+ return gid if isinstance(gid, int) else grp.getgrnam(gid).gr_gid
+
+ os.chown(self, resolve_uid(uid), resolve_gid(gid))
+ return self
+
+ def rename(self, new):
+ """.. seealso:: :func:`os.rename`"""
+ os.rename(self, new)
+ return self._next_class(new)
+
+ def renames(self, new):
+ """.. seealso:: :func:`os.renames`"""
+ os.renames(self, new)
+ return self._next_class(new)
+
+ #
+ # --- Create/delete operations on directories
+
+ def mkdir(self, mode=0o777):
+ """.. seealso:: :func:`os.mkdir`"""
+ os.mkdir(self, mode)
+ return self
+
+ def mkdir_p(self, mode=0o777):
+ """Like :meth:`mkdir`, but does not raise an exception if the
+ directory already exists."""
+ with contextlib.suppress(FileExistsError):
+ self.mkdir(mode)
+ return self
+
+ def makedirs(self, mode=0o777):
+ """.. seealso:: :func:`os.makedirs`"""
+ os.makedirs(self, mode)
+ return self
+
+ def makedirs_p(self, mode=0o777):
+ """Like :meth:`makedirs`, but does not raise an exception if the
+ directory already exists."""
+ with contextlib.suppress(FileExistsError):
+ self.makedirs(mode)
+ return self
+
+ def rmdir(self):
+ """.. seealso:: :func:`os.rmdir`"""
+ os.rmdir(self)
+ return self
+
+ def rmdir_p(self):
+ """Like :meth:`rmdir`, but does not raise an exception if the
+ directory is not empty or does not exist."""
+ suppressed = FileNotFoundError, FileExistsError, DirectoryNotEmpty
+ with contextlib.suppress(suppressed):
+ with DirectoryNotEmpty.translate():
+ self.rmdir()
+ return self
+
+ def removedirs(self):
+ """.. seealso:: :func:`os.removedirs`"""
+ os.removedirs(self)
+ return self
+
+ def removedirs_p(self):
+ """Like :meth:`removedirs`, but does not raise an exception if the
+ directory is not empty or does not exist."""
+ with contextlib.suppress(FileExistsError, DirectoryNotEmpty):
+ with DirectoryNotEmpty.translate():
+ self.removedirs()
+ return self
+
+ # --- Modifying operations on files
+
+ def touch(self):
+ """Set the access/modified times of this file to the current time.
+ Create the file if it does not exist.
+ """
+ os.close(os.open(self, os.O_WRONLY | os.O_CREAT, 0o666))
+ os.utime(self, None)
+ return self
+
+ def remove(self):
+ """.. seealso:: :func:`os.remove`"""
+ os.remove(self)
+ return self
+
+ def remove_p(self):
+ """Like :meth:`remove`, but does not raise an exception if the
+ file does not exist."""
+ with contextlib.suppress(FileNotFoundError):
+ self.unlink()
+ return self
+
+ unlink = remove
+ unlink_p = remove_p
+
+ # --- Links
+
+ def link(self, newpath):
+ """Create a hard link at `newpath`, pointing to this file.
+
+ .. seealso:: :func:`os.link`
+ """
+ os.link(self, newpath)
+ return self._next_class(newpath)
+
+ def symlink(self, newlink=None):
+ """Create a symbolic link at `newlink`, pointing here.
+
+ If newlink is not supplied, the symbolic link will assume
+ the name self.basename(), creating the link in the cwd.
+
+ .. seealso:: :func:`os.symlink`
+ """
+ if newlink is None:
+ newlink = self.basename()
+ os.symlink(self, newlink)
+ return self._next_class(newlink)
+
+ def readlink(self):
+ """Return the path to which this symbolic link points.
+
+ The result may be an absolute or a relative path.
+
+ .. seealso:: :meth:`readlinkabs`, :func:`os.readlink`
+ """
+ return self._next_class(os.readlink(self))
+
+ def readlinkabs(self):
+ """Return the path to which this symbolic link points.
+
+ The result is always an absolute path.
+
+ .. seealso:: :meth:`readlink`, :func:`os.readlink`
+ """
+ p = self.readlink()
+ return p if p.isabs() else (self.parent / p).abspath()
+
+ # High-level functions from shutil
+ # These functions will be bound to the instance such that
+ # Path(name).copy(target) will invoke shutil.copy(name, target)
+
+ copyfile = shutil.copyfile
+ copymode = shutil.copymode
+ copystat = shutil.copystat
+ copy = shutil.copy
+ copy2 = shutil.copy2
+ copytree = shutil.copytree
+ if hasattr(shutil, 'move'):
+ move = shutil.move
+ rmtree = shutil.rmtree
+
+ def rmtree_p(self):
+ """Like :meth:`rmtree`, but does not raise an exception if the
+ directory does not exist."""
+ with contextlib.suppress(FileNotFoundError):
+ self.rmtree()
+ return self
+
+ def chdir(self):
+ """.. seealso:: :func:`os.chdir`"""
+ os.chdir(self)
+
+ cd = chdir
+
+ def merge_tree(
+ self,
+ dst,
+ symlinks=False,
+ *,
+ copy_function=shutil.copy2,
+ ignore=lambda dir, contents: [],
+ ):
+ """
+ Copy entire contents of self to dst, overwriting existing
+ contents in dst with those in self.
+
+ Pass ``symlinks=True`` to copy symbolic links as links.
+
+ Accepts a ``copy_function``, similar to copytree.
+
+ To avoid overwriting newer files, supply a copy function
+ wrapped in ``only_newer``. For example::
+
+ src.merge_tree(dst, copy_function=only_newer(shutil.copy2))
+ """
+ dst = self._next_class(dst)
+ dst.makedirs_p()
+
+ sources = self.listdir()
+ _ignored = ignore(self, [item.name for item in sources])
+
+ def ignored(item):
+ return item.name in _ignored
+
+ for source in itertools.filterfalse(ignored, sources):
+ dest = dst / source.name
+ if symlinks and source.islink():
+ target = source.readlink()
+ target.symlink(dest)
+ elif source.isdir():
+ source.merge_tree(
+ dest,
+ symlinks=symlinks,
+ copy_function=copy_function,
+ ignore=ignore,
+ )
+ else:
+ copy_function(source, dest)
+
+ self.copystat(dst)
+
+ #
+ # --- Special stuff from os
+
+ if hasattr(os, 'chroot'):
+
+ def chroot(self): # pragma: nocover
+ """.. seealso:: :func:`os.chroot`"""
+ os.chroot(self)
+
+ if hasattr(os, 'startfile'):
+
+ def startfile(self, *args, **kwargs): # pragma: nocover
+ """.. seealso:: :func:`os.startfile`"""
+ os.startfile(self, *args, **kwargs)
+ return self
+
+ # in-place re-writing, courtesy of Martijn Pieters
+ # http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/
+ @contextlib.contextmanager
+ def in_place(
+ self,
+ mode='r',
+ buffering=-1,
+ encoding=None,
+ errors=None,
+ newline=None,
+ backup_extension=None,
+ ):
+ """
+ A context in which a file may be re-written in-place with
+ new content.
+
+ Yields a tuple of :samp:`({readable}, {writable})` file
+ objects, where `writable` replaces `readable`.
+
+ If an exception occurs, the old file is restored, removing the
+ written data.
+
+ Mode *must not* use ``'w'``, ``'a'``, or ``'+'``; only
+ read-only-modes are allowed. A :exc:`ValueError` is raised
+ on invalid modes.
+
+ For example, to add line numbers to a file::
+
+ p = Path(filename)
+ assert p.isfile()
+ with p.in_place() as (reader, writer):
+ for number, line in enumerate(reader, 1):
+ writer.write('{0:3}: '.format(number)))
+ writer.write(line)
+
+ Thereafter, the file at `filename` will have line numbers in it.
+ """
+ if set(mode).intersection('wa+'):
+ raise ValueError('Only read-only file modes can be used')
+
+ # move existing file to backup, create new file with same permissions
+ # borrowed extensively from the fileinput module
+ backup_fn = self + (backup_extension or os.extsep + 'bak')
+ backup_fn.remove_p()
+ self.rename(backup_fn)
+ readable = open(
+ backup_fn,
+ mode,
+ buffering=buffering,
+ encoding=encoding,
+ errors=errors,
+ newline=newline,
+ )
+ try:
+ perm = os.fstat(readable.fileno()).st_mode
+ except OSError:
+ writable = self.open(
+ 'w' + mode.replace('r', ''),
+ buffering=buffering,
+ encoding=encoding,
+ errors=errors,
+ newline=newline,
+ )
+ else:
+ os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
+ os_mode |= getattr(os, 'O_BINARY', 0)
+ fd = os.open(self, os_mode, perm)
+ writable = open(
+ fd,
+ "w" + mode.replace('r', ''),
+ buffering=buffering,
+ encoding=encoding,
+ errors=errors,
+ newline=newline,
+ )
+ with contextlib.suppress(OSError, AttributeError):
+ self.chmod(perm)
+ try:
+ yield readable, writable
+ except Exception:
+ # move backup back
+ readable.close()
+ writable.close()
+ self.remove_p()
+ backup_fn.rename(self)
+ raise
+ else:
+ readable.close()
+ writable.close()
+ finally:
+ backup_fn.remove_p()
+
+ @classes.ClassProperty
+ @classmethod
+ def special(cls):
+ """
+ Return a SpecialResolver object suitable referencing a suitable
+ directory for the relevant platform for the given
+ type of content.
+
+ For example, to get a user config directory, invoke:
+
+ dir = Path.special().user.config
+
+ Uses the `appdirs
+ <https://pypi.python.org/pypi/appdirs/1.4.0>`_ to resolve
+ the paths in a platform-friendly way.
+
+ To create a config directory for 'My App', consider:
+
+ dir = Path.special("My App").user.config.makedirs_p()
+
+ If the ``appdirs`` module is not installed, invocation
+ of special will raise an ImportError.
+ """
+ return functools.partial(SpecialResolver, cls)
+
+
+class DirectoryNotEmpty(OSError):
+ @staticmethod
+ @contextlib.contextmanager
+ def translate():
+ try:
+ yield
+ except OSError as exc:
+ if exc.errno == errno.ENOTEMPTY:
+ raise DirectoryNotEmpty(*exc.args) from exc
+ raise
+
+
+def only_newer(copy_func):
+ """
+ Wrap a copy function (like shutil.copy2) to return
+ the dst if it's newer than the source.
+ """
+
+ @functools.wraps(copy_func)
+ def wrapper(src, dst, *args, **kwargs):
+ is_newer_dst = dst.exists() and dst.getmtime() >= src.getmtime()
+ if is_newer_dst:
+ return dst
+ return copy_func(src, dst, *args, **kwargs)
+
+ return wrapper
+
+
+class ExtantPath(Path):
+ """
+ >>> ExtantPath('.')
+ ExtantPath('.')
+ >>> ExtantPath('does-not-exist')
+ Traceback (most recent call last):
+ OSError: does-not-exist does not exist.
+ """
+
+ def _validate(self):
+ if not self.exists():
+ raise OSError(f"{self} does not exist.")
+
+
+class ExtantFile(Path):
+ """
+ >>> ExtantFile('.')
+ Traceback (most recent call last):
+ FileNotFoundError: . does not exist as a file.
+ >>> ExtantFile('does-not-exist')
+ Traceback (most recent call last):
+ FileNotFoundError: does-not-exist does not exist as a file.
+ """
+
+ def _validate(self):
+ if not self.isfile():
+ raise FileNotFoundError(f"{self} does not exist as a file.")
+
+
+class SpecialResolver:
+ class ResolverScope:
+ def __init__(self, paths, scope):
+ self.paths = paths
+ self.scope = scope
+
+ def __getattr__(self, class_):
+ return self.paths.get_dir(self.scope, class_)
+
+ def __init__(self, path_class, *args, **kwargs):
+ appdirs = importlib.import_module('appdirs')
+
+ vars(self).update(
+ path_class=path_class, wrapper=appdirs.AppDirs(*args, **kwargs)
+ )
+
+ def __getattr__(self, scope):
+ return self.ResolverScope(self, scope)
+
+ def get_dir(self, scope, class_):
+ """
+ Return the callable function from appdirs, but with the
+ result wrapped in self.path_class
+ """
+ prop_name = f'{scope}_{class_}_dir'
+ value = getattr(self.wrapper, prop_name)
+ MultiPath = Multi.for_class(self.path_class)
+ return MultiPath.detect(value)
+
+
+class Multi:
+ """
+ A mix-in for a Path which may contain multiple Path separated by pathsep.
+ """
+
+ @classmethod
+ def for_class(cls, path_cls):
+ name = 'Multi' + path_cls.__name__
+ return type(name, (cls, path_cls), {})
+
+ @classmethod
+ def detect(cls, input):
+ if os.pathsep not in input:
+ cls = cls._next_class
+ return cls(input)
+
+ def __iter__(self):
+ return iter(map(self._next_class, self.split(os.pathsep)))
+
+ @classes.ClassProperty
+ @classmethod
+ def _next_class(cls):
+ """
+ Multi-subclasses should use the parent class
+ """
+ return next(class_ for class_ in cls.__mro__ if not issubclass(class_, Multi))
+
+
+class TempDir(Path):
+ """
+ A temporary directory via :func:`tempfile.mkdtemp`, and
+ constructed with the same parameters that you can use
+ as a context manager.
+
+ For example:
+
+ >>> with TempDir() as d:
+ ... d.isdir() and isinstance(d, Path)
+ True
+
+ The directory is deleted automatically.
+
+ >>> d.isdir()
+ False
+
+ .. seealso:: :func:`tempfile.mkdtemp`
+ """
+
+ @classes.ClassProperty
+ @classmethod
+ def _next_class(cls):
+ return Path
+
+ def __new__(cls, *args, **kwargs):
+ dirname = tempfile.mkdtemp(*args, **kwargs)
+ return super().__new__(cls, dirname)
+
+ def __init__(self, *args, **kwargs):
+ pass
+
+ def __enter__(self):
+ # TempDir should return a Path version of itself and not itself
+ # so that a second context manager does not create a second
+ # temporary directory, but rather changes CWD to the location
+ # of the temporary directory.
+ return self._next_class(self)
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self.rmtree()
+
+
+class Handlers:
+ def strict(msg):
+ raise
+
+ def warn(msg):
+ warnings.warn(msg, TreeWalkWarning)
+
+ def ignore(msg):
+ pass
+
+ @classmethod
+ def _resolve(cls, param):
+ if not callable(param) and param not in vars(Handlers):
+ raise ValueError("invalid errors parameter")
+ return vars(cls).get(param, param)
diff --git a/contrib/python/path/path/classes.py b/contrib/python/path/path/classes.py
new file mode 100644
index 0000000000..b6101d0a7e
--- /dev/null
+++ b/contrib/python/path/path/classes.py
@@ -0,0 +1,27 @@
+import functools
+
+
+class ClassProperty(property):
+ def __get__(self, cls, owner):
+ return self.fget.__get__(None, owner)()
+
+
+class multimethod:
+ """
+ Acts like a classmethod when invoked from the class and like an
+ instancemethod when invoked from the instance.
+ """
+
+ def __init__(self, func):
+ self.func = func
+
+ def __get__(self, instance, owner):
+ """
+ If called on an instance, pass the instance as the first
+ argument.
+ """
+ return (
+ functools.partial(self.func, owner)
+ if instance is None
+ else functools.partial(self.func, owner, instance)
+ )
diff --git a/contrib/python/path/path/masks.py b/contrib/python/path/path/masks.py
new file mode 100644
index 0000000000..e7037e9603
--- /dev/null
+++ b/contrib/python/path/path/masks.py
@@ -0,0 +1,159 @@
+import re
+import functools
+import operator
+import itertools
+
+
+# from jaraco.functools
+def compose(*funcs):
+ compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) # noqa
+ return functools.reduce(compose_two, funcs)
+
+
+# from jaraco.structures.binary
+def gen_bit_values(number):
+ """
+ Return a zero or one for each bit of a numeric value up to the most
+ significant 1 bit, beginning with the least significant bit.
+
+ >>> list(gen_bit_values(16))
+ [0, 0, 0, 0, 1]
+ """
+ digits = bin(number)[2:]
+ return map(int, reversed(digits))
+
+
+# from more_itertools
+def padded(iterable, fillvalue=None, n=None, next_multiple=False):
+ """Yield the elements from *iterable*, followed by *fillvalue*, such that
+ at least *n* items are emitted.
+
+ >>> list(padded([1, 2, 3], '?', 5))
+ [1, 2, 3, '?', '?']
+
+ If *next_multiple* is ``True``, *fillvalue* will be emitted until the
+ number of items emitted is a multiple of *n*::
+
+ >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True))
+ [1, 2, 3, 4, None, None]
+
+ If *n* is ``None``, *fillvalue* will be emitted indefinitely.
+
+ """
+ it = iter(iterable)
+ if n is None:
+ yield from itertools.chain(it, itertools.repeat(fillvalue))
+ elif n < 1:
+ raise ValueError('n must be at least 1')
+ else:
+ item_count = 0
+ for item in it:
+ yield item
+ item_count += 1
+
+ remaining = (n - item_count) % n if next_multiple else n - item_count
+ for _ in range(remaining):
+ yield fillvalue
+
+
+def compound(mode):
+ """
+ Support multiple, comma-separated Unix chmod symbolic modes.
+
+ >>> oct(compound('a=r,u+w')(0))
+ '0o644'
+ """
+ return compose(*map(simple, reversed(mode.split(','))))
+
+
+def simple(mode):
+ """
+ Convert a Unix chmod symbolic mode like ``'ugo+rwx'`` to a function
+ suitable for applying to a mask to affect that change.
+
+ >>> mask = simple('ugo+rwx')
+ >>> mask(0o554) == 0o777
+ True
+
+ >>> simple('go-x')(0o777) == 0o766
+ True
+
+ >>> simple('o-x')(0o445) == 0o444
+ True
+
+ >>> simple('a+x')(0) == 0o111
+ True
+
+ >>> simple('a=rw')(0o057) == 0o666
+ True
+
+ >>> simple('u=x')(0o666) == 0o166
+ True
+
+ >>> simple('g=')(0o157) == 0o107
+ True
+
+ >>> simple('gobbledeegook')
+ Traceback (most recent call last):
+ ValueError: ('Unrecognized symbolic mode', 'gobbledeegook')
+ """
+ # parse the symbolic mode
+ parsed = re.match('(?P<who>[ugoa]+)(?P<op>[-+=])(?P<what>[rwx]*)$', mode)
+ if not parsed:
+ raise ValueError("Unrecognized symbolic mode", mode)
+
+ # generate a mask representing the specified permission
+ spec_map = dict(r=4, w=2, x=1)
+ specs = (spec_map[perm] for perm in parsed.group('what'))
+ spec = functools.reduce(operator.or_, specs, 0)
+
+ # now apply spec to each subject in who
+ shift_map = dict(u=6, g=3, o=0)
+ who = parsed.group('who').replace('a', 'ugo')
+ masks = (spec << shift_map[subj] for subj in who)
+ mask = functools.reduce(operator.or_, masks)
+
+ op = parsed.group('op')
+
+ # if op is -, invert the mask
+ if op == '-':
+ mask ^= 0o777
+
+ # if op is =, retain extant values for unreferenced subjects
+ if op == '=':
+ masks = (0o7 << shift_map[subj] for subj in who)
+ retain = functools.reduce(operator.or_, masks) ^ 0o777
+
+ op_map = {
+ '+': operator.or_,
+ '-': operator.and_,
+ '=': lambda mask, target: target & retain ^ mask,
+ }
+ return functools.partial(op_map[op], mask)
+
+
+class Permissions(int):
+ """
+ >>> perms = Permissions(0o764)
+ >>> oct(perms)
+ '0o764'
+ >>> perms.symbolic
+ 'rwxrw-r--'
+ >>> str(perms)
+ 'rwxrw-r--'
+ >>> str(Permissions(0o222))
+ '-w--w--w-'
+ """
+
+ @property
+ def symbolic(self):
+ return ''.join(
+ ['-', val][bit] for val, bit in zip(itertools.cycle('rwx'), self.bits)
+ )
+
+ @property
+ def bits(self):
+ return reversed(tuple(padded(gen_bit_values(self), 0, n=9)))
+
+ def __str__(self):
+ return self.symbolic
diff --git a/contrib/python/path/path/matchers.py b/contrib/python/path/path/matchers.py
new file mode 100644
index 0000000000..63ca218a80
--- /dev/null
+++ b/contrib/python/path/path/matchers.py
@@ -0,0 +1,59 @@
+import ntpath
+import fnmatch
+
+
+def load(param):
+ """
+ If the supplied parameter is a string, assume it's a simple
+ pattern.
+ """
+ return (
+ Pattern(param)
+ if isinstance(param, str)
+ else param
+ if param is not None
+ else Null()
+ )
+
+
+class Base:
+ pass
+
+
+class Null(Base):
+ def __call__(self, path):
+ return True
+
+
+class Pattern(Base):
+ def __init__(self, pattern):
+ self.pattern = pattern
+
+ def get_pattern(self, normcase):
+ try:
+ return self._pattern
+ except AttributeError:
+ pass
+ self._pattern = normcase(self.pattern)
+ return self._pattern
+
+ def __call__(self, path):
+ normcase = getattr(self, 'normcase', path.module.normcase)
+ pattern = self.get_pattern(normcase)
+ return fnmatch.fnmatchcase(normcase(path.name), pattern)
+
+
+class CaseInsensitive(Pattern):
+ """
+ A Pattern with a ``'normcase'`` property, suitable for passing to
+ :meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`,
+ :meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive.
+
+ For example, to get all files ending in .py, .Py, .pY, or .PY in the
+ current directory::
+
+ from path import Path, matchers
+ Path('.').files(matchers.CaseInsensitive('*.py'))
+ """
+
+ normcase = staticmethod(ntpath.normcase)
diff --git a/contrib/python/path/path/py.typed b/contrib/python/path/path/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/path/path/py.typed
diff --git a/contrib/python/path/ya.make b/contrib/python/path/ya.make
new file mode 100644
index 0000000000..aa9cb07250
--- /dev/null
+++ b/contrib/python/path/ya.make
@@ -0,0 +1,30 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+VERSION(16.7.1)
+
+LICENSE(MIT)
+
+NO_LINT()
+
+PY_SRCS(
+ TOP_LEVEL
+ path/__init__.py
+ path/__init__.pyi
+ path/classes.py
+ path/classes.pyi
+ path/masks.py
+ path/masks.pyi
+ path/matchers.py
+ path/matchers.pyi
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/path/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+ path/py.typed
+)
+
+END()