From 734c3bfff11a955b01a064f45bb121105e2e0e79 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 31 Mar 2026 00:43:39 +0300 Subject: [PATCH] scanner code v1 & new libs --- core/scanner.py | 41 + lib/pathlib-1.0.1.dist-info/INSTALLER | 1 + lib/pathlib-1.0.1.dist-info/LICENSE.txt | 19 + lib/pathlib-1.0.1.dist-info/METADATA | 180 +++ lib/pathlib-1.0.1.dist-info/RECORD | 9 + lib/pathlib-1.0.1.dist-info/REQUESTED | 0 lib/pathlib-1.0.1.dist-info/WHEEL | 5 + lib/pathlib-1.0.1.dist-info/top_level.txt | 1 + lib/pathlib.py | 1280 +++++++++++++++++ lib/pathspec-1.0.4.dist-info/INSTALLER | 1 + lib/pathspec-1.0.4.dist-info/METADATA | 356 +++++ lib/pathspec-1.0.4.dist-info/RECORD | 69 + lib/pathspec-1.0.4.dist-info/REQUESTED | 0 lib/pathspec-1.0.4.dist-info/WHEEL | 4 + lib/pathspec-1.0.4.dist-info/licenses/LICENSE | 373 +++++ lib/pathspec/__init__.py | 68 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 1446 bytes .../__pycache__/_meta.cpython-314.pyc | Bin 0 -> 3842 bytes .../__pycache__/_typing.cpython-314.pyc | Bin 0 -> 3454 bytes .../__pycache__/_version.cpython-314.pyc | Bin 0 -> 237 bytes .../__pycache__/backend.cpython-314.pyc | Bin 0 -> 2026 bytes .../__pycache__/gitignore.cpython-314.pyc | Bin 0 -> 6654 bytes .../__pycache__/pathspec.cpython-314.pyc | Bin 0 -> 20242 bytes .../__pycache__/pattern.cpython-314.pyc | Bin 0 -> 8981 bytes lib/pathspec/__pycache__/util.cpython-314.pyc | Bin 0 -> 34565 bytes lib/pathspec/_backends/__init__.py | 4 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 305 bytes .../__pycache__/_utils.cpython-314.pyc | Bin 0 -> 1785 bytes .../_backends/__pycache__/agg.cpython-314.pyc | Bin 0 -> 3674 bytes lib/pathspec/_backends/_utils.py | 45 + lib/pathspec/_backends/agg.py | 104 ++ lib/pathspec/_backends/hyperscan/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 175 bytes .../__pycache__/_base.cpython-314.pyc | Bin 0 -> 1857 bytes .../__pycache__/base.cpython-314.pyc | Bin 0 -> 786 bytes .../__pycache__/gitignore.cpython-314.pyc | Bin 0 -> 8282 bytes .../__pycache__/pathspec.cpython-314.pyc | Bin 0 -> 8057 bytes lib/pathspec/_backends/hyperscan/_base.py | 78 + lib/pathspec/_backends/hyperscan/base.py | 24 + lib/pathspec/_backends/hyperscan/gitignore.py | 245 ++++ lib/pathspec/_backends/hyperscan/pathspec.py | 251 ++++ lib/pathspec/_backends/re2/__init__.py | 0 .../re2/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 169 bytes .../re2/__pycache__/_base.cpython-314.pyc | Bin 0 -> 2087 bytes .../re2/__pycache__/base.cpython-314.pyc | Bin 0 -> 629 bytes .../re2/__pycache__/gitignore.cpython-314.pyc | Bin 0 -> 5775 bytes .../re2/__pycache__/pathspec.cpython-314.pyc | Bin 0 -> 6512 bytes lib/pathspec/_backends/re2/_base.py | 95 ++ lib/pathspec/_backends/re2/base.py | 18 + lib/pathspec/_backends/re2/gitignore.py | 179 +++ lib/pathspec/_backends/re2/pathspec.py | 187 +++ lib/pathspec/_backends/simple/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 172 bytes .../__pycache__/gitignore.cpython-314.pyc | Bin 0 -> 4007 bytes .../__pycache__/pathspec.cpython-314.pyc | Bin 0 -> 3114 bytes lib/pathspec/_backends/simple/gitignore.py | 104 ++ lib/pathspec/_backends/simple/pathspec.py | 76 + lib/pathspec/_meta.py | 67 + lib/pathspec/_typing.py | 64 + lib/pathspec/_version.py | 5 + lib/pathspec/backend.py | 40 + lib/pathspec/gitignore.py | 165 +++ lib/pathspec/pathspec.py | 460 ++++++ lib/pathspec/pattern.py | 241 ++++ lib/pathspec/patterns/__init__.py | 12 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 393 bytes .../__pycache__/gitwildmatch.cpython-314.pyc | Bin 0 -> 2120 bytes lib/pathspec/patterns/gitignore/__init__.py | 17 + .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 502 bytes .../__pycache__/base.cpython-314.pyc | Bin 0 -> 4737 bytes .../__pycache__/basic.cpython-314.pyc | Bin 0 -> 8152 bytes .../__pycache__/spec.cpython-314.pyc | Bin 0 -> 8731 bytes lib/pathspec/patterns/gitignore/base.py | 176 +++ lib/pathspec/patterns/gitignore/basic.py | 317 ++++ lib/pathspec/patterns/gitignore/spec.py | 335 +++++ lib/pathspec/patterns/gitwildmatch.py | 52 + lib/pathspec/py.typed | 1 + lib/pathspec/util.py | 847 +++++++++++ newplan?.md | 14 + 79 files changed, 6630 insertions(+) create mode 100644 core/scanner.py create mode 100644 lib/pathlib-1.0.1.dist-info/INSTALLER create mode 100644 lib/pathlib-1.0.1.dist-info/LICENSE.txt create mode 100644 lib/pathlib-1.0.1.dist-info/METADATA create mode 100644 lib/pathlib-1.0.1.dist-info/RECORD create mode 100644 lib/pathlib-1.0.1.dist-info/REQUESTED create mode 100644 lib/pathlib-1.0.1.dist-info/WHEEL create mode 100644 lib/pathlib-1.0.1.dist-info/top_level.txt create mode 100644 lib/pathlib.py create mode 100644 lib/pathspec-1.0.4.dist-info/INSTALLER create mode 100644 lib/pathspec-1.0.4.dist-info/METADATA create mode 100644 lib/pathspec-1.0.4.dist-info/RECORD create mode 100644 lib/pathspec-1.0.4.dist-info/REQUESTED create mode 100644 lib/pathspec-1.0.4.dist-info/WHEEL create mode 100644 lib/pathspec-1.0.4.dist-info/licenses/LICENSE create mode 100644 lib/pathspec/__init__.py create mode 100644 lib/pathspec/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/pathspec/__pycache__/_meta.cpython-314.pyc create mode 100644 lib/pathspec/__pycache__/_typing.cpython-314.pyc create mode 100644 lib/pathspec/__pycache__/_version.cpython-314.pyc create mode 100644 lib/pathspec/__pycache__/backend.cpython-314.pyc create mode 100644 lib/pathspec/__pycache__/gitignore.cpython-314.pyc create mode 100644 lib/pathspec/__pycache__/pathspec.cpython-314.pyc create mode 100644 lib/pathspec/__pycache__/pattern.cpython-314.pyc create mode 100644 lib/pathspec/__pycache__/util.cpython-314.pyc create mode 100644 lib/pathspec/_backends/__init__.py create mode 100644 lib/pathspec/_backends/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/pathspec/_backends/__pycache__/_utils.cpython-314.pyc create mode 100644 lib/pathspec/_backends/__pycache__/agg.cpython-314.pyc create mode 100644 lib/pathspec/_backends/_utils.py create mode 100644 lib/pathspec/_backends/agg.py create mode 100644 lib/pathspec/_backends/hyperscan/__init__.py create mode 100644 lib/pathspec/_backends/hyperscan/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/pathspec/_backends/hyperscan/__pycache__/_base.cpython-314.pyc create mode 100644 lib/pathspec/_backends/hyperscan/__pycache__/base.cpython-314.pyc create mode 100644 lib/pathspec/_backends/hyperscan/__pycache__/gitignore.cpython-314.pyc create mode 100644 lib/pathspec/_backends/hyperscan/__pycache__/pathspec.cpython-314.pyc create mode 100644 lib/pathspec/_backends/hyperscan/_base.py create mode 100644 lib/pathspec/_backends/hyperscan/base.py create mode 100644 lib/pathspec/_backends/hyperscan/gitignore.py create mode 100644 lib/pathspec/_backends/hyperscan/pathspec.py create mode 100644 lib/pathspec/_backends/re2/__init__.py create mode 100644 lib/pathspec/_backends/re2/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/pathspec/_backends/re2/__pycache__/_base.cpython-314.pyc create mode 100644 lib/pathspec/_backends/re2/__pycache__/base.cpython-314.pyc create mode 100644 lib/pathspec/_backends/re2/__pycache__/gitignore.cpython-314.pyc create mode 100644 lib/pathspec/_backends/re2/__pycache__/pathspec.cpython-314.pyc create mode 100644 lib/pathspec/_backends/re2/_base.py create mode 100644 lib/pathspec/_backends/re2/base.py create mode 100644 lib/pathspec/_backends/re2/gitignore.py create mode 100644 lib/pathspec/_backends/re2/pathspec.py create mode 100644 lib/pathspec/_backends/simple/__init__.py create mode 100644 lib/pathspec/_backends/simple/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/pathspec/_backends/simple/__pycache__/gitignore.cpython-314.pyc create mode 100644 lib/pathspec/_backends/simple/__pycache__/pathspec.cpython-314.pyc create mode 100644 lib/pathspec/_backends/simple/gitignore.py create mode 100644 lib/pathspec/_backends/simple/pathspec.py create mode 100644 lib/pathspec/_meta.py create mode 100644 lib/pathspec/_typing.py create mode 100644 lib/pathspec/_version.py create mode 100644 lib/pathspec/backend.py create mode 100644 lib/pathspec/gitignore.py create mode 100644 lib/pathspec/pathspec.py create mode 100644 lib/pathspec/pattern.py create mode 100644 lib/pathspec/patterns/__init__.py create mode 100644 lib/pathspec/patterns/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/pathspec/patterns/__pycache__/gitwildmatch.cpython-314.pyc create mode 100644 lib/pathspec/patterns/gitignore/__init__.py create mode 100644 lib/pathspec/patterns/gitignore/__pycache__/__init__.cpython-314.pyc create mode 100644 lib/pathspec/patterns/gitignore/__pycache__/base.cpython-314.pyc create mode 100644 lib/pathspec/patterns/gitignore/__pycache__/basic.cpython-314.pyc create mode 100644 lib/pathspec/patterns/gitignore/__pycache__/spec.cpython-314.pyc create mode 100644 lib/pathspec/patterns/gitignore/base.py create mode 100644 lib/pathspec/patterns/gitignore/basic.py create mode 100644 lib/pathspec/patterns/gitignore/spec.py create mode 100644 lib/pathspec/patterns/gitwildmatch.py create mode 100644 lib/pathspec/py.typed create mode 100644 lib/pathspec/util.py create mode 100644 newplan?.md diff --git a/core/scanner.py b/core/scanner.py new file mode 100644 index 0000000..e17647e --- /dev/null +++ b/core/scanner.py @@ -0,0 +1,41 @@ +import os +import lib.hashlib as hashlib + + +from lib.pathlib import Path +from lib.pathspec import PathSpec + + + +class Scanner: + def __init__(self, base_path, ignore_file=".syncignore", include_file=".syncinclude"): + self.base_path = Path(base_path) + self.ignore_file = self._load_ignore_spec(ignore_file) + self.include_file= self._load_include_spec(include_file) + + def _load_ignore_spec(self, file): + pass + + + def _load_include_spec(self, file): + pass + + def get_current_state(self): + state = {} + + for root , dirs, files in os.walk(self.base_path): + dirs[:] = [d for d in dirs if not self.spec.match_file(str(Path(root, d).relative_to(self.base_path)))] + + for file in files: + full_path = Path(root) / file + rel_path = str(full_path.relative_to(self.base_path)) + + if not self.spec.match_file(rel_path): + state[rel_path] = self._hash_file(rel_path) + + return state + + + def _hash_file(self, path) + return hashlib.md5(open(path, 'rb').read()).hexdigest() + diff --git a/lib/pathlib-1.0.1.dist-info/INSTALLER b/lib/pathlib-1.0.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/lib/pathlib-1.0.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/pathlib-1.0.1.dist-info/LICENSE.txt b/lib/pathlib-1.0.1.dist-info/LICENSE.txt new file mode 100644 index 0000000..b272571 --- /dev/null +++ b/lib/pathlib-1.0.1.dist-info/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2012-2014 Antoine Pitrou and contributors + +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/lib/pathlib-1.0.1.dist-info/METADATA b/lib/pathlib-1.0.1.dist-info/METADATA new file mode 100644 index 0000000..1e007dd --- /dev/null +++ b/lib/pathlib-1.0.1.dist-info/METADATA @@ -0,0 +1,180 @@ +Metadata-Version: 2.1 +Name: pathlib +Version: 1.0.1 +Summary: Object-oriented filesystem paths +Home-page: https://pathlib.readthedocs.org/ +Download-URL: https://pypi.python.org/pypi/pathlib/ +Author: Antoine Pitrou +Author-email: solipsis@pitrou.net +License: MIT License +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: System :: Filesystems +License-File: LICENSE.txt + +pathlib offers a set of classes to handle filesystem paths. It offers the +following advantages over using string objects: + +* No more cumbersome use of os and os.path functions. Everything can be + done easily through operators, attribute accesses, and method calls. + +* Embodies the semantics of different path types. For example, comparing + Windows paths ignores casing. + +* Well-defined semantics, eliminating any warts or ambiguities (forward vs. + backward slashes, etc.). + +Requirements +------------ + +Python 3.2 or later is recommended, but pathlib is also usable with Python 2.7 +and 2.6. + +Install +------- + +In Python 3.4, pathlib is now part of the standard library. For Python 3.3 +and earlier, ``easy_install pathlib`` or ``pip install pathlib`` should do +the trick. + +Examples +-------- + +Importing the module classes:: + + >>> from pathlib import * + +Listing Python source files in a directory:: + + >>> list(p.glob('*.py')) + [PosixPath('test_pathlib.py'), PosixPath('setup.py'), + PosixPath('pathlib.py')] + +Navigating inside a directory tree:: + + >>> p = Path('/etc') + >>> q = p / 'init.d' / 'reboot' + >>> q + PosixPath('/etc/init.d/reboot') + >>> q.resolve() + PosixPath('/etc/rc.d/init.d/halt') + +Querying path properties:: + + >>> q.exists() + True + >>> q.is_dir() + False + +Opening a file:: + + >>> with q.open() as f: f.readline() + ... + '#!/bin/bash\n' + + +Documentation +------------- + +The full documentation can be read at `Read the Docs +`_. + + +Contributing +------------ + +Main development now takes place in the Python standard library: see +the `Python developer's guide `_, and +report issues on the `Python bug tracker `_. + +However, if you find an issue specific to prior versions of Python +(such as 2.7 or 3.2), you can post an issue on the +`BitBucket project page `_. + + +History +------- + +Version 1.0.1 +^^^^^^^^^^^^^ + +- Pull requestion #4: Python 2.6 compatibility by eevee. + +Version 1.0 +^^^^^^^^^^^ + +This version brings ``pathlib`` up to date with the official Python 3.4 +release, and also fixes a couple of 2.7-specific issues. + +- Python issue #20765: Add missing documentation for PurePath.with_name() + and PurePath.with_suffix(). +- Fix test_mkdir_parents when the working directory has additional bits + set (such as the setgid or sticky bits). +- Python issue #20111: pathlib.Path.with_suffix() now sanity checks the + given suffix. +- Python issue #19918: Fix PurePath.relative_to() under Windows. +- Python issue #19921: When Path.mkdir() is called with parents=True, any + missing parent is created with the default permissions, ignoring the mode + argument (mimicking the POSIX "mkdir -p" command). +- Python issue #19887: Improve the Path.resolve() algorithm to support + certain symlink chains. +- Make pathlib usable under Python 2.7 with unicode pathnames (only pure + ASCII, though). +- Issue #21: fix TypeError under Python 2.7 when using new division. +- Add tox support for easier testing. + +Version 0.97 +^^^^^^^^^^^^ + +This version brings ``pathlib`` up to date with the final API specified +in :pep:`428`. The changes are too long to list here, it is recommended +to read the `documentation `_. + +.. warning:: + The API in this version is partially incompatible with pathlib 0.8 and + earlier. Be sure to check your code for possible breakage! + +Version 0.8 +^^^^^^^^^^^ + +- Add PurePath.name and PurePath.anchor. +- Add Path.owner and Path.group. +- Add Path.replace(). +- Add Path.as_uri(). +- Issue #10: when creating a file with Path.open(), don't set the executable + bit. +- Issue #11: fix comparisons with non-Path objects. + +Version 0.7 +^^^^^^^^^^^ + +- Add '**' (recursive) patterns to Path.glob(). +- Fix openat() support after the API refactoring in Python 3.3 beta1. +- Add a *target_is_directory* argument to Path.symlink_to() + +Version 0.6 +^^^^^^^^^^^ + +- Add Path.is_file() and Path.is_symlink() +- Add Path.glob() and Path.rglob() +- Add PurePath.match() + +Version 0.5 +^^^^^^^^^^^ + +- Add Path.mkdir(). +- Add Python 2.7 compatibility by Michele Lacchia. +- Make parent() raise ValueError when the level is greater than the path + length. + + diff --git a/lib/pathlib-1.0.1.dist-info/RECORD b/lib/pathlib-1.0.1.dist-info/RECORD new file mode 100644 index 0000000..f702952 --- /dev/null +++ b/lib/pathlib-1.0.1.dist-info/RECORD @@ -0,0 +1,9 @@ +__pycache__/pathlib.cpython-314.pyc,, +pathlib-1.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pathlib-1.0.1.dist-info/LICENSE.txt,sha256=7FK0RgeHp9ofbbcLvp4eZJxz98sU7rrxDl25_n9-nNk,1080 +pathlib-1.0.1.dist-info/METADATA,sha256=NrN3I2q6qf7u2iD0kK0Or6JrsqSP-WXXK41ZAkQznuI,5149 +pathlib-1.0.1.dist-info/RECORD,, +pathlib-1.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pathlib-1.0.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 +pathlib-1.0.1.dist-info/top_level.txt,sha256=zhefsaNuASYaoZF-ELG3TPFn6S2cuB1fd8PlNFLrMh0,8 +pathlib.py,sha256=9MfboQTEcuscgm7ZALBMTDpLwD-2PCTSUyhlA2tcvqw,41481 diff --git a/lib/pathlib-1.0.1.dist-info/REQUESTED b/lib/pathlib-1.0.1.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/lib/pathlib-1.0.1.dist-info/WHEEL b/lib/pathlib-1.0.1.dist-info/WHEEL new file mode 100644 index 0000000..becc9a6 --- /dev/null +++ b/lib/pathlib-1.0.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/lib/pathlib-1.0.1.dist-info/top_level.txt b/lib/pathlib-1.0.1.dist-info/top_level.txt new file mode 100644 index 0000000..c7709d3 --- /dev/null +++ b/lib/pathlib-1.0.1.dist-info/top_level.txt @@ -0,0 +1 @@ +pathlib diff --git a/lib/pathlib.py b/lib/pathlib.py new file mode 100644 index 0000000..9ab0e70 --- /dev/null +++ b/lib/pathlib.py @@ -0,0 +1,1280 @@ +import fnmatch +import functools +import io +import ntpath +import os +import posixpath +import re +import sys +import time +from collections import Sequence +from contextlib import contextmanager +from errno import EINVAL, ENOENT +from operator import attrgetter +from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO +try: + from urllib import quote as urlquote, quote as urlquote_from_bytes +except ImportError: + from urllib.parse import quote as urlquote, quote_from_bytes as urlquote_from_bytes + + +try: + intern = intern +except NameError: + intern = sys.intern +try: + basestring = basestring +except NameError: + basestring = str + +supports_symlinks = True +try: + import nt +except ImportError: + nt = None +else: + if sys.getwindowsversion()[:2] >= (6, 0) and sys.version_info >= (3, 2): + from nt import _getfinalpathname + else: + supports_symlinks = False + _getfinalpathname = None + + +__all__ = [ + "PurePath", "PurePosixPath", "PureWindowsPath", + "Path", "PosixPath", "WindowsPath", + ] + +# +# Internals +# + +_py2 = sys.version_info < (3,) +_py2_fs_encoding = 'ascii' + +def _py2_fsencode(parts): + # py2 => minimal unicode support + return [part.encode(_py2_fs_encoding) if isinstance(part, unicode) + else part for part in parts] + +def _is_wildcard_pattern(pat): + # Whether this pattern needs actual matching using fnmatch, or can + # be looked up directly as a file. + return "*" in pat or "?" in pat or "[" in pat + + +class _Flavour(object): + """A flavour implements a particular (platform-specific) set of path + semantics.""" + + def __init__(self): + self.join = self.sep.join + + def parse_parts(self, parts): + if _py2: + parts = _py2_fsencode(parts) + parsed = [] + sep = self.sep + altsep = self.altsep + drv = root = '' + it = reversed(parts) + for part in it: + if not part: + continue + if altsep: + part = part.replace(altsep, sep) + drv, root, rel = self.splitroot(part) + if sep in rel: + for x in reversed(rel.split(sep)): + if x and x != '.': + parsed.append(intern(x)) + else: + if rel and rel != '.': + parsed.append(intern(rel)) + if drv or root: + if not drv: + # If no drive is present, try to find one in the previous + # parts. This makes the result of parsing e.g. + # ("C:", "/", "a") reasonably intuitive. + for part in it: + drv = self.splitroot(part)[0] + if drv: + break + break + if drv or root: + parsed.append(drv + root) + parsed.reverse() + return drv, root, parsed + + def join_parsed_parts(self, drv, root, parts, drv2, root2, parts2): + """ + Join the two paths represented by the respective + (drive, root, parts) tuples. Return a new (drive, root, parts) tuple. + """ + if root2: + if not drv2 and drv: + return drv, root2, [drv + root2] + parts2[1:] + elif drv2: + if drv2 == drv or self.casefold(drv2) == self.casefold(drv): + # Same drive => second path is relative to the first + return drv, root, parts + parts2[1:] + else: + # Second path is non-anchored (common case) + return drv, root, parts + parts2 + return drv2, root2, parts2 + + +class _WindowsFlavour(_Flavour): + # Reference for Windows paths can be found at + # http://msdn.microsoft.com/en-us/library/aa365247%28v=vs.85%29.aspx + + sep = '\\' + altsep = '/' + has_drv = True + pathmod = ntpath + + is_supported = (nt is not None) + + drive_letters = ( + set(chr(x) for x in range(ord('a'), ord('z') + 1)) | + set(chr(x) for x in range(ord('A'), ord('Z') + 1)) + ) + ext_namespace_prefix = '\\\\?\\' + + reserved_names = ( + set(['CON', 'PRN', 'AUX', 'NUL']) | + set(['COM%d' % i for i in range(1, 10)]) | + set(['LPT%d' % i for i in range(1, 10)]) + ) + + # Interesting findings about extended paths: + # - '\\?\c:\a', '//?/c:\a' and '//?/c:/a' are all supported + # but '\\?\c:/a' is not + # - extended paths are always absolute; "relative" extended paths will + # fail. + + def splitroot(self, part, sep=sep): + first = part[0:1] + second = part[1:2] + if (second == sep and first == sep): + # XXX extended paths should also disable the collapsing of "." + # components (according to MSDN docs). + prefix, part = self._split_extended_path(part) + first = part[0:1] + second = part[1:2] + else: + prefix = '' + third = part[2:3] + if (second == sep and first == sep and third != sep): + # is a UNC path: + # vvvvvvvvvvvvvvvvvvvvv root + # \\machine\mountpoint\directory\etc\... + # directory ^^^^^^^^^^^^^^ + index = part.find(sep, 2) + if index != -1: + index2 = part.find(sep, index + 1) + # a UNC path can't have two slashes in a row + # (after the initial two) + if index2 != index + 1: + if index2 == -1: + index2 = len(part) + if prefix: + return prefix + part[1:index2], sep, part[index2+1:] + else: + return part[:index2], sep, part[index2+1:] + drv = root = '' + if second == ':' and first in self.drive_letters: + drv = part[:2] + part = part[2:] + first = third + if first == sep: + root = first + part = part.lstrip(sep) + return prefix + drv, root, part + + def casefold(self, s): + return s.lower() + + def casefold_parts(self, parts): + return [p.lower() for p in parts] + + def resolve(self, path): + s = str(path) + if not s: + return os.getcwd() + if _getfinalpathname is not None: + return self._ext_to_normal(_getfinalpathname(s)) + # Means fallback on absolute + return None + + def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix): + prefix = '' + if s.startswith(ext_prefix): + prefix = s[:4] + s = s[4:] + if s.startswith('UNC\\'): + prefix += s[:3] + s = '\\' + s[3:] + return prefix, s + + def _ext_to_normal(self, s): + # Turn back an extended path into a normal DOS-like path + return self._split_extended_path(s)[1] + + def is_reserved(self, parts): + # NOTE: the rules for reserved names seem somewhat complicated + # (e.g. r"..\NUL" is reserved but not r"foo\NUL"). + # We err on the side of caution and return True for paths which are + # not considered reserved by Windows. + if not parts: + return False + if parts[0].startswith('\\\\'): + # UNC paths are never reserved + return False + return parts[-1].partition('.')[0].upper() in self.reserved_names + + def make_uri(self, path): + # Under Windows, file URIs use the UTF-8 encoding. + drive = path.drive + if len(drive) == 2 and drive[1] == ':': + # It's a path on a local drive => 'file:///c:/a/b' + rest = path.as_posix()[2:].lstrip('/') + return 'file:///%s/%s' % ( + drive, urlquote_from_bytes(rest.encode('utf-8'))) + else: + # It's a path on a network drive => 'file://host/share/a/b' + return 'file:' + urlquote_from_bytes(path.as_posix().encode('utf-8')) + + +class _PosixFlavour(_Flavour): + sep = '/' + altsep = '' + has_drv = False + pathmod = posixpath + + is_supported = (os.name != 'nt') + + def splitroot(self, part, sep=sep): + if part and part[0] == sep: + stripped_part = part.lstrip(sep) + # According to POSIX path resolution: + # http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11 + # "A pathname that begins with two successive slashes may be + # interpreted in an implementation-defined manner, although more + # than two leading slashes shall be treated as a single slash". + if len(part) - len(stripped_part) == 2: + return '', sep * 2, stripped_part + else: + return '', sep, stripped_part + else: + return '', '', part + + def casefold(self, s): + return s + + def casefold_parts(self, parts): + return parts + + def resolve(self, path): + sep = self.sep + accessor = path._accessor + seen = {} + def _resolve(path, rest): + if rest.startswith(sep): + path = '' + + for name in rest.split(sep): + if not name or name == '.': + # current dir + continue + if name == '..': + # parent dir + path, _, _ = path.rpartition(sep) + continue + newpath = path + sep + name + if newpath in seen: + # Already seen this path + path = seen[newpath] + if path is not None: + # use cached value + continue + # The symlink is not resolved, so we must have a symlink loop. + raise RuntimeError("Symlink loop from %r" % newpath) + # Resolve the symbolic link + try: + target = accessor.readlink(newpath) + except OSError as e: + if e.errno != EINVAL: + raise + # Not a symlink + path = newpath + else: + seen[newpath] = None # not resolved symlink + path = _resolve(path, target) + seen[newpath] = path # resolved symlink + + return path + # NOTE: according to POSIX, getcwd() cannot contain path components + # which are symlinks. + base = '' if path.is_absolute() else os.getcwd() + return _resolve(base, str(path)) or sep + + def is_reserved(self, parts): + return False + + def make_uri(self, path): + # We represent the path using the local filesystem encoding, + # for portability to other applications. + bpath = bytes(path) + return 'file://' + urlquote_from_bytes(bpath) + + +_windows_flavour = _WindowsFlavour() +_posix_flavour = _PosixFlavour() + + +class _Accessor: + """An accessor implements a particular (system-specific or not) way of + accessing paths on the filesystem.""" + + +class _NormalAccessor(_Accessor): + + def _wrap_strfunc(strfunc): + @functools.wraps(strfunc) + def wrapped(pathobj, *args): + return strfunc(str(pathobj), *args) + return staticmethod(wrapped) + + def _wrap_binary_strfunc(strfunc): + @functools.wraps(strfunc) + def wrapped(pathobjA, pathobjB, *args): + return strfunc(str(pathobjA), str(pathobjB), *args) + return staticmethod(wrapped) + + stat = _wrap_strfunc(os.stat) + + lstat = _wrap_strfunc(os.lstat) + + open = _wrap_strfunc(os.open) + + listdir = _wrap_strfunc(os.listdir) + + chmod = _wrap_strfunc(os.chmod) + + if hasattr(os, "lchmod"): + lchmod = _wrap_strfunc(os.lchmod) + else: + def lchmod(self, pathobj, mode): + raise NotImplementedError("lchmod() not available on this system") + + mkdir = _wrap_strfunc(os.mkdir) + + unlink = _wrap_strfunc(os.unlink) + + rmdir = _wrap_strfunc(os.rmdir) + + rename = _wrap_binary_strfunc(os.rename) + + if sys.version_info >= (3, 3): + replace = _wrap_binary_strfunc(os.replace) + + if nt: + if supports_symlinks: + symlink = _wrap_binary_strfunc(os.symlink) + else: + def symlink(a, b, target_is_directory): + raise NotImplementedError("symlink() not available on this system") + else: + # Under POSIX, os.symlink() takes two args + @staticmethod + def symlink(a, b, target_is_directory): + return os.symlink(str(a), str(b)) + + utime = _wrap_strfunc(os.utime) + + # Helper for resolve() + def readlink(self, path): + return os.readlink(path) + + +_normal_accessor = _NormalAccessor() + + +# +# Globbing helpers +# + +@contextmanager +def _cached(func): + try: + func.__cached__ + yield func + except AttributeError: + cache = {} + def wrapper(*args): + try: + return cache[args] + except KeyError: + value = cache[args] = func(*args) + return value + wrapper.__cached__ = True + try: + yield wrapper + finally: + cache.clear() + +def _make_selector(pattern_parts): + pat = pattern_parts[0] + child_parts = pattern_parts[1:] + if pat == '**': + cls = _RecursiveWildcardSelector + elif '**' in pat: + raise ValueError("Invalid pattern: '**' can only be an entire path component") + elif _is_wildcard_pattern(pat): + cls = _WildcardSelector + else: + cls = _PreciseSelector + return cls(pat, child_parts) + +if hasattr(functools, "lru_cache"): + _make_selector = functools.lru_cache()(_make_selector) + + +class _Selector: + """A selector matches a specific glob pattern part against the children + of a given path.""" + + def __init__(self, child_parts): + self.child_parts = child_parts + if child_parts: + self.successor = _make_selector(child_parts) + else: + self.successor = _TerminatingSelector() + + def select_from(self, parent_path): + """Iterate over all child paths of `parent_path` matched by this + selector. This can contain parent_path itself.""" + path_cls = type(parent_path) + is_dir = path_cls.is_dir + exists = path_cls.exists + listdir = parent_path._accessor.listdir + return self._select_from(parent_path, is_dir, exists, listdir) + + +class _TerminatingSelector: + + def _select_from(self, parent_path, is_dir, exists, listdir): + yield parent_path + + +class _PreciseSelector(_Selector): + + def __init__(self, name, child_parts): + self.name = name + _Selector.__init__(self, child_parts) + + def _select_from(self, parent_path, is_dir, exists, listdir): + if not is_dir(parent_path): + return + path = parent_path._make_child_relpath(self.name) + if exists(path): + for p in self.successor._select_from(path, is_dir, exists, listdir): + yield p + + +class _WildcardSelector(_Selector): + + def __init__(self, pat, child_parts): + self.pat = re.compile(fnmatch.translate(pat)) + _Selector.__init__(self, child_parts) + + def _select_from(self, parent_path, is_dir, exists, listdir): + if not is_dir(parent_path): + return + cf = parent_path._flavour.casefold + for name in listdir(parent_path): + casefolded = cf(name) + if self.pat.match(casefolded): + path = parent_path._make_child_relpath(name) + for p in self.successor._select_from(path, is_dir, exists, listdir): + yield p + + +class _RecursiveWildcardSelector(_Selector): + + def __init__(self, pat, child_parts): + _Selector.__init__(self, child_parts) + + def _iterate_directories(self, parent_path, is_dir, listdir): + yield parent_path + for name in listdir(parent_path): + path = parent_path._make_child_relpath(name) + if is_dir(path): + for p in self._iterate_directories(path, is_dir, listdir): + yield p + + def _select_from(self, parent_path, is_dir, exists, listdir): + if not is_dir(parent_path): + return + with _cached(listdir) as listdir: + yielded = set() + try: + successor_select = self.successor._select_from + for starting_point in self._iterate_directories(parent_path, is_dir, listdir): + for p in successor_select(starting_point, is_dir, exists, listdir): + if p not in yielded: + yield p + yielded.add(p) + finally: + yielded.clear() + + +# +# Public API +# + +class _PathParents(Sequence): + """This object provides sequence-like access to the logical ancestors + of a path. Don't try to construct it yourself.""" + __slots__ = ('_pathcls', '_drv', '_root', '_parts') + + def __init__(self, path): + # We don't store the instance to avoid reference cycles + self._pathcls = type(path) + self._drv = path._drv + self._root = path._root + self._parts = path._parts + + def __len__(self): + if self._drv or self._root: + return len(self._parts) - 1 + else: + return len(self._parts) + + def __getitem__(self, idx): + if idx < 0 or idx >= len(self): + raise IndexError(idx) + return self._pathcls._from_parsed_parts(self._drv, self._root, + self._parts[:-idx - 1]) + + def __repr__(self): + return "<{0}.parents>".format(self._pathcls.__name__) + + +class PurePath(object): + """PurePath represents a filesystem path and offers operations which + don't imply any actual filesystem I/O. Depending on your system, + instantiating a PurePath will return either a PurePosixPath or a + PureWindowsPath object. You can also instantiate either of these classes + directly, regardless of your system. + """ + __slots__ = ( + '_drv', '_root', '_parts', + '_str', '_hash', '_pparts', '_cached_cparts', + ) + + def __new__(cls, *args): + """Construct a PurePath from one or several strings and or existing + PurePath objects. The strings and path objects are combined so as + to yield a canonicalized path, which is incorporated into the + new PurePath object. + """ + if cls is PurePath: + cls = PureWindowsPath if os.name == 'nt' else PurePosixPath + return cls._from_parts(args) + + def __reduce__(self): + # Using the parts tuple helps share interned path parts + # when pickling related paths. + return (self.__class__, tuple(self._parts)) + + @classmethod + def _parse_args(cls, args): + # This is useful when you don't want to create an instance, just + # canonicalize some constructor arguments. + parts = [] + for a in args: + if isinstance(a, PurePath): + parts += a._parts + elif isinstance(a, basestring): + parts.append(a) + else: + raise TypeError( + "argument should be a path or str object, not %r" + % type(a)) + return cls._flavour.parse_parts(parts) + + @classmethod + def _from_parts(cls, args, init=True): + # We need to call _parse_args on the instance, so as to get the + # right flavour. + self = object.__new__(cls) + drv, root, parts = self._parse_args(args) + self._drv = drv + self._root = root + self._parts = parts + if init: + self._init() + return self + + @classmethod + def _from_parsed_parts(cls, drv, root, parts, init=True): + self = object.__new__(cls) + self._drv = drv + self._root = root + self._parts = parts + if init: + self._init() + return self + + @classmethod + def _format_parsed_parts(cls, drv, root, parts): + if drv or root: + return drv + root + cls._flavour.join(parts[1:]) + else: + return cls._flavour.join(parts) + + def _init(self): + # Overriden in concrete Path + pass + + def _make_child(self, args): + drv, root, parts = self._parse_args(args) + drv, root, parts = self._flavour.join_parsed_parts( + self._drv, self._root, self._parts, drv, root, parts) + return self._from_parsed_parts(drv, root, parts) + + def __str__(self): + """Return the string representation of the path, suitable for + passing to system calls.""" + try: + return self._str + except AttributeError: + self._str = self._format_parsed_parts(self._drv, self._root, + self._parts) or '.' + return self._str + + def as_posix(self): + """Return the string representation of the path with forward (/) + slashes.""" + f = self._flavour + return str(self).replace(f.sep, '/') + + def __bytes__(self): + """Return the bytes representation of the path. This is only + recommended to use under Unix.""" + if sys.version_info < (3, 2): + raise NotImplementedError("needs Python 3.2 or later") + return os.fsencode(str(self)) + + def __repr__(self): + return "{0}({1!r})".format(self.__class__.__name__, self.as_posix()) + + def as_uri(self): + """Return the path as a 'file' URI.""" + if not self.is_absolute(): + raise ValueError("relative path can't be expressed as a file URI") + return self._flavour.make_uri(self) + + @property + def _cparts(self): + # Cached casefolded parts, for hashing and comparison + try: + return self._cached_cparts + except AttributeError: + self._cached_cparts = self._flavour.casefold_parts(self._parts) + return self._cached_cparts + + def __eq__(self, other): + if not isinstance(other, PurePath): + return NotImplemented + return self._cparts == other._cparts and self._flavour is other._flavour + + def __ne__(self, other): + return not self == other + + def __hash__(self): + try: + return self._hash + except AttributeError: + self._hash = hash(tuple(self._cparts)) + return self._hash + + def __lt__(self, other): + if not isinstance(other, PurePath) or self._flavour is not other._flavour: + return NotImplemented + return self._cparts < other._cparts + + def __le__(self, other): + if not isinstance(other, PurePath) or self._flavour is not other._flavour: + return NotImplemented + return self._cparts <= other._cparts + + def __gt__(self, other): + if not isinstance(other, PurePath) or self._flavour is not other._flavour: + return NotImplemented + return self._cparts > other._cparts + + def __ge__(self, other): + if not isinstance(other, PurePath) or self._flavour is not other._flavour: + return NotImplemented + return self._cparts >= other._cparts + + drive = property(attrgetter('_drv'), + doc="""The drive prefix (letter or UNC path), if any.""") + + root = property(attrgetter('_root'), + doc="""The root of the path, if any.""") + + @property + def anchor(self): + """The concatenation of the drive and root, or ''.""" + anchor = self._drv + self._root + return anchor + + @property + def name(self): + """The final path component, if any.""" + parts = self._parts + if len(parts) == (1 if (self._drv or self._root) else 0): + return '' + return parts[-1] + + @property + def suffix(self): + """The final component's last suffix, if any.""" + name = self.name + i = name.rfind('.') + if 0 < i < len(name) - 1: + return name[i:] + else: + return '' + + @property + def suffixes(self): + """A list of the final component's suffixes, if any.""" + name = self.name + if name.endswith('.'): + return [] + name = name.lstrip('.') + return ['.' + suffix for suffix in name.split('.')[1:]] + + @property + def stem(self): + """The final path component, minus its last suffix.""" + name = self.name + i = name.rfind('.') + if 0 < i < len(name) - 1: + return name[:i] + else: + return name + + def with_name(self, name): + """Return a new path with the file name changed.""" + if not self.name: + raise ValueError("%r has an empty name" % (self,)) + return self._from_parsed_parts(self._drv, self._root, + self._parts[:-1] + [name]) + + def with_suffix(self, suffix): + """Return a new path with the file suffix changed (or added, if none).""" + # XXX if suffix is None, should the current suffix be removed? + drv, root, parts = self._flavour.parse_parts((suffix,)) + if drv or root or len(parts) != 1: + raise ValueError("Invalid suffix %r" % (suffix)) + suffix = parts[0] + if not suffix.startswith('.'): + raise ValueError("Invalid suffix %r" % (suffix)) + name = self.name + if not name: + raise ValueError("%r has an empty name" % (self,)) + old_suffix = self.suffix + if not old_suffix: + name = name + suffix + else: + name = name[:-len(old_suffix)] + suffix + return self._from_parsed_parts(self._drv, self._root, + self._parts[:-1] + [name]) + + def relative_to(self, *other): + """Return the relative path to another path identified by the passed + arguments. If the operation is not possible (because this is not + a subpath of the other path), raise ValueError. + """ + # For the purpose of this method, drive and root are considered + # separate parts, i.e.: + # Path('c:/').relative_to('c:') gives Path('/') + # Path('c:/').relative_to('/') raise ValueError + if not other: + raise TypeError("need at least one argument") + parts = self._parts + drv = self._drv + root = self._root + if root: + abs_parts = [drv, root] + parts[1:] + else: + abs_parts = parts + to_drv, to_root, to_parts = self._parse_args(other) + if to_root: + to_abs_parts = [to_drv, to_root] + to_parts[1:] + else: + to_abs_parts = to_parts + n = len(to_abs_parts) + cf = self._flavour.casefold_parts + if (root or drv) if n == 0 else cf(abs_parts[:n]) != cf(to_abs_parts): + formatted = self._format_parsed_parts(to_drv, to_root, to_parts) + raise ValueError("{!r} does not start with {!r}" + .format(str(self), str(formatted))) + return self._from_parsed_parts('', root if n == 1 else '', + abs_parts[n:]) + + @property + def parts(self): + """An object providing sequence-like access to the + components in the filesystem path.""" + # We cache the tuple to avoid building a new one each time .parts + # is accessed. XXX is this necessary? + try: + return self._pparts + except AttributeError: + self._pparts = tuple(self._parts) + return self._pparts + + def joinpath(self, *args): + """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._make_child(args) + + def __truediv__(self, key): + return self._make_child((key,)) + + def __rtruediv__(self, key): + return self._from_parts([key] + self._parts) + + if sys.version_info < (3,): + __div__ = __truediv__ + __rdiv__ = __rtruediv__ + + @property + def parent(self): + """The logical parent of the path.""" + drv = self._drv + root = self._root + parts = self._parts + if len(parts) == 1 and (drv or root): + return self + return self._from_parsed_parts(drv, root, parts[:-1]) + + @property + def parents(self): + """A sequence of this path's logical parents.""" + return _PathParents(self) + + def is_absolute(self): + """True if the path is absolute (has both a root and, if applicable, + a drive).""" + if not self._root: + return False + return not self._flavour.has_drv or bool(self._drv) + + def is_reserved(self): + """Return True if the path contains one of the special names reserved + by the system, if any.""" + return self._flavour.is_reserved(self._parts) + + def match(self, path_pattern): + """ + Return True if this path matches the given pattern. + """ + cf = self._flavour.casefold + path_pattern = cf(path_pattern) + drv, root, pat_parts = self._flavour.parse_parts((path_pattern,)) + if not pat_parts: + raise ValueError("empty pattern") + if drv and drv != cf(self._drv): + return False + if root and root != cf(self._root): + return False + parts = self._cparts + if drv or root: + if len(pat_parts) != len(parts): + return False + pat_parts = pat_parts[1:] + elif len(pat_parts) > len(parts): + return False + for part, pat in zip(reversed(parts), reversed(pat_parts)): + if not fnmatch.fnmatchcase(part, pat): + return False + return True + + +class PurePosixPath(PurePath): + _flavour = _posix_flavour + __slots__ = () + + +class PureWindowsPath(PurePath): + _flavour = _windows_flavour + __slots__ = () + + +# Filesystem-accessing classes + + +class Path(PurePath): + __slots__ = ( + '_accessor', + ) + + def __new__(cls, *args, **kwargs): + if cls is Path: + cls = WindowsPath if os.name == 'nt' else PosixPath + self = cls._from_parts(args, init=False) + if not self._flavour.is_supported: + raise NotImplementedError("cannot instantiate %r on your system" + % (cls.__name__,)) + self._init() + return self + + def _init(self, + # Private non-constructor arguments + template=None, + ): + if template is not None: + self._accessor = template._accessor + else: + self._accessor = _normal_accessor + + def _make_child_relpath(self, part): + # This is an optimization used for dir walking. `part` must be + # a single part relative to this path. + parts = self._parts + [part] + return self._from_parsed_parts(self._drv, self._root, parts) + + def _opener(self, name, flags, mode=0o666): + # A stub for the opener argument to built-in open() + return self._accessor.open(self, flags, mode) + + def _raw_open(self, flags, mode=0o777): + """ + Open the file pointed by this path and return a file descriptor, + as os.open() does. + """ + return self._accessor.open(self, flags, mode) + + # Public API + + @classmethod + def cwd(cls): + """Return a new path pointing to the current working directory + (as returned by os.getcwd()). + """ + return cls(os.getcwd()) + + def iterdir(self): + """Iterate over the files in this directory. Does not yield any + result for the special paths '.' and '..'. + """ + for name in self._accessor.listdir(self): + if name in ('.', '..'): + # Yielding a path object for these makes little sense + continue + yield self._make_child_relpath(name) + + def glob(self, pattern): + """Iterate over this subtree and yield all existing files (of any + kind, including directories) matching the given pattern. + """ + pattern = self._flavour.casefold(pattern) + drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) + if drv or root: + raise NotImplementedError("Non-relative patterns are unsupported") + selector = _make_selector(tuple(pattern_parts)) + for p in selector.select_from(self): + yield p + + def rglob(self, pattern): + """Recursively yield all existing files (of any kind, including + directories) matching the given pattern, anywhere in this subtree. + """ + pattern = self._flavour.casefold(pattern) + drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) + if drv or root: + raise NotImplementedError("Non-relative patterns are unsupported") + selector = _make_selector(("**",) + tuple(pattern_parts)) + for p in selector.select_from(self): + yield p + + def absolute(self): + """Return an absolute version of this path. This function works + even if the path doesn't point to anything. + + No normalization is done, i.e. all '.' and '..' will be kept along. + Use resolve() to get the canonical path to a file. + """ + # XXX untested yet! + if self.is_absolute(): + return self + # FIXME this must defer to the specific flavour (and, under Windows, + # use nt._getfullpathname()) + obj = self._from_parts([os.getcwd()] + self._parts, init=False) + obj._init(template=self) + return obj + + def resolve(self): + """ + Make the path absolute, resolving all symlinks on the way and also + normalizing it (for example turning slashes into backslashes under + Windows). + """ + s = self._flavour.resolve(self) + if s is None: + # No symlink resolution => for consistency, raise an error if + # the path doesn't exist or is forbidden + self.stat() + s = str(self.absolute()) + # Now we have no symlinks in the path, it's safe to normalize it. + normed = self._flavour.pathmod.normpath(s) + obj = self._from_parts((normed,), init=False) + obj._init(template=self) + return obj + + def stat(self): + """ + Return the result of the stat() system call on this path, like + os.stat() does. + """ + return self._accessor.stat(self) + + def owner(self): + """ + Return the login name of the file owner. + """ + import pwd + return pwd.getpwuid(self.stat().st_uid).pw_name + + def group(self): + """ + Return the group name of the file gid. + """ + import grp + return grp.getgrgid(self.stat().st_gid).gr_name + + def open(self, mode='r', buffering=-1, encoding=None, + errors=None, newline=None): + """ + Open the file pointed by this path and return a file object, as + the built-in open() function does. + """ + if sys.version_info >= (3, 3): + return io.open(str(self), mode, buffering, encoding, errors, newline, + opener=self._opener) + else: + return io.open(str(self), mode, buffering, encoding, errors, newline) + + def touch(self, mode=0o666, exist_ok=True): + """ + Create this file with the given access mode, if it doesn't exist. + """ + if exist_ok: + # First try to bump modification time + # Implementation note: GNU touch uses the UTIME_NOW option of + # the utimensat() / futimens() functions. + t = time.time() + try: + self._accessor.utime(self, (t, t)) + except OSError: + # Avoid exception chaining + pass + else: + return + flags = os.O_CREAT | os.O_WRONLY + if not exist_ok: + flags |= os.O_EXCL + fd = self._raw_open(flags, mode) + os.close(fd) + + def mkdir(self, mode=0o777, parents=False): + if not parents: + self._accessor.mkdir(self, mode) + else: + try: + self._accessor.mkdir(self, mode) + except OSError as e: + if e.errno != ENOENT: + raise + self.parent.mkdir(parents=True) + self._accessor.mkdir(self, mode) + + def chmod(self, mode): + """ + Change the permissions of the path, like os.chmod(). + """ + self._accessor.chmod(self, mode) + + def lchmod(self, mode): + """ + Like chmod(), except if the path points to a symlink, the symlink's + permissions are changed, rather than its target's. + """ + self._accessor.lchmod(self, mode) + + def unlink(self): + """ + Remove this file or link. + If the path is a directory, use rmdir() instead. + """ + self._accessor.unlink(self) + + def rmdir(self): + """ + Remove this directory. The directory must be empty. + """ + self._accessor.rmdir(self) + + def lstat(self): + """ + Like stat(), except if the path points to a symlink, the symlink's + status information is returned, rather than its target's. + """ + return self._accessor.lstat(self) + + def rename(self, target): + """ + Rename this path to the given path. + """ + self._accessor.rename(self, target) + + def replace(self, target): + """ + Rename this path to the given path, clobbering the existing + destination if it exists. + """ + if sys.version_info < (3, 3): + raise NotImplementedError("replace() is only available " + "with Python 3.3 and later") + self._accessor.replace(self, target) + + def symlink_to(self, target, target_is_directory=False): + """ + Make this path a symlink pointing to the given path. + Note the order of arguments (self, target) is the reverse of os.symlink's. + """ + self._accessor.symlink(target, self, target_is_directory) + + # Convenience functions for querying the stat results + + def exists(self): + """ + Whether this path exists. + """ + try: + self.stat() + except OSError as e: + if e.errno != ENOENT: + raise + return False + return True + + def is_dir(self): + """ + Whether this path is a directory. + """ + try: + return S_ISDIR(self.stat().st_mode) + except OSError as e: + if e.errno != ENOENT: + raise + # Path doesn't exist or is a broken symlink + # (see https://bitbucket.org/pitrou/pathlib/issue/12/) + return False + + def is_file(self): + """ + Whether this path is a regular file (also True for symlinks pointing + to regular files). + """ + try: + return S_ISREG(self.stat().st_mode) + except OSError as e: + if e.errno != ENOENT: + raise + # Path doesn't exist or is a broken symlink + # (see https://bitbucket.org/pitrou/pathlib/issue/12/) + return False + + def is_symlink(self): + """ + Whether this path is a symbolic link. + """ + try: + return S_ISLNK(self.lstat().st_mode) + except OSError as e: + if e.errno != ENOENT: + raise + # Path doesn't exist + return False + + def is_block_device(self): + """ + Whether this path is a block device. + """ + try: + return S_ISBLK(self.stat().st_mode) + except OSError as e: + if e.errno != ENOENT: + raise + # Path doesn't exist or is a broken symlink + # (see https://bitbucket.org/pitrou/pathlib/issue/12/) + return False + + def is_char_device(self): + """ + Whether this path is a character device. + """ + try: + return S_ISCHR(self.stat().st_mode) + except OSError as e: + if e.errno != ENOENT: + raise + # Path doesn't exist or is a broken symlink + # (see https://bitbucket.org/pitrou/pathlib/issue/12/) + return False + + def is_fifo(self): + """ + Whether this path is a FIFO. + """ + try: + return S_ISFIFO(self.stat().st_mode) + except OSError as e: + if e.errno != ENOENT: + raise + # Path doesn't exist or is a broken symlink + # (see https://bitbucket.org/pitrou/pathlib/issue/12/) + return False + + def is_socket(self): + """ + Whether this path is a socket. + """ + try: + return S_ISSOCK(self.stat().st_mode) + except OSError as e: + if e.errno != ENOENT: + raise + # Path doesn't exist or is a broken symlink + # (see https://bitbucket.org/pitrou/pathlib/issue/12/) + return False + + +class PosixPath(Path, PurePosixPath): + __slots__ = () + +class WindowsPath(Path, PureWindowsPath): + __slots__ = () + diff --git a/lib/pathspec-1.0.4.dist-info/INSTALLER b/lib/pathspec-1.0.4.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/lib/pathspec-1.0.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/lib/pathspec-1.0.4.dist-info/METADATA b/lib/pathspec-1.0.4.dist-info/METADATA new file mode 100644 index 0000000..348a68b --- /dev/null +++ b/lib/pathspec-1.0.4.dist-info/METADATA @@ -0,0 +1,356 @@ +Metadata-Version: 2.4 +Name: pathspec +Version: 1.0.4 +Summary: Utility library for gitignore style pattern matching of file paths. +Author-email: "Caleb P. Burns" +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Utilities +License-File: LICENSE +Requires-Dist: hyperscan >=0.7 ; extra == "hyperscan" +Requires-Dist: typing-extensions >=4 ; extra == "optional" +Requires-Dist: google-re2 >=1.1 ; extra == "re2" +Requires-Dist: pytest >=9 ; extra == "tests" +Requires-Dist: typing-extensions >=4.15 ; extra == "tests" +Project-URL: Documentation, https://python-path-specification.readthedocs.io/en/latest/index.html +Project-URL: Issue Tracker, https://github.com/cpburnz/python-pathspec/issues +Project-URL: Source Code, https://github.com/cpburnz/python-pathspec +Provides-Extra: hyperscan +Provides-Extra: optional +Provides-Extra: re2 +Provides-Extra: tests + + +PathSpec +======== + +*pathspec* is a utility library for pattern matching of file paths. So far this +only includes Git's `gitignore`_ pattern matching. + +.. _`gitignore`: http://git-scm.com/docs/gitignore + + +Tutorial +-------- + +Say you have a "Projects" directory and you want to back it up, but only +certain files, and ignore others depending on certain conditions:: + + >>> from pathspec import PathSpec + >>> # The gitignore-style patterns for files to select, but we're including + >>> # instead of ignoring. + >>> spec_text = """ + ... + ... # This is a comment because the line begins with a hash: "#" + ... + ... # Include several project directories (and all descendants) relative to + ... # the current directory. To reference only a directory you must end with a + ... # slash: "/" + ... /project-a/ + ... /project-b/ + ... /project-c/ + ... + ... # Patterns can be negated by prefixing with exclamation mark: "!" + ... + ... # Ignore temporary files beginning or ending with "~" and ending with + ... # ".swp". + ... !~* + ... !*~ + ... !*.swp + ... + ... # These are python projects so ignore compiled python files from + ... # testing. + ... !*.pyc + ... + ... # Ignore the build directories but only directly under the project + ... # directories. + ... !/*/build/ + ... + ... """ + +The ``PathSpec`` class provides an abstraction around pattern implementations, +and we want to compile our patterns as "gitignore" patterns. You could call it a +wrapper for a list of compiled patterns:: + + >>> spec = PathSpec.from_lines('gitignore', spec_text.splitlines()) + +If we wanted to manually compile the patterns, we can use the ``GitIgnoreBasicPattern`` +class directly. It is used in the background for "gitignore" which internally +converts patterns to regular expressions:: + + >>> from pathspec.patterns.gitignore.basic import GitIgnoreBasicPattern + >>> patterns = map(GitIgnoreBasicPattern, spec_text.splitlines()) + >>> spec = PathSpec(patterns) + +``PathSpec.from_lines()`` is a class method which simplifies that. + +If you want to load the patterns from file, you can pass the file object +directly as well:: + + >>> with open('patterns.list', 'r') as fh: + >>> spec = PathSpec.from_lines('gitignore', fh) + +You can perform matching on a whole directory tree with:: + + >>> matches = set(spec.match_tree_files('path/to/directory')) + +Or you can perform matching on a specific set of file paths with:: + + >>> matches = set(spec.match_files(file_paths)) + +Or check to see if an individual file matches:: + + >>> is_matched = spec.match_file(file_path) + +There's actually two implementations of "gitignore". The basic implementation is +used by ``PathSpec`` and follows patterns as documented by `gitignore`_. +However, Git's behavior differs from the documented patterns. There's some +edge-cases, and in particular, Git allows including files from excluded +directories which appears to contradict the documentation. ``GitIgnoreSpec`` +handles these cases to more closely replicate Git's behavior:: + + >>> from pathspec import GitIgnoreSpec + >>> spec = GitIgnoreSpec.from_lines(spec_text.splitlines()) + +You do not specify the style of pattern for ``GitIgnoreSpec`` because it should +always use ``GitIgnoreSpecPattern`` internally. + + +Performance +----------- + +Running lots of regular expression matches against thousands of files in Python +is slow. Alternate regular expression backends can be used to improve +performance. ``PathSpec`` and ``GitIgnoreSpec`` both accept a ``backend`` +parameter to control the backend. The default is "best" to automatically choose +the best available backend. There are currently 3 backends. + +The "simple" backend is the default and it simply uses Python's ``re.Pattern`` +objects that are normally created. This can be the fastest when there's only 1 +or 2 patterns. + +The "hyperscan" backend uses the `hyperscan`_ library. Hyperscan tends to be at +least 2 times faster than "simple", and generally slower than "re2". This can be +faster than "re2" under the right conditions with pattern counts of 1-25. + +The "re2" backend uses the `google-re2`_ library (not to be confused with the +*re2* library on PyPI which is unrelated and abandoned). Google's re2 tends to +be significantly faster than "simple", and 3 times faster than "hyperscan" at +high pattern counts. + +See `benchmarks_backends.md`_ for comparisons between native Python regular +expressions and the optional backends. + + +.. _`benchmarks_backends.md`: https://github.com/cpburnz/python-pathspec/blob/master/benchmarks_backends.md +.. _`google-re2`: https://pypi.org/project/google-re2/ +.. _`hyperscan`: https://pypi.org/project/hyperscan/ + + +FAQ +--- + + +1. How do I ignore files like *.gitignore*? ++++++++++++++++++++++++++++++++++++++++++++ + +``GitIgnoreSpec`` (and ``PathSpec``) positively match files by default. To find +the files to keep, and exclude files like *.gitignore*, you need to set +``negate=True`` to flip the results:: + + >>> from pathspec import GitIgnoreSpec + >>> spec = GitIgnoreSpec.from_lines([...]) + >>> keep_files = set(spec.match_tree_files('path/to/directory', negate=True)) + >>> ignore_files = set(spec.match_tree_files('path/to/directory')) + + +License +------- + +*pathspec* is licensed under the `Mozilla Public License Version 2.0`_. See +`LICENSE`_ or the `FAQ`_ for more information. + +In summary, you may use *pathspec* with any closed or open source project +without affecting the license of the larger work so long as you: + +- give credit where credit is due, + +- and release any custom changes made to *pathspec*. + +.. _`Mozilla Public License Version 2.0`: http://www.mozilla.org/MPL/2.0 +.. _`LICENSE`: LICENSE +.. _`FAQ`: http://www.mozilla.org/MPL/2.0/FAQ.html + + +Source +------ + +The source code for *pathspec* is available from the GitHub repo +`cpburnz/python-pathspec`_. + +.. _`cpburnz/python-pathspec`: https://github.com/cpburnz/python-pathspec + + +Installation +------------ + +*pathspec* is available for install through `PyPI`_:: + + pip install pathspec + +*pathspec* can also be built from source. The following packages will be +required: + +- `build`_ (>=0.6.0) + +*pathspec* can then be built and installed with:: + + python -m build + pip install dist/pathspec-*-py3-none-any.whl + +The following optional dependencies can be installed: + +- `google-re2`_: Enables optional "re2" backend. +- `hyperscan`_: Enables optional "hyperscan" backend. +- `typing-extensions`_: Improves some type hints. + +.. _`PyPI`: http://pypi.python.org/pypi/pathspec +.. _`build`: https://pypi.org/project/build/ +.. _`typing-extensions`: https://pypi.org/project/typing-extensions/ + + +Documentation +------------- + +Documentation for *pathspec* is available on `Read the Docs`_. + +The full change history can be found in `CHANGES.rst`_ and `Change History`_. + +An upgrade guide is available in `UPGRADING.rst`_ and `Upgrade Guide`_. + +.. _`CHANGES.rst`: https://github.com/cpburnz/python-pathspec/blob/master/CHANGES.rst +.. _`Change History`: https://python-path-specification.readthedocs.io/en/stable/changes.html +.. _`Read the Docs`: https://python-path-specification.readthedocs.io +.. _`UPGRADING.rst`: https://github.com/cpburnz/python-pathspec/blob/master/UPGRADING.rst +.. _`Upgrade Guide`: https://python-path-specification.readthedocs.io/en/stable/upgrading.html + + +Other Languages +--------------- + +The related project `pathspec-ruby`_ (by *highb*) provides a similar library as +a `Ruby gem`_. + +.. _`pathspec-ruby`: https://github.com/highb/pathspec-ruby +.. _`Ruby gem`: https://rubygems.org/gems/pathspec + + +Change History +============== + + +1.0.4 (2026-01-26) +------------------ + +- `Issue #103`_: Using re2 fails if pyre2 is also installed. + +.. _`Issue #103`: https://github.com/cpburnz/python-pathspec/issues/103 + + +1.0.3 (2026-01-09) +------------------ + +Bug fixes: + +- `Issue #101`_: pyright strict errors with pathspec >= 1.0.0. +- `Issue #102`_: No module named 'tomllib'. + + +.. _`Issue #101`: https://github.com/cpburnz/python-pathspec/issues/101 +.. _`Issue #102`: https://github.com/cpburnz/python-pathspec/issues/102 + + +1.0.2 (2026-01-07) +------------------ + +Bug fixes: + +- Type hint `collections.abc.Callable` does not properly replace `typing.Callable` until Python 3.9.2. + + +1.0.1 (2026-01-06) +------------------ + +Bug fixes: + +- `Issue #100`_: ValueError(f"{patterns=!r} cannot be empty.") when using black. + + +.. _`Issue #100`: https://github.com/cpburnz/python-pathspec/issues/100 + + +1.0.0 (2026-01-05) +------------------ + +Major changes: + +- `Issue #91`_: Dropped support of EoL Python 3.8. +- Added concept of backends to allow for faster regular expression matching. The backend can be controlled using the `backend` argument to `PathSpec()`, `PathSpec.from_lines()`, `GitIgnoreSpec()`, and `GitIgnoreSpec.from_lines()`. +- Renamed "gitwildmatch" pattern back to "gitignore". The "gitignore" pattern behaves slightly differently when used with `PathSpec` (*gitignore* as documented) than with `GitIgnoreSpec` (replicates *Git*'s edge cases). + +API changes: + +- Breaking: protected method `pathspec.pathspec.PathSpec._match_file()` (with a leading underscore) has been removed and replaced by backends. This does not affect normal usage of `PathSpec` or `GitIgnoreSpec`. Only custom subclasses will be affected. If this breaks your usage, let me know by `opening an issue `_. +- Deprecated: "gitwildmatch" is now an alias for "gitignore". +- Deprecated: `pathspec.patterns.GitWildMatchPattern` is now an alias for `pathspec.patterns.gitignore.spec.GitIgnoreSpecPattern`. +- Deprecated: `pathspec.patterns.gitwildmatch` module has been replaced by the `pathspec.patterns.gitignore` package. +- Deprecated: `pathspec.patterns.gitwildmatch.GitWildMatchPattern` is now an alias for `pathspec.patterns.gitignore.spec.GitIgnoreSpecPattern`. +- Deprecated: `pathspec.patterns.gitwildmatch.GitWildMatchPatternError` is now an alias for `pathspec.patterns.gitignore.GitIgnorePatternError`. +- Removed: `pathspec.patterns.gitwildmatch.GitIgnorePattern` has been deprecated since v0.4 (2016-07-15). +- Signature of method `pathspec.pattern.RegexPattern.match_file()` has been changed from `def match_file(self, file: str) -> RegexMatchResult | None` to `def match_file(self, file: AnyStr) -> RegexMatchResult | None` to reflect usage. +- Signature of class method `pathspec.pattern.RegexPattern.pattern_to_regex()` has been changed from `def pattern_to_regex(cls, pattern: str) -> tuple[str, bool]` to `def pattern_to_regex(cls, pattern: AnyStr) -> tuple[AnyStr | None, bool | None]` to reflect usage and documentation. + +New features: + +- Added optional "hyperscan" backend using `hyperscan`_ library. It will automatically be used when installed. This dependency can be installed with ``pip install 'pathspec[hyperscan]'``. +- Added optional "re2" backend using the `google-re2`_ library. It will automatically be used when installed. This dependency can be installed with ``pip install 'pathspec[re2]'``. +- Added optional dependency on `typing-extensions`_ library to improve some type hints. + +Bug fixes: + +- `Issue #93`_: Do not remove leading spaces. +- `Issue #95`_: Matching for files inside folder does not seem to behave like .gitignore's. +- `Issue #98`_: UnboundLocalError in RegexPattern when initialized with `pattern=None`. +- Type hint on return value of `pathspec.pattern.RegexPattern.match_file()` to match documentation. + +Improvements: + +- Mark Python 3.13 and 3.14 as supported. +- No-op patterns are now filtered out when matching files, slightly improving performance. +- Fix performance regression in `iter_tree_files()` from v0.10. + + +.. _`Issue #38`: https://github.com/cpburnz/python-pathspec/issues/38 +.. _`Issue #91`: https://github.com/cpburnz/python-pathspec/issues/91 +.. _`Issue #93`: https://github.com/cpburnz/python-pathspec/issues/93 +.. _`Issue #95`: https://github.com/cpburnz/python-pathspec/issues/95 +.. _`Issue #98`: https://github.com/cpburnz/python-pathspec/issues/98 +.. _`google-re2`: https://pypi.org/project/google-re2/ +.. _`hyperscan`: https://pypi.org/project/hyperscan/ +.. _`typing-extensions`: https://pypi.org/project/typing-extensions/ + diff --git a/lib/pathspec-1.0.4.dist-info/RECORD b/lib/pathspec-1.0.4.dist-info/RECORD new file mode 100644 index 0000000..3445cad --- /dev/null +++ b/lib/pathspec-1.0.4.dist-info/RECORD @@ -0,0 +1,69 @@ +pathspec-1.0.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pathspec-1.0.4.dist-info/METADATA,sha256=pekHVZjpp_VHVlDo7U032-fIhSGEbY_V8jjmYrEgaWM,13755 +pathspec-1.0.4.dist-info/RECORD,, +pathspec-1.0.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pathspec-1.0.4.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +pathspec-1.0.4.dist-info/licenses/LICENSE,sha256=-rPda9qyJvHAhjCx3ZF-Efy07F4eAg4sFvg6ChOGPoU,16726 +pathspec/__init__.py,sha256=0PnZCecVo4UjsfA0EFGsAUikyz1jSDFmQP9gCoKXW_Y,1408 +pathspec/__pycache__/__init__.cpython-314.pyc,, +pathspec/__pycache__/_meta.cpython-314.pyc,, +pathspec/__pycache__/_typing.cpython-314.pyc,, +pathspec/__pycache__/_version.cpython-314.pyc,, +pathspec/__pycache__/backend.cpython-314.pyc,, +pathspec/__pycache__/gitignore.cpython-314.pyc,, +pathspec/__pycache__/pathspec.cpython-314.pyc,, +pathspec/__pycache__/pattern.cpython-314.pyc,, +pathspec/__pycache__/util.cpython-314.pyc,, +pathspec/_backends/__init__.py,sha256=CjgX4uSPMC5UH4iy_IrdFXrcLQ_gwK8MKW5Qbspz_uE,130 +pathspec/_backends/__pycache__/__init__.cpython-314.pyc,, +pathspec/_backends/__pycache__/_utils.cpython-314.pyc,, +pathspec/_backends/__pycache__/agg.cpython-314.pyc,, +pathspec/_backends/_utils.py,sha256=mDjbGpndOyVkt9Fue0WDWKTkk-jVqOejof9Bv9pzArE,1066 +pathspec/_backends/agg.py,sha256=naHFqYXMR53hwtgHtEHrwNJEBFpbUWbdMbF0zguxHlE,2505 +pathspec/_backends/hyperscan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pathspec/_backends/hyperscan/__pycache__/__init__.cpython-314.pyc,, +pathspec/_backends/hyperscan/__pycache__/_base.cpython-314.pyc,, +pathspec/_backends/hyperscan/__pycache__/base.cpython-314.pyc,, +pathspec/_backends/hyperscan/__pycache__/gitignore.cpython-314.pyc,, +pathspec/_backends/hyperscan/__pycache__/pathspec.cpython-314.pyc,, +pathspec/_backends/hyperscan/_base.py,sha256=b8E_kClW6Wtkdserr3qZzMPWVomrI4yhfxSlGVYdT3c,1719 +pathspec/_backends/hyperscan/base.py,sha256=BclDnsbCH6Fvx58YT6wqxGDcfWKNUQAcy_9jV63WkCI,563 +pathspec/_backends/hyperscan/gitignore.py,sha256=OyqtXEoZWrMB3Uh_2xNzY0aGK5UdBBjkFeGAFKQh7Oo,6761 +pathspec/_backends/hyperscan/pathspec.py,sha256=74RsGQt9x3nTxjz5S5grEQI34x8eFew78wluiIzhOpw,6500 +pathspec/_backends/re2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pathspec/_backends/re2/__pycache__/__init__.cpython-314.pyc,, +pathspec/_backends/re2/__pycache__/_base.cpython-314.pyc,, +pathspec/_backends/re2/__pycache__/base.cpython-314.pyc,, +pathspec/_backends/re2/__pycache__/gitignore.cpython-314.pyc,, +pathspec/_backends/re2/__pycache__/pathspec.cpython-314.pyc,, +pathspec/_backends/re2/_base.py,sha256=VDThfjwEOnrDOfri_EnPifXH8pOYt71nxq3tUQAScfU,2149 +pathspec/_backends/re2/base.py,sha256=0sCZzhDpvyZLg9imO7BdE9KOmy3L0mgyHuzPhHWNbRU,462 +pathspec/_backends/re2/gitignore.py,sha256=0RPjCzg1vxE_6qDOL29V4qAyi9UnMKT2bb3k2XDimew,5094 +pathspec/_backends/re2/pathspec.py,sha256=aUtY_DdVHQyxHMbMGiovmXTIpuLKgIAeGtZerMVHIhI,4871 +pathspec/_backends/simple/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pathspec/_backends/simple/__pycache__/__init__.cpython-314.pyc,, +pathspec/_backends/simple/__pycache__/gitignore.cpython-314.pyc,, +pathspec/_backends/simple/__pycache__/pathspec.cpython-314.pyc,, +pathspec/_backends/simple/gitignore.py,sha256=45SfH2SM-YF7CppdSrQ15z7A4GUAesFzLWs8QaKdER4,2865 +pathspec/_backends/simple/pathspec.py,sha256=Zzebst2evN8-juZr5w6VBwIox7LToYT4K2zD4Jp3M7U,2095 +pathspec/_meta.py,sha256=3sxdG_ghfAmwhV7AGeJS9VUZptsmaBFVSPhQqVLpiMk,2937 +pathspec/_typing.py,sha256=xega7efBH3B4StmBzxpGvrk-yJWYKnD6Lk5Id0IiHzc,1642 +pathspec/_version.py,sha256=iV7XOjXu_8FpfpC966oeh6PC-5XA35XwWlO7oI-p2ys,64 +pathspec/backend.py,sha256=QXFus8SgZ1hKH8LZ8eOnZcyGNTO1_YQYwRM_kTkvi2M,1161 +pathspec/gitignore.py,sha256=oFWfSgeecaJFSCgI0TwdYxz0jluQxztgf-T779OxIN8,5263 +pathspec/pathspec.py,sha256=5JhgxfZTyzUcG0bEUN91xTdcvF_S9sdhXGK59nIpDOY,15151 +pathspec/pattern.py,sha256=smqkNSWc9LmPZS1MqYBGjXFXZRteiSpwF8iAy9250DY,6695 +pathspec/patterns/__init__.py,sha256=6pfTpyrSIJxN8A12hKWpa9JFvVMTR39FV3QE1HBQbho,404 +pathspec/patterns/__pycache__/__init__.cpython-314.pyc,, +pathspec/patterns/__pycache__/gitwildmatch.cpython-314.pyc,, +pathspec/patterns/gitignore/__init__.py,sha256=MaSAZd0DDg0vCH9k1LslaJjBJw5DkX4ty-FuLmB1z_4,422 +pathspec/patterns/gitignore/__pycache__/__init__.cpython-314.pyc,, +pathspec/patterns/gitignore/__pycache__/base.cpython-314.pyc,, +pathspec/patterns/gitignore/__pycache__/basic.cpython-314.pyc,, +pathspec/patterns/gitignore/__pycache__/spec.cpython-314.pyc,, +pathspec/patterns/gitignore/base.py,sha256=mkLYm-prSD2SXNDpxnFhL0FRV8FRPAsIBVeXyNOWjCI,4688 +pathspec/patterns/gitignore/basic.py,sha256=0pTlzzJt8qMpy-SnGHhozZVWVDH9ErPDy29MV3Q8UOw,9924 +pathspec/patterns/gitignore/spec.py,sha256=8jB3Q7Wbb6fLvtIfNax89tEtw2UZgATbAKnpGQleU8Q,10186 +pathspec/patterns/gitwildmatch.py,sha256=bF2PUtc9gOFHuFwHJ035x91y3R8An5dIY5oRibylsco,1463 +pathspec/py.typed,sha256=wq7wwDeyBungK6DsiV4O-IujgKzARwHz94uQshdpdEU,68 +pathspec/util.py,sha256=KbG9seqfTOBLPoSJ8I4CdeDFVof6rDGCMy69cZb4Du4,24728 diff --git a/lib/pathspec-1.0.4.dist-info/REQUESTED b/lib/pathspec-1.0.4.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/lib/pathspec-1.0.4.dist-info/WHEEL b/lib/pathspec-1.0.4.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/lib/pathspec-1.0.4.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/lib/pathspec-1.0.4.dist-info/licenses/LICENSE b/lib/pathspec-1.0.4.dist-info/licenses/LICENSE new file mode 100644 index 0000000..14e2f77 --- /dev/null +++ b/lib/pathspec-1.0.4.dist-info/licenses/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/lib/pathspec/__init__.py b/lib/pathspec/__init__.py new file mode 100644 index 0000000..f41cfee --- /dev/null +++ b/lib/pathspec/__init__.py @@ -0,0 +1,68 @@ +""" +The *pathspec* package provides pattern matching for file paths. So far this +only includes Git's *gitignore* patterns. + +The following classes are imported and made available from the root of the +`pathspec` package: + +- :class:`pathspec.gitignore.GitIgnoreSpec` + +- :class:`pathspec.pathspec.PathSpec` + +- :class:`pathspec.pattern.Pattern` + +- :class:`pathspec.pattern.RegexPattern` + +- :class:`pathspec.util.RecursionError` + +The following functions are also imported: + +- :func:`pathspec.util.lookup_pattern` + +The following deprecated functions are also imported to maintain backward +compatibility: + +- :func:`pathspec.util.iter_tree` + +- :func:`pathspec.util.match_files` +""" + +from .gitignore import ( + GitIgnoreSpec) +from .pathspec import ( + PathSpec) +from .pattern import ( + Pattern, + RegexPattern) +from .util import ( + RecursionError, + iter_tree, # Deprecated since 0.10.0. + lookup_pattern, + match_files) # Deprecated since 0.10.0. + +from ._meta import ( + __author__, + __copyright__, + __credits__, + __license__) +from ._version import ( + __version__) + +# Load pattern implementations. +from . import patterns + +# Declare private imports as part of the public interface. Deprecated imports +# are deliberately excluded. +__all__ = [ + 'GitIgnoreSpec', + 'PathSpec', + 'Pattern', + 'RecursionError', + 'RegexPattern', + '__author__', + '__copyright__', + '__credits__', + '__license__', + '__version__', + 'lookup_pattern', +] diff --git a/lib/pathspec/__pycache__/__init__.cpython-314.pyc b/lib/pathspec/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99abf804b4a82511fa7e0e3fc288903a5b6ab9a9 GIT binary patch literal 1446 zcma)6O>fgc5cQ@`;y7O|rBni>uEYVQs0kM&s8DYR=K?6@%yw&Ul2skAwVt%(R`uWT zSNaot<;Dr9s$QAhe2_?0TT7nItY_bQ`^NFBM%{PtI{9@e&x?-p(-iZoFiWm3FgbU6 z4mjUPk90{;fWnT+3*EKA1=r?l-C|IJQc#Ant=+B{RGYn6l}t#&CA`#!4_=oIBlm_`ApzR&wJste13_N{I5encT*0-EEVA{ zO?i022ZE+roytgLxBwt@LPs3Jp-cv}uQct;7+cJyHa%3d&ozai%zTx^6DpH19-FNn zCA`e&?m&VZBuWc|Zo#s)Z+ZJFj@6j~4P%~VxP@y${& zYJ^mRYNddxzRCTgx%?v}u=4kPe~(VDeG9$6=xHzHv{7Hz3OmH3AKkHKj^F-*M}9!F zG0gOu-+ORvYflWs_XqTkLB{Q#2*)~;D*2$b(g<%R_}|I?I0*rtPQB!DrWS@SyJFgw zOtHwZQYYh-;b~{WSCJ!;Y7ufXnL9`*C>)wh0B?GX<2d6w@njt*1rJTN18WuX^3348wrqFH%^RzZ=9LA1-dMFHuP!m- zPM9IM1P?=NJ1;X9sSwF3ix=F2*M7I&*;?#$+PVy>>duz4s#(jbm}3pCb^#J3GMedyNqCr|^up*ZYL5 pU%P}Pe|k1bIA5zuU0Kwl=V}D_y>rZwf6u3 literal 0 HcmV?d00001 diff --git a/lib/pathspec/__pycache__/_meta.cpython-314.pyc b/lib/pathspec/__pycache__/_meta.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e32873f5c8c0ef26bfa28fb02bc32e2087394d1 GIT binary patch literal 3842 zcmZ{n*>c>*6^1d8WYMH4$-BHsuojBjEF5m8B*WoiaTAve8Oc_bNe|E*V6gP0(V#iF zQt37F04ZIjQk9$J%Cv6s1TI~rDtEc+lwTl!_mEDds)tp7%{M$h8r`Q)A7G}gj1F7) zi~jwOus32^|59Jh|LimP$u3#ebE{zqt5FiAhAr%h)wr-*YFvaaL2p2pp&{r^Xc!uS z-hxJmFF612EbeWvwC(JKX+Wv-Fc6}f-LF>Lp$PI;#bZg;CqWz% zx45^$N$RuI*}#}aXMjMk*J9&wUUZuI)faHQjy%W6P)Of%Og|6jCoMx$cz>a?wb&IiePTNj{F<%%*ZF3cZ?@`Qnm^z0QqwPp= zb`)aiV=LIWM>NKU7>(Utgu{kCLoo*|M&>tI9OG0C@TUP>{Wu2S-g~^fySchy9Qr(n zy;T|5AU$)U5bUs2h2h-H2=QtQGYiHce~!Av%mHnYOoRk`^^DS^BjW}CGd;tpYA_sc zRm`}^JQw9SH(=))s7dF5#?gRmaKtkm#^_rtz+BWJp#$t4< z{_(w-2F^b3b#YWo4ViUy3~73tCn$WQDb}IcMO4dn&V{q%Z4Y<}i4ab_h~j}aw#HgS z^b&J{o(Iunu?&W1`7Vh_LU?YR0oDUgdS-Ep`LiB|+WtC=@boIoCHb>WxiEcHj%r5K zH{PX1_;I;I>j^{^m#gE2dnJw&fG=RFZBq+XPg9-dv!Hp>L_f&piKIi@^hc)VTS*?;WRE4AH;ORq0Cn?CcJ z&DX=tCdoy>IC!+#^v+*8@pS}Wk&;kk{V#Ed_n?e6n=h@OQ1aI6`Km~|)o$1=3xY;2 z%1ffxW&V7v8i%cFw})ZVYL|#0>r$`UR4=@hZtwNb!~Scr%hfLm{9>@OKjIgJyih8Y z{$tyvi*m@ia^v~%kJlETk4pQ&Rr|-%q`ZL7f8z5JKII#hJti+J8nWzJ`KF>_%dW@~ zMQ>U5j2u;TMMbYFx@Orm`L?2WlwFqYF-1^<=2`YfopPI^9J~~+|;}UF3WArkHOP&NAoVYBKI`!YyPF?2Dl~aYgUga?J_1k5 zw&no5C_~L1@RE!)$J(E0PQkO1Y3^$NgJuq%mYHS&o{_oY=Ogl@WL>@fVuTN&NdF4} CQRoK% literal 0 HcmV?d00001 diff --git a/lib/pathspec/__pycache__/_typing.cpython-314.pyc b/lib/pathspec/__pycache__/_typing.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ba3631aab64e75f881bba75709b6f5303c0258e7 GIT binary patch literal 3454 zcmbVO%}*Rh7Jt>>3^M}^1oB}_6V{j^cqWn^<0V!kV3UPth2y~@djJp3Ov6k(On0h# zaL8$7trYK#11tGpX*ul0QvQJ5!%Ay%%gK^sDd~;nLvqQUnqUdXzU+I|JwQHWWw)e$ z_0g|>_3C}rYw1)1uwDDl=hY81fNyBhYB(2d|26|?LIHfZ3I#UHd?tFym#~*+jCD(R^)2_w@mIg*avWWB5iqQ*oB`lLN4%4?;m-QVYhCa#k99xpf>sp>14>W`0^A zBxa?V4!7Y}ZRw6ySQLTB?}qwpI7cs-GxXb#Z!i-K}l5wwM!<*b~brWVOClv_JCgx;TA# z%(UudbJi?7LhS1#q3@XM7TyHEgk^5vVGf;Ja@*rLehi(NS<56^cT#y%y7I#5e6tPb8#bj$W$BxE7j>fz)1X=YvIycbQ zZO^mTY)<=lik37kl#tZ}*+VUnP=P`&7U=I=ypBc;WV8^cJMuOps38sAaNY7f(!s02 zc-~)k^KR8W<6C^q_Rstlw>#xGUeDL6D|uHqVcxa;is#y;d@=HjoVyt$i$&o?sN!Og zzer&CdG8=;!T}`Zo;@O8Cp$tJ`^Orjd$-HC$N#eaAoZJ2rzHbAyTdjbCBW$1?%I;K zFK=lN^uE1zT--+_IJ6dNfUYO3MJHKcF-xM1*ar)20T!gQa)Dul8nsSWUXW35(&298 z7rVQ$-+vv+udp@k5iKtb!5x`*;3QzH9J3AFVX-!OFU}%W@;)32MFbRku@mMl)|`@6 z^WMrG*VhQ`qNGczQkCvKBHNQ8b+-q_jK74x#RaezO+_j$Fo%PSvW1(Eobx?W&`;VR z%)`0Z)fXgGk-VUsRY+kq<6O+^vIR`uSyno}ms}bXBkW&6a#8se=P2{iyn2<5A%CkC z*f<#r+W^eVgBV+(Ox9;m zY9313b|_5H|6EyxlaOdXG~sJC{YmjgaqIGZweLWZ)V_Tgv=>7fg&HKgn{z_6JCIc( zB^~An{u&~96gn&AvKcuk7B%2RM998?gQw_-Np!yN$pgbAwD;VTyQAA;vUM=&ax7#& zizg9{|4%&e6C}b(Bw6;0F(d-%JpMln6DBA30Lk`XNZoBFWqy&sPr-7`DnSO%!effZgyEF2gfb`*H{o#-jNYxa z`})ay?4-~@wkObw#j;bvdPHH(hmZtXRI)f(BdUXBM@~U0UKK(6>^eQc#C?QG@G-IJ z+n%rtr^+pIy@;2fD?&T=Gzv1mi<3PnB`&(cp@hJ~?Synunldg%Z>+cYn+Qy1@fgxj zW{mAgI#c#Dz`Fho9S4%a68nH8Ovr5HA$vL0WH$Vey|ib5n!Gu^_0f*JGrwaz=*xbg zj5b-QWLVEbW`@H$OFm>L4l?KpKRPn^EdODB#z3yt{i z@FRALmWH2&X|dGFdP1o~>UkuMKKYUmQW7p;S;llTG9l_WZtm?np!)qiRr*jymZzbP zJD-Ik0z_rWP*SAiBWWZ8k)-5-guyE!IFxaU8<6X%*FaU=(k9dJS8ehX{6m|(0Cz!~ z8iu>+uE|&ByMv7Asnp~f^4)Wc=mmXhNZv&rL)taErv}vBR~W6FWHcU7XgsV>x68XZ ZZR)(Vdp^Md_YD6Ll8p=Ll9#LV-S-vgC=v85?4q@X0bwU zeoARhszOR?T4r8qu|i2kszO<6QE_H|o*q{ftD&BOo{66((=GP+_>}zQ`1o7g@$pdk z`1q9!pFvjKvd}NdEzmE>EYK}UEJ{x;(X}k8EXYqWHPFw=Owuo?EXl~v(=SLY$tW&J xP1c9$*DI*J#bE=n->!%qXb{N3#ezWM12ZEd<3}bYMwSm84E$nE+(oQFQ2@yDKjHuY literal 0 HcmV?d00001 diff --git a/lib/pathspec/__pycache__/backend.cpython-314.pyc b/lib/pathspec/__pycache__/backend.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2aaab52a774cad5026e1964b0b9577e5b7fa510b GIT binary patch literal 2026 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 a7paNm_=*}+;&-wgMLB$_^fjYi9^}7x`?=!) literal 0 HcmV?d00001 diff --git a/lib/pathspec/__pycache__/pathspec.cpython-314.pyc b/lib/pathspec/__pycache__/pathspec.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90f1eb34d79260334fb90dd0406308001a270d7a GIT binary patch literal 20242 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 D3G=ED literal 0 HcmV?d00001 diff --git a/lib/pathspec/__pycache__/util.cpython-314.pyc b/lib/pathspec/__pycache__/util.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed4a0ee5124277829647efdb75e5280015b625a5 GIT binary patch literal 34565 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 oBpu6|q@ImPQua)hB;!oSh($ literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/__pycache__/agg.cpython-314.pyc b/lib/pathspec/_backends/__pycache__/agg.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..468ba4675b4c51cc217e55453bedb4be314cabe5 GIT binary patch literal 3674 zcmds4&2QXP5P$Z1KeL<7_XpWDrY#j)+Fd9?ghHVol&&d+;#LKuv>V6!vTMb$&GXwr z5JExfp+^oJK|ULEApb?(^_AxpZ5zs zqYcys`Jmvl+E8tn4+}o0jnqc@sNj3FvD!Ev7kplOs5ZeTrpP(i&5|@lCdj$LMTL+O z(R+F6)$F-KXHhmhT7(qZE*{%7Tb==E!P421-H#r-l93}$*J=3DdsWZ!_H%v`BF#!i=J-h|ktF?QQg^H3uT%_BmC@fObbjckwZNv8u&VLYPLZt~v)O1`%D<>?j3GOs_od?j_ ziwU_`eKeW`P%&RdfWf6v7XeIXahwvZ)-|o7K|Uj(&7*zfta2*MbMT0GWLN-#=+|_Y zJY@-ree|L6t|}4k%$F;q9aCrz`DmJzUlvq~&Dv(vOxq~FLmtML!yOAAP-Tj@CxpeZnGP;qS zxcuUkvCsG2c=+ir$JR?n*R#jgfQT`c0Gh=pb`Quf zeE*Rlh6t$C)_ul=zlDScf(T5-T>2v*u!m8$6G%D3b|H`b?VoENkAfj4v)xGGUixE| z4~r^~q7)Z-8SF5LeGcS775OJ)>)D-a^3HoJYz20LfVOp5Z0WBJOclD}?aTNXuxda= zARXiKdsfyZgfHlMWi||qh#Rze6ZJaacLQz_*D>s3Y54n)P~6G+o=KVGCU`7EFQIK?*t_ z%V~=94Emx2s&sCSu$#iJqDu!x*mQ;c6(WAn8~21T(WeYsj$m+G7^AzmFM@$>O|+m$ zsvrXmX6ON%kD!3V?YVfb1bLLjvoP&~zOW7LvJ{t3u_KO+MS6-1#=~Kldjdrv=ol%1 zu!qA-Df3+TALV(5=PM%rE-=4tCL~GvMTQvP>64_MJBlQw@1|s_2TAcxMv?|lHXM^Y za>bYk?Z8%wNb066OUh=3D1~c@p_~09A5Pv(4qZ!@ZuXZxocykTe0B0p3cAF*GeE?F F{tlL3Ta5q! literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/_utils.py b/lib/pathspec/_backends/_utils.py new file mode 100644 index 0000000..77c7cd9 --- /dev/null +++ b/lib/pathspec/_backends/_utils.py @@ -0,0 +1,45 @@ +""" +This module provides private utility functions for backends. + +WARNING: The *pathspec._backends* package is not part of the public API. Its +contents and structure are likely to change. +""" + +from collections.abc import ( + Iterable) +from typing import ( + TypeVar) + +from pathspec.pattern import ( + Pattern) + +TPattern = TypeVar("TPattern", bound=Pattern) + + +def enumerate_patterns( + patterns: Iterable[TPattern], + filter: bool, + reverse: bool, +) -> list[tuple[int, TPattern]]: + """ + Enumerate the patterns. + + *patterns* (:class:`Iterable` of :class:`.Pattern`) contains the patterns. + + *filter* (:class:`bool`) is whether to remove no-op patterns (:data:`True`), + or keep them (:data:`False`). + + *reverse* (:class:`bool`) is whether to reverse the pattern order + (:data:`True`), or keep the order (:data:`True`). + + Returns the enumerated patterns (:class:`list` of :class:`tuple`). + """ + out_patterns = [ + (__i, __pat) + for __i, __pat in enumerate(patterns) + if not filter or __pat.include is not None + ] + if reverse: + out_patterns.reverse() + + return out_patterns diff --git a/lib/pathspec/_backends/agg.py b/lib/pathspec/_backends/agg.py new file mode 100644 index 0000000..c387146 --- /dev/null +++ b/lib/pathspec/_backends/agg.py @@ -0,0 +1,104 @@ +""" +This module provides aggregated private data and utilities functions about the +available backends. + +WARNING: The *pathspec._backends* package is not part of the public API. Its +contents and structure are likely to change. +""" + +from collections.abc import ( + Sequence) +from typing import ( + cast) + +from pathspec.backend import ( + BackendNamesHint, + _Backend) +from pathspec.pattern import ( + Pattern, + RegexPattern) + +from .hyperscan.base import ( + hyperscan_error) +from .hyperscan.gitignore import ( + HyperscanGiBackend) +from .hyperscan.pathspec import ( + HyperscanPsBackend) +from .re2.base import ( + re2_error) +from .re2.gitignore import ( + Re2GiBackend) +from .re2.pathspec import ( + Re2PsBackend) +from .simple.gitignore import ( + SimpleGiBackend) +from .simple.pathspec import ( + SimplePsBackend) + +_BEST_BACKEND: BackendNamesHint +""" +The best available backend. +""" + +if re2_error is None: + _BEST_BACKEND = 're2' +elif hyperscan_error is None: + _BEST_BACKEND = 'hyperscan' +else: + _BEST_BACKEND = 'simple' + + +def make_gitignore_backend( + name: BackendNamesHint, + patterns: Sequence[Pattern], +) -> _Backend: + """ + Create the specified backend with the supplied patterns for + :class:`~pathspec.gitignore.GitIgnoreSpec`. + + *name* (:class:`str`) is the name of the backend. + + *patterns* (:class:`.Iterable` of :class:`.Pattern`) contains the compiled + patterns. + + Returns the backend (:class:`._Backend`). + """ + if name == 'best': + name = _BEST_BACKEND + + if name == 'hyperscan': + return HyperscanGiBackend(cast(Sequence[RegexPattern], patterns)) + elif name == 're2': + return Re2GiBackend(cast(Sequence[RegexPattern], patterns)) + elif name == 'simple': + return SimpleGiBackend(cast(Sequence[RegexPattern], patterns)) + else: + raise ValueError(f"Backend {name=!r} is invalid.") + + +def make_pathspec_backend( + name: BackendNamesHint, + patterns: Sequence[Pattern], +) -> _Backend: + """ + Create the specified backend with the supplied patterns for + :class:`~pathspec.pathspec.PathSpec`. + + *name* (:class:`str`) is the name of the backend. + + *patterns* (:class:`Iterable` of :class:`Pattern`) contains the compiled + patterns. + + Returns the backend (:class:`._Backend`). + """ + if name == 'best': + name = _BEST_BACKEND + + if name == 'hyperscan': + return HyperscanPsBackend(cast(Sequence[RegexPattern], patterns)) + elif name == 're2': + return Re2PsBackend(cast(Sequence[RegexPattern], patterns)) + elif name == 'simple': + return SimplePsBackend(patterns) + else: + raise ValueError(f"Backend {name=!r} is invalid.") diff --git a/lib/pathspec/_backends/hyperscan/__init__.py b/lib/pathspec/_backends/hyperscan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/pathspec/_backends/hyperscan/__pycache__/__init__.cpython-314.pyc b/lib/pathspec/_backends/hyperscan/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..74e05bdac46f3f30d26cdc7c6d1e8c3d6503a78b GIT binary patch literal 175 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08C%SXQ?w?Mxj zvp}~bu_!&YMAx#UvLHXj)IdKcGfBUovLquvPro3sB%`<>HCaDCDKR-aH7})DKcliB ywWv5bF;71}J~J<~BtBlRpz;=nO>TZlX-=wL5i8JekoCnN#wTV*M#ds$APWGzK`b}` literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/hyperscan/__pycache__/_base.cpython-314.pyc b/lib/pathspec/_backends/hyperscan/__pycache__/_base.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71f3a47423e42a2f58b21c313dd6cb2f32ab094e GIT binary patch literal 1857 zcmaJ>&2QX96rb_eI$rOlNlBU|jXG5-q%G-gD~f=C3fs~&E2W`w5+Q^(j@Of|Zg`Uf5(^xEs z!YMQ@JzCQgX3M-WKFD==2J0J#^?^KBcx4ksM{kuJ<1H{vqn3UeIR(dj52}%40)akq z0y$Qzd{G_PE)EtXMqe9UglM}p`k&}yjy8)3YS{(~OoUFNQ&71u*O4gBprtDz`cR!g zA+DBo|Fl-OLr!**pc7G&vgB46P!2+P%M+9Yp76+K!idlKXx2zmaN;MifJ!*=;(&0$I=<*IN<8?Y@FtDA zL?p!D_TnvCv#K~NdNHKT3+JGY$kX|e$EBjL#c-bGObug^nTsp#!cybf$|`t_O_uD^ z*x!dDU-sd_{RL3>PzxvoG*Z@23l#{3g;oP7Kf^)?zzs(k0PYwdtNXIFYf^TAT#h-5 zs#bPvad>%GZ>Q{2PwcJ%Je17mMb2;h+r2?@hrFr|r-3{z7YUp`hmFw)Ak3{^Y*ENa zIfpaWtgH~me$)wQriXDrZ)fF@yFtiYKt@m&XIeW+BBn!iKcVHS%4AQmqMU4LGc(h5 zV{eDLZf3de0D6#Z*ZsQVMf;vn*WC;m7f~3~I01LTb%VqQVZ?QLln4$Y1RVke3=`m! z5cw|D!oqe(P>5q?_-`LRlr`&OC#|PpdQNz3i;8m}h;Et$@1Cy%-|A^sY$tJjNU{0> z#p(wXlQvFkX_t*azI-|j0)GdlZ_!gtRrF`JR#x{aJsTBA@67ZRF!d}nV&8eQrzjws zXngVkp6E*ln;K?$JwxWeeTX5AAwwLcCi$Tb`2h`DaCG2-k&Pb(nzlPzyYzpUAOnV6 z$O!-)5d@)ZP%k2ZbdxZKHr~m(ZyjLH`(Jgv=cP#IQFQKRANV^LVv~mQN*{D3M z1Y>aQij<~@Qv(Dpwf$>^`x~A7?u)(36SMLFSNihq2K!`Cbu629UFhLqrAL{-V$x z$SudO=a1iVA{LTP99(5AVQezboV359z8LT%FB!8LaB%rzf5PP9GxMVX{Vy@u%+kSs zfazsV#TY+V46MBz&y|x{zk92XVCkz!nd(Wy(@U@XHr9WwRQm|lJm8rE(#xKY rkvaVwDY*QoG`3fHWS72~>gk~V&Bvh3@3qDSbnim3@v(aE5{CI7@b&Gz literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/hyperscan/__pycache__/base.cpython-314.pyc b/lib/pathspec/_backends/hyperscan/__pycache__/base.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0ccb6c59af0fa45b28f38f9cbb38bcb6f969dbc GIT binary patch literal 786 zcmZuu&ubGw6n>Nakz_+#Yb`};$w);=rMnfeU=^_kwLy)t6g)_rZgwZh&}3(rnF#5r z7ykqQ0dHRYH@tcY>(QI1h%FSoIGfm@;K0m$-@JL>_h#OW`8f~FIQ;gB-6{Zl&B^(& zrvUws21IZSTcF_@grL!D2vNPTXKAYn1{dn~o}-;Atme$ByV?ycT@3A}7dqEXc4(~w zhGxlyWC7p`=(35s&9W}8nV2r`mY=RncZL|;wbp>4ntL?z_Pb2sK2HWciiPBFSV9%n zU5Zap0{eNp#uT`L^(RGUf^j?x4x zEeEk4NQxsfd+d<*hFEhPccZjJeXoYHN|YMON#V-S(sPl&A?h`b*3RLpXOo9FxPHV3 zY4T7?E}PlHtUf~XlUgNPCB)b#Y^p&1QYIuT5|Z%P0FRLNATNp#X?hKrtA~Q69cksH zJd>4qc%!9nKz8Geud($r7m-9wBS65HWayGUQLMH_m*{Ab>)=zDODr46{%U{sq zXldoRI`-gV@EQE?1q<0{;|i2kPoRL7M{|qEmq$yN-d2B&9m9XB3r6pqdllXnx2+HM KP3xnxZT$v^R_=5F literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/hyperscan/__pycache__/gitignore.cpython-314.pyc b/lib/pathspec/_backends/hyperscan/__pycache__/gitignore.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f10927cb9e74f8359380f11909f6aafcfbf43d7b GIT binary patch literal 8282 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 SztBoPYn3kaaGxDyp#DEENvjS3 literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/hyperscan/_base.py b/lib/pathspec/_backends/hyperscan/_base.py new file mode 100644 index 0000000..cb58f48 --- /dev/null +++ b/lib/pathspec/_backends/hyperscan/_base.py @@ -0,0 +1,78 @@ +""" +This module provides private data for the base implementation for the +:module:`hyperscan` library. + +WARNING: The *pathspec._backends.hyperscan* package is not part of the public +API. Its contents and structure are likely to change. +""" +from __future__ import annotations + +from dataclasses import ( + dataclass) +from typing import ( + Union) # Replaced by `X | Y` in 3.10. + +try: + import hyperscan +except ModuleNotFoundError: + hyperscan = None + HS_FLAGS = 0 +else: + HS_FLAGS = hyperscan.HS_FLAG_SINGLEMATCH | hyperscan.HS_FLAG_UTF8 + +HS_FLAGS: int +""" +The hyperscan flags to use: + +- HS_FLAG_SINGLEMATCH is needed to ensure the partial patterns only match once. + +- HS_FLAG_UTF8 is required to support unicode paths. +""" + + +@dataclass(frozen=True) +class HyperscanExprDat(object): + """ + The :class:`HyperscanExprDat` class is used to store data related to an + expression. + """ + + # The slots argument is not supported until Python 3.10. + __slots__ = [ + 'include', + 'index', + 'is_dir_pattern', + ] + + include: bool + """ + *include* (:class:`bool`) is whether is whether the matched files should be + included (:data:`True`), or excluded (:data:`False`). + """ + + index: int + """ + *index* (:class:`int`) is the pattern index. + """ + + is_dir_pattern: bool + """ + *is_dir_pattern* (:class:`bool`) is whether the pattern is a directory + pattern for gitignore. + """ + + +@dataclass(frozen=True) +class HyperscanExprDebug(HyperscanExprDat): + """ + The :class:`HyperscanExprDebug` class stores additional debug information + related to an expression. + """ + + # The slots argument is not supported until Python 3.10. + __slots__ = ['regex'] + + regex: Union[str, bytes] + """ + *regex* (:class:`str` or :class:`bytes`) is the regular expression. + """ diff --git a/lib/pathspec/_backends/hyperscan/base.py b/lib/pathspec/_backends/hyperscan/base.py new file mode 100644 index 0000000..ac219b4 --- /dev/null +++ b/lib/pathspec/_backends/hyperscan/base.py @@ -0,0 +1,24 @@ +""" +This module provides the base implementation for the :module:`hyperscan` +backend. + +WARNING: The *pathspec._backends.hyperscan* package is not part of the public +API. Its contents and structure are likely to change. +""" +from __future__ import annotations + +from typing import ( + Optional) + +try: + import hyperscan + hyperscan_error = None +except ModuleNotFoundError as e: + hyperscan = None + hyperscan_error = e + +hyperscan_error: Optional[ModuleNotFoundError] +""" +*hyperscan_error* (:class:`ModuleNotFoundError` or :data:`None`) is the +hyperscan import error. +""" diff --git a/lib/pathspec/_backends/hyperscan/gitignore.py b/lib/pathspec/_backends/hyperscan/gitignore.py new file mode 100644 index 0000000..2428b59 --- /dev/null +++ b/lib/pathspec/_backends/hyperscan/gitignore.py @@ -0,0 +1,245 @@ +""" +This module provides the :module:`hyperscan` backend for :class:`~pathspec.gitignore.GitIgnoreSpec`. + +WARNING: The *pathspec._backends.hyperscan* package is not part of the public +API. Its contents and structure are likely to change. +""" +from __future__ import annotations + +from collections.abc import ( + Sequence) +from typing import ( + Any, + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. + Optional, # Replaced by `X | None` in 3.10. + Union) # Replaced by `X | Y` in 3.10. + +try: + import hyperscan +except ModuleNotFoundError: + hyperscan = None + +from pathspec.pattern import ( + RegexPattern) +from pathspec.patterns.gitignore.spec import ( + GitIgnoreSpecPattern, + _BYTES_ENCODING, + _DIR_MARK_CG, + _DIR_MARK_OPT) +from pathspec._typing import ( + override) # Added in 3.12. + +from ._base import ( + HS_FLAGS, + HyperscanExprDat, + HyperscanExprDebug) +from .pathspec import ( + HyperscanPsBackend) + + +class HyperscanGiBackend(HyperscanPsBackend): + """ + The :class:`HyperscanGiBackend` class is the :module:`hyperscan` + implementation used by :class:`~pathspec.gitignore.GitIgnoreSpec`. The + Hyperscan database uses block mode for matching files. + """ + + # Change type hint. + _out: tuple[Optional[bool], int, int] + + def __init__( + self, + patterns: Sequence[RegexPattern], + *, + _debug_exprs: Optional[bool] = None, + _test_sort: Optional[Callable[[list], None]] = None, + ) -> None: + """ + Initialize the :class:`HyperscanMatcher` instance. + + *patterns* (:class:`Sequence` of :class:`.RegexPattern`) contains the + compiled patterns. + """ + super().__init__(patterns, _debug_exprs=_debug_exprs, _test_sort=_test_sort) + + self._out = (None, -1, 0) + """ + *_out* (:class:`tuple`) stores the current match: + + - *0* (:class:`bool` or :data:`None`) is the match include. + + - *1* (:class:`int`) is the match index. + + - *2* (:class:`int`) is the match priority. + """ + + @override + @staticmethod + def _init_db( + db: hyperscan.Database, + debug: bool, + patterns: list[tuple[int, RegexPattern]], + sort_ids: Optional[Callable[[list[int]], None]], + ) -> list[HyperscanExprDat]: + """ + Create the Hyperscan database from the given patterns. + + *db* (:class:`hyperscan.Hyperscan`) is the Hyperscan database. + + *debug* (:class:`bool`) is whether to include additional debugging + information for the expressions. + + *patterns* (:class:`~collections.abc.Sequence` of :class:`.RegexPattern`) + contains the patterns. + + *sort_ids* (:class:`callable` or :data:`None`) is a function used to sort + the compiled expression ids. This is used during testing to ensure the order + of expressions is not accidentally relied on. + + Returns a :class:`list` indexed by expression id (:class:`int`) to its data + (:class:`HyperscanExprDat`). + """ + # WARNING: Hyperscan raises a `hyperscan.error` exception when compiled with + # zero elements. + assert patterns, patterns + + # Prepare patterns. + expr_data: list[HyperscanExprDat] = [] + exprs: list[bytes] = [] + for pattern_index, pattern in patterns: + assert pattern.include is not None, (pattern_index, pattern) + + # Encode regex. + assert isinstance(pattern, RegexPattern), pattern + regex = pattern.regex.pattern + + use_regexes: list[tuple[Union[str, bytes], bool]] = [] + if isinstance(pattern, GitIgnoreSpecPattern): + # GitIgnoreSpecPattern uses capture groups for its directory marker but + # Hyperscan does not support capture groups. Handle this scenario. + regex_str: str + if isinstance(regex, str): + regex_str: str = regex + else: + assert isinstance(regex, bytes), regex + regex_str = regex.decode(_BYTES_ENCODING) + + if _DIR_MARK_CG in regex_str: + # Found directory marker. + if regex_str.endswith(_DIR_MARK_OPT): + # Regex has optional directory marker. Split regex into directory + # and file variants. + base_regex = regex_str[:-len(_DIR_MARK_OPT)] + use_regexes.append((f'{base_regex}/', True)) + use_regexes.append((f'{base_regex}$', False)) + else: + # Remove capture group. + base_regex = regex_str.replace(_DIR_MARK_CG, '/') + use_regexes.append((base_regex, True)) + + if not use_regexes: + # No special case for regex. + use_regexes.append((regex, False)) + + for regex, is_dir_pattern in use_regexes: + if isinstance(regex, bytes): + regex_bytes = regex + else: + assert isinstance(regex, str), regex + regex_bytes = regex.encode('utf8') + + if debug: + expr_data.append(HyperscanExprDebug( + include=pattern.include, + index=pattern_index, + is_dir_pattern=is_dir_pattern, + regex=regex, + )) + else: + expr_data.append(HyperscanExprDat( + include=pattern.include, + index=pattern_index, + is_dir_pattern=is_dir_pattern, + )) + + exprs.append(regex_bytes) + + # Sort expressions. + ids = list(range(len(exprs))) + if sort_ids is not None: + sort_ids(ids) + exprs = [exprs[__id] for __id in ids] + + # Compile patterns. + db.compile( + expressions=exprs, + ids=ids, + elements=len(exprs), + flags=HS_FLAGS, + ) + return expr_data + + @override + def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: + """ + Check the file against the patterns. + + *file* (:class:`str`) is the normalized file path to check. + + Returns a :class:`tuple` containing whether to include *file* (:class:`bool` + or :data:`None`), and the index of the last matched pattern (:class:`int` or + :data:`None`). + """ + # NOTICE: According to benchmarking, a method callback is 13% faster than + # using a closure here. + db = self._db + if self._db is None: + # Database was not initialized because there were no patterns. Return no + # match. + return (None, None) + + self._out = (None, -1, 0) + db.scan(file.encode('utf8'), match_event_handler=self.__on_match) + + out_include, out_index = self._out[:2] + if out_index == -1: + out_index = None + + return (out_include, out_index) + + @override + def __on_match( + self, + expr_id: int, + _from: int, + _to: int, + _flags: int, + _context: Any, + ) -> Optional[bool]: + """ + Called on each match. + + *expr_id* (:class:`int`) is the expression id (index) of the matched + pattern. + """ + expr_dat = self._expr_data[expr_id] + + is_dir_pattern = expr_dat.is_dir_pattern + if is_dir_pattern: + # Pattern matched by a directory pattern. + priority = 1 + else: + # Pattern matched by a file pattern. + priority = 2 + + # WARNING: Hyperscan does not guarantee matches will be produced in order! + include = expr_dat.include + index = expr_dat.index + prev_index = self._out[1] + prev_priority = self._out[2] + if ( + (include and is_dir_pattern and index > prev_index) + or (priority == prev_priority and index > prev_index) + or priority > prev_priority + ): + self._out = (include, expr_dat.index, priority) diff --git a/lib/pathspec/_backends/hyperscan/pathspec.py b/lib/pathspec/_backends/hyperscan/pathspec.py new file mode 100644 index 0000000..d55c314 --- /dev/null +++ b/lib/pathspec/_backends/hyperscan/pathspec.py @@ -0,0 +1,251 @@ +""" +This module provides the :module:`hyperscan` backend for :class:`~pathspec.pathspec.PathSpec`. + +WARNING: The *pathspec._backends.hyperscan* package is not part of the public +API. Its contents and structure are likely to change. +""" +from __future__ import annotations + +from collections.abc import ( + Sequence) +from typing import ( + Any, + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. + Optional) # Replaced by `X | None` in 3.10. + +try: + import hyperscan +except ModuleNotFoundError: + hyperscan = None + +from pathspec.backend import ( + _Backend) +from pathspec.pattern import ( + RegexPattern) +from pathspec._typing import ( + override) # Added in 3.12. + +from .._utils import ( + enumerate_patterns) + +from .base import ( + hyperscan_error) +from ._base import ( + HS_FLAGS, + HyperscanExprDat, + HyperscanExprDebug) + + +class HyperscanPsBackend(_Backend): + """ + The :class:`HyperscanPsBackend` class is the :module:`hyperscan` + implementation used by :class:`~pathspec.pathspec.PathSpec` for matching + files. The Hyperscan database uses block mode for matching files. + """ + + def __init__( + self, + patterns: Sequence[RegexPattern], + *, + _debug_exprs: Optional[bool] = None, + _test_sort: Optional[Callable[[list], None]] = None, + ) -> None: + """ + Initialize the :class:`HyperscanPsBackend` instance. + + *patterns* (:class:`Sequence` of :class:`.RegexPattern`) contains the + compiled patterns. + """ + if hyperscan is None: + raise hyperscan_error + + if patterns and not isinstance(patterns[0], RegexPattern): + raise TypeError(f"{patterns[0]=!r} must be a RegexPattern.") + + use_patterns = enumerate_patterns( + patterns, filter=True, reverse=False, + ) + + debug_exprs = bool(_debug_exprs) + if use_patterns: + db = self._make_db() + expr_data = self._init_db( + db=db, + debug=debug_exprs, + patterns=use_patterns, + sort_ids=_test_sort, + ) + else: + # WARNING: The hyperscan database cannot be initialized with zero + # patterns. + db = None + expr_data = [] + + self._db: Optional[hyperscan.Database] = db + """ + *_db* (:class:`hyperscan.Database`) is the Hyperscan database. + """ + + self._debug_exprs = debug_exprs + """ + *_debug_exprs* (:class:`bool`) is whether to include additional debugging + information for the expressions. + """ + + self._expr_data: list[HyperscanExprDat] = expr_data + """ + *_expr_data* (:class:`list`) maps expression index (:class:`int`) to + expression data (:class:`:class:`HyperscanExprDat`). + """ + + self._out: tuple[Optional[bool], int] = (None, -1) + """ + *_out* (:class:`tuple`) stores the current match: + + - *0* (:class:`bool` or :data:`None`) is the match include. + + - *1* (:class:`int`) is the match index. + """ + + self._patterns: dict[int, RegexPattern] = dict(use_patterns) + """ + *_patterns* (:class:`dict`) maps pattern index (:class:`int`) to pattern + (:class:`RegexPattern`). + """ + + @staticmethod + def _init_db( + db: hyperscan.Database, + debug: bool, + patterns: list[tuple[int, RegexPattern]], + sort_ids: Optional[Callable[[list[int]], None]], + ) -> list[HyperscanExprDat]: + """ + Initialize the Hyperscan database from the given patterns. + + *db* (:class:`hyperscan.Hyperscan`) is the Hyperscan database. + + *debug* (:class:`bool`) is whether to include additional debugging + information for the expressions. + + *patterns* (:class:`~collections.abc.Sequence` of :class:`.RegexPattern`) + contains the patterns. + + *sort_ids* (:class:`callable` or :data:`None`) is a function used to sort + the compiled expression ids. This is used during testing to ensure the order + of expressions is not accidentally relied on. + + Returns a :class:`list` indexed by expression id (:class:`int`) to its data + (:class:`HyperscanExprDat`). + """ + # WARNING: Hyperscan raises a `hyperscan.error` exception when compiled with + # zero elements. + assert patterns, patterns + + # Prepare patterns. + expr_data: list[HyperscanExprDat] = [] + exprs: list[bytes] = [] + for pattern_index, pattern in patterns: + assert pattern.include is not None, (pattern_index, pattern) + + # Encode regex. + assert isinstance(pattern, RegexPattern), pattern + regex = pattern.regex.pattern + + if isinstance(regex, bytes): + regex_bytes = regex + else: + assert isinstance(regex, str), regex + regex_bytes = regex.encode('utf8') + + if debug: + expr_data.append(HyperscanExprDebug( + include=pattern.include, + index=pattern_index, + is_dir_pattern=False, + regex=regex, + )) + else: + expr_data.append(HyperscanExprDat( + include=pattern.include, + index=pattern_index, + is_dir_pattern=False, + )) + + exprs.append(regex_bytes) + + # Sort expressions. + ids = list(range(len(exprs))) + if sort_ids is not None: + sort_ids(ids) + exprs = [exprs[__id] for __id in ids] + + # Compile patterns. + db.compile( + expressions=exprs, + ids=ids, + elements=len(exprs), + flags=HS_FLAGS, + ) + + return expr_data + + @override + def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: + """ + Check the file against the patterns. + + *file* (:class:`str`) is the normalized file path to check. + + Returns a :class:`tuple` containing whether to include *file* (:class:`bool` + or :data:`None`), and the index of the last matched pattern (:class:`int` or + :data:`None`). + """ + # NOTICE: According to benchmarking, a method callback is 20% faster than + # using a closure here. + db = self._db + if self._db is None: + # Database was not initialized because there were no patterns. Return no + # match. + return (None, None) + + self._out = (None, -1) + db.scan(file.encode('utf8'), match_event_handler=self.__on_match) + + out_include, out_index = self._out + if out_index == -1: + out_index = None + + return (out_include, out_index) + + @staticmethod + def _make_db() -> hyperscan.Database: + """ + Create the Hyperscan database. + + Returns the database (:class:`hyperscan.Database`). + """ + return hyperscan.Database(mode=hyperscan.HS_MODE_BLOCK) + + def __on_match( + self, + expr_id: int, + _from: int, + _to: int, + _flags: int, + _context: Any, + ) -> Optional[bool]: + """ + Called on each match. + + *expr_id* (:class:`int`) is the expression id (index) of the matched + pattern. + """ + # Store match. + # - WARNING: Hyperscan does not guarantee matches will be produced in order! + # Later expressions have higher priority. + expr_dat = self._expr_data[expr_id] + index = expr_dat.index + prev_index = self._out[1] + if index > prev_index: + self._out = (expr_dat.include, index) diff --git a/lib/pathspec/_backends/re2/__init__.py b/lib/pathspec/_backends/re2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/pathspec/_backends/re2/__pycache__/__init__.cpython-314.pyc b/lib/pathspec/_backends/re2/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8888f2263dfa1b7b862703eee8bf03bcbb37840f GIT binary patch literal 169 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08C%T2!|w?Mxj zvp}~bu_!&YMAx#UvLHXj)IdKcGfBUovLquvPro3sB%`<>HCaDCDKR-aH7})DzbMs6 sKR!M)FS8^*Uaz3?7Kcr4eoARhs$CH)&{&Yg#UREfW=2NFB4!{90C!a>ZU6uP literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/re2/__pycache__/_base.cpython-314.pyc b/lib/pathspec/_backends/re2/__pycache__/_base.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f774a38e74888daa5ef890c9afc1c1c2869526d1 GIT binary patch literal 2087 zcmZuy&2Jk;6rb5I+w0GKwyB$DORK7BTBiy{h=3vzXo*E}swJpGmAhGcC(e@1?lLp3 zlAJ766&x!#aN)+Oe+D^n4F_ux2#Fgfw@yVxFTB~c)0TE6&-~uUyq)*n@2%$wc^$#C z{r4{}Nh9=^5QdMI0Nmfe2-&EEt|E@kBNK6a7n!&!t;$@UL4c*IvZ``5#&T6#P4U!f znx|(_Sy+{7hG$HL>t$5W+?pJQf3crM*9HR5nQA@f=Z7{^1F3Ksu}}b^@pE z5hJ9*J=Y-&fP2s8#BgkG8=C<&_!co5HY0|+6MAHa_}u1h;16y3ViI9-gOY^}!*d(d zrh64#zrAeMsv_)leZ}`Z1{QZHu3fh9vICn+i#JIK8vHQ?L)}; zvJAwG$f)VrjLk|>dMy-5*j}W5;RC4MSl`dWg(``jMOpLug0*&Qy}DNWe1B}{edqda zlf)k%7$8-32ZKtua?BK&v&(RUn~e zN&}*4faL)edPQIpC~l_Dp;;X+MixxcBD){keE&A|kQj?`R*M@iSvHJVC(7Dp#1WNY zJb+5YZKcG67ROrKprqCIY^sypkP^nA7!^HAxqj1YJ0wzF-yyqE!DW`?QVZJ13H2kT z5d>Z&yFPz}YO^xU!X;@=EMk}*rK7ZE`SuR6tVp-4gdu=)mi2Yp_6C-sWo^2YanJRM zAAmV!Sx(Rdkg+W01)Ko{rW3w0Z9PQZ_r;JtnLq3EM|z7x)eZg_>; zv_<%p_xN5IIPYAY2XfAbdweVK=ZAcpKjz~+^ixQTRKh(fzEknC0D!#>%Qxt;B1`I% zoKlcGQ@tF@7Vl5@BvAEql*!#c)srN^X*4={h$s5OKu-;$yqOr6!Tb_2%poyMsUm10 z4`~6@n=(~szZT_>n@k$*)_&{F1Q50%1>(NL&^V5pP~LEa*>HWB?ww?o2ZYdpCdY&@ zULk}EG$FhDBk{D{f$6(!H1?qK^|}cLs|29plP*}b*%tE;t(Sx%dpzERQ89Xv25oo!7F9r? zs?f;LVww^tHJEd%2}$>YRx-*g%KPLVp;prlMf50hJn<1G(b#A4*RmGy8$sK5u8ZlX zCq)P?XnaMQJmF281*S7#VB$~WLn@Mw(jO#)uteuT6bAMOEYEr}#`s9mu=2Ek@VTc7 z#<_nK38#DM5}x=CXS?|kU|dO)Cc34uAF|(Nzt4B(x@Ds?_p5ZWd*&^%mw|iIvAXlClra|B6o>NvE;;@IoKK)|Zh4&VU{-y|CxN{%Ie<7Tcdl zfS&cXG0Kb{Aqg%}D0XJMrSS)I&wDD!&wD3Ox_pFWJl;)ZJ7e8^p)=Jjjy)(p@1;Qb vFRMU`KWZzJ=%<kr|D?W9LcTS@?zFaoUZ0>4;g}zBx_D$@t}v zogv`R2q>J9S;YT1T!j8ZX3#HY2zRW~fT0DZryH@tI;l}zAMs<|vz3^dCMQO#r&4rskEDnxFYbr^PEnWk@1qTa*WTw1H`M2@8@Y!8_Y*FC`3ud5cN^rsh4f88VWVC_@7ziw07yK0kO|0}us!}F+rjaKp- D1ungV literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/re2/__pycache__/gitignore.cpython-314.pyc b/lib/pathspec/_backends/re2/__pycache__/gitignore.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a6dc402427b1df19f00931367375e0b9f33766e6 GIT binary patch literal 5775 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 Bghc=V literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/re2/__pycache__/pathspec.cpython-314.pyc b/lib/pathspec/_backends/re2/__pycache__/pathspec.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3d921c5d08b36ac4ae204c4cb788523c536610aa GIT binary patch literal 6512 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 literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/re2/_base.py b/lib/pathspec/_backends/re2/_base.py new file mode 100644 index 0000000..4e6ae9f --- /dev/null +++ b/lib/pathspec/_backends/re2/_base.py @@ -0,0 +1,95 @@ +""" +This module provides private data for the base implementation for the +:module:`re2` library. + +WARNING: The *pathspec._backends.re2* package is not part of the public API. Its +contents and structure are likely to change. +""" +from __future__ import annotations + +from dataclasses import ( + dataclass) +from typing import ( + Optional, # Replaced by `X | None` in 3.10. + Union) # Replaced by `X | Y` in 3.10. + +try: + import re2 + re2_error = None +except ModuleNotFoundError as e: + re2 = None + re2_error = e + RE2_OPTIONS = None +else: + # Both the `google-re2` and `pyre2` libraries use the `re2` namespace. + # `google-re2` is the only one currently supported. + try: + RE2_OPTIONS = re2.Options() + RE2_OPTIONS.log_errors = False + RE2_OPTIONS.never_capture = True + except Exception as e: + re2_error = e + RE2_OPTIONS = None + +RE2_OPTIONS: re2.Options +""" +The re2 options to use: + +- `log_errors=False` disables logging to stderr. + +- `never_capture=True` disables capture groups because they effectively cannot + be utilized with :class:`re2.Set`. +""" + +re2_error: Optional[Exception] +""" +*re2_error* (:class:`Exception` or :data:`None`) is the re2 import error. +""" + + +@dataclass(frozen=True) +class Re2RegexDat(object): + """ + The :class:`Re2RegexDat` class is used to store data related to a regular + expression. + """ + + # The slots argument is not supported until Python 3.10. + __slots__ = [ + 'include', + 'index', + 'is_dir_pattern', + ] + + include: bool + """ + *include* (:class:`bool`) is whether is whether the matched files should be + included (:data:`True`), or excluded (:data:`False`). + """ + + index: int + """ + *index* (:class:`int`) is the pattern index. + """ + + is_dir_pattern: bool + """ + *is_dir_pattern* (:class:`bool`) is whether the pattern is a directory + pattern for gitignore. + """ + + +@dataclass(frozen=True) +class Re2RegexDebug(Re2RegexDat): + """ + The :class:`Re2RegexDebug` class stores additional debug information related + to a regular expression. + """ + + # The slots argument is not supported until Python 3.10. + __slots__ = ['regex'] + + regex: Union[str, bytes] + """ + *regex* (:class:`str` or :class:`bytes`) is the regular expression. + """ diff --git a/lib/pathspec/_backends/re2/base.py b/lib/pathspec/_backends/re2/base.py new file mode 100644 index 0000000..fa24f4d --- /dev/null +++ b/lib/pathspec/_backends/re2/base.py @@ -0,0 +1,18 @@ +""" +This module provides the base implementation for the :module:`re2` backend. + +WARNING: The *pathspec._backends.re2* package is not part of the public API. Its +contents and structure are likely to change. +""" +from __future__ import annotations + +from typing import ( + Optional) # Replaced by `X | None` in 3.10. + +from ._base import ( + re2_error) + +re2_error: Optional[Exception] +""" +*re2_error* (:class:`Exception` or :data:`None`) is the re2 import error. +""" diff --git a/lib/pathspec/_backends/re2/gitignore.py b/lib/pathspec/_backends/re2/gitignore.py new file mode 100644 index 0000000..cb2525f --- /dev/null +++ b/lib/pathspec/_backends/re2/gitignore.py @@ -0,0 +1,179 @@ +""" +This module provides the :module:`re2` backend for :class:`~pathspec.gitignore.GitIgnoreSpec`. + +WARNING: The *pathspec._backends.re2* package is not part of the public API. Its +contents and structure are likely to change. +""" +from __future__ import annotations + +from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. + Optional, # Replaced by `X | None` in 3.10. + Union) # Replaced by `X | Y` in 3.10. + +try: + import re2 +except ModuleNotFoundError: + re2 = None + +from pathspec.pattern import ( + RegexPattern) +from pathspec.patterns.gitignore.spec import ( + GitIgnoreSpecPattern, + _BYTES_ENCODING, + _DIR_MARK_CG, + _DIR_MARK_OPT) +from pathspec._typing import ( + override) # Added in 3.12. + +from ._base import ( + Re2RegexDat, + Re2RegexDebug) +from .pathspec import ( + Re2PsBackend) + + +class Re2GiBackend(Re2PsBackend): + """ + The :class:`Re2GiBackend` class is the :module:`re2` implementation used by + :class:`~pathspec.gitignore.GitIgnoreSpec` for matching files. + """ + + @override + @staticmethod + def _init_set( + debug: bool, + patterns: dict[int, RegexPattern], + regex_set: re2.Set, + sort_indices: Optional[Callable[[list[int]], None]], + ) -> list[Re2RegexDat]: + """ + Create the re2 regex set. + + *debug* (:class:`bool`) is whether to include additional debugging + information for the regular expressions. + + *patterns* (:class:`dict`) maps pattern index (:class:`int`) to pattern + (:class:`.RegexPattern`). + + *regex_set* (:class:`re2.Set`) is the regex set. + + *sort_indices* (:class:`callable` or :data:`None`) is a function used to + sort the patterns by index. This is used during testing to ensure the order + of patterns is not accidentally relied on. + + Returns a :class:`list` indexed by regex id (:class:`int`) to its data + (:class:`Re2RegexDat`). + """ + # Sort patterns. + indices = list(patterns.keys()) + if sort_indices is not None: + sort_indices(indices) + + # Prepare patterns. + regex_data: list[Re2RegexDat] = [] + for pattern_index in indices: + pattern = patterns[pattern_index] + if pattern.include is None: + continue + + assert isinstance(pattern, RegexPattern), pattern + regex = pattern.regex.pattern + + use_regexes: list[tuple[Union[str, bytes], bool]] = [] + if isinstance(pattern, GitIgnoreSpecPattern): + # GitIgnoreSpecPattern uses capture groups for its directory marker. Re2 + # supports capture groups, but they cannot be utilized when using + # `re2.Set`. Handle this scenario. + regex_str: str + if isinstance(regex, str): + regex_str = regex + else: + assert isinstance(regex, bytes), regex + regex_str = regex.decode(_BYTES_ENCODING) + + if _DIR_MARK_CG in regex_str: + # Found directory marker. + if regex_str.endswith(_DIR_MARK_OPT): + # Regex has optional directory marker. Split regex into directory + # and file variants. + base_regex = regex_str[:-len(_DIR_MARK_OPT)] + use_regexes.append((f'{base_regex}/', True)) + use_regexes.append((f'{base_regex}$', False)) + else: + # Remove capture group. + base_regex = regex_str.replace(_DIR_MARK_CG, '/') + use_regexes.append((base_regex, True)) + + if not use_regexes: + # No special case for regex. + use_regexes.append((regex, False)) + + for regex, is_dir_pattern in use_regexes: + if debug: + regex_data.append(Re2RegexDebug( + include=pattern.include, + index=pattern_index, + is_dir_pattern=is_dir_pattern, + regex=regex, + )) + else: + regex_data.append(Re2RegexDat( + include=pattern.include, + index=pattern_index, + is_dir_pattern=is_dir_pattern, + )) + + regex_set.Add(regex) + + # Compile patterns. + regex_set.Compile() + return regex_data + + @override + def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: + """ + Check the file against the patterns. + + *file* (:class:`str`) is the normalized file path to check. + + Returns a :class:`tuple` containing whether to include *file* (:class:`bool` + or :data:`None`), and the index of the last matched pattern (:class:`int` or + :data:`None`). + """ + # Find best match. + match_ids: Optional[list[int]] = self._set.Match(file) + if not match_ids: + return (None, None) + + out_include: Optional[bool] = None + out_index: int = -1 + out_priority = -1 + + regex_data = self._regex_data + for regex_id in match_ids: + regex_dat = regex_data[regex_id] + + is_dir_pattern = regex_dat.is_dir_pattern + if is_dir_pattern: + # Pattern matched by a directory pattern. + priority = 1 + else: + # Pattern matched by a file pattern. + priority = 2 + + # WARNING: According to the documentation on `RE2::Set::Match()`, there is + # no guarantee matches will be produced in order! + include = regex_dat.include + index = regex_dat.index + if ( + (include and is_dir_pattern and index > out_index) + or (priority == out_priority and index > out_index) + or priority > out_priority + ): + out_include = include + out_index = index + out_priority = priority + + assert out_index != -1, (out_index, out_include, out_priority) + return (out_include, out_index) diff --git a/lib/pathspec/_backends/re2/pathspec.py b/lib/pathspec/_backends/re2/pathspec.py new file mode 100644 index 0000000..2c58b45 --- /dev/null +++ b/lib/pathspec/_backends/re2/pathspec.py @@ -0,0 +1,187 @@ +""" +This module provides the :module:`re2` backend for :class:`~pathspec.pathspec.PathSpec`. + +WARNING: The *pathspec._backends.re2* package is not part of the public API. Its +contents and structure are likely to change. +""" +from __future__ import annotations + +from collections.abc import ( + Sequence) +from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. + Optional) # Replaced by `X | None` in 3.10. + +try: + import re2 +except ModuleNotFoundError: + re2 = None + +from pathspec.backend import ( + _Backend) +from pathspec.pattern import ( + RegexPattern) +from pathspec._typing import ( + override) # Added in 3.12. + +from .._utils import ( + enumerate_patterns) + +from .base import ( + re2_error) +from ._base import ( + RE2_OPTIONS, + Re2RegexDat, + Re2RegexDebug) + + +class Re2PsBackend(_Backend): + """ + The :class:`Re2PsBackend` class is the :module:`re2` implementation used by + :class:`~pathspec.pathspec.PathSpec` for matching files. + """ + + def __init__( + self, + patterns: Sequence[RegexPattern], + *, + _debug_regex: Optional[bool] = None, + _test_sort: Optional[Callable[[list], None]] = None, + ) -> None: + """ + Initialize the :class:`Re2PsBackend` instance. + + *patterns* (:class:`Sequence` of :class:`.RegexPattern`) contains the + compiled patterns. + """ + if re2_error is not None: + raise re2_error + + if patterns and not isinstance(patterns[0], RegexPattern): + raise TypeError(f"{patterns[0]=!r} must be a RegexPattern.") + + use_patterns = dict(enumerate_patterns( + patterns, filter=True, reverse=False, + )) + regex_set = self._make_set() + + self._debug_regex = bool(_debug_regex) + """ + *_debug_regex* (:class:`bool`) is whether to include additional debugging + information for the regular expressions. + """ + + self._patterns: dict[int, RegexPattern] = use_patterns + """ + *_patterns* (:class:`dict`) maps pattern index (:class:`int`) to pattern + (:class:`RegexPattern`). + """ + + self._regex_data: list[Re2RegexDat] = self._init_set( + debug=self._debug_regex, + patterns=use_patterns, + regex_set=regex_set, + sort_indices=_test_sort, + ) + """ + *_regex_data* (:class:`list`) maps regex index (:class:`int`) to regex data + (:class:`Re2RegexDat`). + """ + + self._set: re2.Set = regex_set + """ + *_set* (:class:`re2.Set`) is the re2 regex set. + """ + + @staticmethod + def _init_set( + debug: bool, + patterns: dict[int, RegexPattern], + regex_set: re2.Set, + sort_indices: Optional[Callable[[list[int]], None]], + ) -> list[Re2RegexDat]: + """ + Create the re2 regex set. + + *debug* (:class:`bool`) is whether to include additional debugging + information for the regular expressions. + + *patterns* (:class:`dict`) maps pattern index (:class:`int`) to pattern + (:class:`.RegexPattern`). + + *regex_set* (:class:`re2.Set`) is the regex set. + + *sort_indices* (:class:`callable` or :data:`None`) is a function used to + sort the patterns by index. This is used during testing to ensure the order + of patterns is not accidentally relied on. + + Returns a :class:`list` indexed by regex id (:class:`int`) to its data + (:class:`Re2RegexDat`). + """ + # Sort patterns. + indices = list(patterns.keys()) + if sort_indices is not None: + sort_indices(indices) + + # Prepare patterns. + regex_data: list[Re2RegexDat] = [] + for pattern_index in indices: + pattern = patterns[pattern_index] + if pattern.include is None: + continue + + assert isinstance(pattern, RegexPattern), pattern + regex = pattern.regex.pattern + + if debug: + regex_data.append(Re2RegexDebug( + include=pattern.include, + index=pattern_index, + is_dir_pattern=False, + regex=regex, + )) + else: + regex_data.append(Re2RegexDat( + include=pattern.include, + index=pattern_index, + is_dir_pattern=False, + )) + + regex_set.Add(regex) + + # Compile patterns. + regex_set.Compile() + return regex_data + + @staticmethod + def _make_set() -> re2.Set: + """ + Create the re2 regex set. + + Returns the set (:class:`re2.Set`). + """ + return re2.Set.SearchSet(RE2_OPTIONS) + + @override + def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: + """ + Check the file against the patterns. + + *file* (:class:`str`) is the normalized file path to check. + + Returns a :class:`tuple` containing whether to include *file* (:class:`bool` + or :data:`None`), and the index of the last matched pattern (:class:`int` or + :data:`None`). + """ + # Find best match. + # - WARNING: According to the documentation on `RE2::Set::Match()`, there is + # no guarantee matches will be produced in order! Later expressions have + # higher priority. + match_ids: Optional[list[int]] = self._set.Match(file) + if not match_ids: + return (None, None) + + regex_data = self._regex_data + pattern_index = max(regex_data[__id].index for __id in match_ids) + pattern = self._patterns[pattern_index] + return (pattern.include, pattern_index) diff --git a/lib/pathspec/_backends/simple/__init__.py b/lib/pathspec/_backends/simple/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/pathspec/_backends/simple/__pycache__/__init__.cpython-314.pyc b/lib/pathspec/_backends/simple/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbfe8bd47d91b4f5d27110773db645736b12ded2 GIT binary patch literal 172 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08C%TvE3w?Mxj zvp}~bu_!&YMAx#UvLHXj)IdKcGfBUovLquvPro3sB%`<>HCaDCDKR-aH7})Dzc@3u vASYEnK0Y%qvm`!Vub}c4hfQvNN@-52T@fqLWRTs(AjT(VMn=XWW*`dy9h55a literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/simple/__pycache__/gitignore.cpython-314.pyc b/lib/pathspec/_backends/simple/__pycache__/gitignore.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d67face00e5c6fee65a0efdc779b911c85cdd84 GIT binary patch literal 4007 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 literal 0 HcmV?d00001 diff --git a/lib/pathspec/_backends/simple/__pycache__/pathspec.cpython-314.pyc b/lib/pathspec/_backends/simple/__pycache__/pathspec.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..076cdb48e8e7b4b02c9d5f667d85ef4d833b237a GIT binary patch literal 3114 zcma)8O>Er86(0U9cSZfizk%b#bRwu)#ol#m2X37pwPZBb9;&LeVHc=vF2x;b3318I zaMrDS2=)*lryw=ZLk$Eddb5G#&|?ohD3uIbAlaaCz)G0%G`;Y);S|z zQ_i~f8>~8c=b?SI9i~nvY4sxRbVYJIZ1L2QZSJIDrwiGJA8hlurm#0lDdyv1vFRm&<@ z`1?H`2b}m{bY+;tenh;tk{<|s8Jm_|#3W8`b0Khc@N+?%2iso9mqFXx3?q^q;Bl|R zg)g~>^UzOeFpML;&EjM478oDVyvd}7i0ZeFN#h7h5Laosx@)+mYt;2??5yUNj#W($4B%MVBG!I18QRX`EuTPG$dns=@8#^O|FN(KH4CSjZ-g3xYY0b_i;)GlW zN3CTxc){V{lm{A)L=O$%fl)s0H9_9d`s^C=_QxMG)`Ijn`=!>_TFLdzda1B&8M8xnDq=MauLmB!}c!#U>9M+h9 zM{hb|oJt>nMuDN4qA+W428AzT(MbI1;mF9wMdY3y2#X)Jv2l{M=3jI zmEw<|0iEBpInX1h%xrVsMZr3qbWyVMjbI-wU-}oCS4EFEtEX&$OYjb=3a!@}O~2k)K|)tp4MxUlyv2`QFTFjHq#N~4AIeW5f`GAm$aR+2q5NL47rOQd3i zv8?Li0C;f_q^J+dJ>oD)kASSu%nv~nZf*UNw#$g#yEIVmKmF{%;-AlbUi-`97ia%^ z`m3eO59e2Njob?(5WxghDg{(B+GjV0r4=w;AJ@Yyjq7vk6tjI}cqsrRHGi5Qd5IdJ zN+LS6<8Kjgq!J7ho-&vw$8r}w$)Q528dJd}P~+x6Ht=6NZ_zM~pIudQ(auL$Ro+Ce zLZioW)J(=87sf%2(yp< zVg8S`d$s%X-T1nt};x)2FbzLlgWQWncsM!Y|3YZ8tdXlG5e`&101w4v8ud3y# zOqq2t&t$sM3L?~!)1QJo{RoHI)eXHgu&K3F*oik(%Nf|rnSVm<=HJwu1}hpfCZFh( z?t2E<$BI>_RTkH(bK(>XX0t&OMLeL}NLuqZ0@au^3w;%dk@w~~N-1>-E$)?qayxq_ zE+VYj`Iy)%jF2<3>5-A13?mik%<_m&RTWgbdcg7>=4I}k-1qN_^DsbeN#6z;7@DT- n>Ltzmen!(Kzc-0k1!w1 None: + """ + Initialize the :class:`SimpleGiBackend` instance. + + *patterns* (:class:`Sequence` of :class:`.RegexPattern`) contains the + compiled patterns. + + *no_filter* (:class:`bool`) is whether to keep no-op patterns (:data:`True`), + or remove them (:data:`False`). + + *no_reverse* (:class:`bool`) is whether to keep the pattern order + (:data:`True`), or reverse the order (:data:`True`). + """ + super().__init__(patterns, no_filter=no_filter, no_reverse=no_reverse) + + @override + def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: + """ + Check the file against the patterns. + + *file* (:class:`str`) is the normalized file path to check. + + Returns a :class:`tuple` containing whether to include *file* (:class:`bool` + or :data:`None`), and the index of the last matched pattern (:class:`int` or + :data:`None`). + """ + is_reversed = self._is_reversed + + out_include: Optional[bool] = None + out_index: Optional[int] = None + out_priority = 0 + for index, pattern in self._patterns: + if ( + (include := pattern.include) is not None + and (match := pattern.match_file(file)) is not None + ): + # Pattern matched. + + # Check for directory marker. + dir_mark = match.match.groupdict().get(_DIR_MARK) + + if dir_mark: + # Pattern matched by a directory pattern. + priority = 1 + else: + # Pattern matched by a file pattern. + priority = 2 + + if is_reversed: + if priority > out_priority: + out_include = include + out_index = index + out_priority = priority + else: + # Forward. + if (include and dir_mark) or priority >= out_priority: + out_include = include + out_index = index + out_priority = priority + + if is_reversed and priority == 2: + # Patterns are being checked in reverse order. The first pattern that + # matches with priority 2 takes precedence. + break + + return (out_include, out_index) diff --git a/lib/pathspec/_backends/simple/pathspec.py b/lib/pathspec/_backends/simple/pathspec.py new file mode 100644 index 0000000..2ded1be --- /dev/null +++ b/lib/pathspec/_backends/simple/pathspec.py @@ -0,0 +1,76 @@ +""" +This module provides the simple backend for :class:`~pathspec.pathspec.PathSpec`. + +WARNING: The *pathspec._backends.simple* package is not part of the public API. +Its contents and structure are likely to change. +""" + +from collections.abc import ( + Sequence) +from typing import ( + Optional) # Replaced by `X | None` in 3.10. + +from pathspec.backend import ( + _Backend) +from pathspec.pattern import ( + Pattern) +from pathspec._typing import ( + override) # Added in 3.12. +from pathspec.util import ( + check_match_file) + +from .._utils import ( + enumerate_patterns) + + +class SimplePsBackend(_Backend): + """ + The :class:`SimplePsBackend` class is the default (or simple) implementation + used by :class:`~pathspec.pathspec.PathSpec` for matching files. + """ + + def __init__( + self, + patterns: Sequence[Pattern], + *, + no_filter: Optional[bool] = None, + no_reverse: Optional[bool] = None, + ) -> None: + """ + Initialize the :class:`SimplePsBackend` instance. + + *patterns* (:class:`Sequence` of :class:`.Pattern`) contains the compiled + patterns. + + *no_filter* (:class:`bool`) is whether to keep no-op patterns (:data:`True`), + or remove them (:data:`False`). + + *no_reverse* (:class:`bool`) is whether to keep the pattern order + (:data:`True`), or reverse the order (:data:`True`). + """ + + self._is_reversed: bool = not no_reverse + """ + *_is_reversed* (:class:`bool`) is whether to the pattern order was reversed. + """ + + self._patterns: list[tuple[int, Pattern]] = enumerate_patterns( + patterns, filter=not no_filter, reverse=not no_reverse, + ) + """ + *_patterns* (:class:`list` of :class:`tuple`) contains the enumerated + patterns. + """ + + @override + def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: + """ + Check the file against the patterns. + + *file* (:class:`str`) is the normalized file path to check. + + Returns a :class:`tuple` containing whether to include *file* (:class:`bool` + or :data:`None`), and the index of the last matched pattern (:class:`int` or + :data:`None`). + """ + return check_match_file(self._patterns, file, self._is_reversed) diff --git a/lib/pathspec/_meta.py b/lib/pathspec/_meta.py new file mode 100644 index 0000000..4e4c782 --- /dev/null +++ b/lib/pathspec/_meta.py @@ -0,0 +1,67 @@ +""" +This module contains the project meta-data. +""" + +__author__ = "Caleb P. Burns" +__copyright__ = "Copyright © 2013-2026 Caleb P. Burns" +__credits__ = [ + "Hong Minhee ", + "Brandon High ", + "029xue ", + "Michael Huynh ", + "Nick Humrich ", + "David Fraser ", + "Charles Samborski ", + "George Hickman ", + "Vincent Driessen ", + "Adrien Vergé ", + "Anders Blomdell ", + "Xavier Thomas ", + "Wim Jeantine-Glenn ", + "Hugo van Kemenade ", + "Dan Cecile ", + "MrOutis ", + "Jon Dufresne ", + "Greg Roodt ", + "Florin T. ", + "Ben Felder ", + "Nicholas Hollander ", + "KOLANICH ", + "Jon Hays ", + "Isaac0616 ", + "Sebastiaan Zeeff ", + "Roel Adriaans ", + "Ravi Selker ", + "Johan Vergeer ", + "danjer ", + "Jan-Hein Bührman ", + "Wim-Peter Dirks ", + "Karthikeyan Singaravelan ", + "John Vandenberg ", + "John T. Wodder II ", + "Tomasz KÅ‚oczko ", + "Oren ", + "SP Mohanty ", + "Richard Si ", + "Jakub Kuczys ", + "MichaÅ‚ Górny ", + "BartÅ‚omiej Å»ak ", + "Matthias ", + "Avasam ", + "Anıl Karagenç ", + "Yannic Schröder ", + "axesider ", + "TomRuk ", + "Oleh Prypin ", + "Lumina ", + "Kurt McKee ", + "Dobatymo ", + "Tomoki Nakamaru ", + "Sebastien Eskenazi ", + "Bar Vered ", + "Tzach Shabtay ", + "Adam Dangoor ", + "Marcel Telka ", + "Dmytro Kostochko ", +] +__license__ = "MPL 2.0" diff --git a/lib/pathspec/_typing.py b/lib/pathspec/_typing.py new file mode 100644 index 0000000..049966c --- /dev/null +++ b/lib/pathspec/_typing.py @@ -0,0 +1,64 @@ +""" +This module provides stubs for type hints not supported by all relevant Python +versions. + +NOTICE: This project should have zero required dependencies which means it +cannot simply require :module:`typing_extensions`, and I do not want to maintain +a vendored copy of :module:`typing_extensions`. +""" + +import functools +import warnings +from typing import ( + Any, + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. + Optional, # Replaced by `X | None` in 3.10. + TypeVar) +try: + from typing import AnyStr # Removed in 3.18. +except ImportError: + AnyStr = TypeVar('AnyStr', str, bytes) +try: + from typing import Never # Added in 3.11. +except ImportError: + from typing import NoReturn as Never + +F = TypeVar('F', bound=Callable[..., Any]) + +try: + from warnings import deprecated # Added in 3.13. +except ImportError: + try: + from typing_extensions import deprecated + except ImportError: + def deprecated( + message: str, + /, *, + category: Optional[type[Warning]] = DeprecationWarning, + stacklevel: int = 1, + ) -> Callable[[F], F]: + def decorator(f: F) -> F: + @functools.wraps(f) + def wrapper(*a, **k): + warnings.warn(message, category=category, stacklevel=stacklevel+1) + return f(*a, **k) + return wrapper + return decorator + +try: + from typing import override # Added in 3.12. +except ImportError: + try: + from typing_extensions import override + except ImportError: + def override(f: F) -> F: + return f + + +def assert_unreachable(message: str) -> Never: + """ + The code path is unreachable. Raises an :class:`AssertionError`. + + *message* (:class:`str`) is the error message. + """ + raise AssertionError(message) diff --git a/lib/pathspec/_version.py b/lib/pathspec/_version.py new file mode 100644 index 0000000..421d8fa --- /dev/null +++ b/lib/pathspec/_version.py @@ -0,0 +1,5 @@ +""" +This module defines the version. +""" + +__version__ = "1.0.4" diff --git a/lib/pathspec/backend.py b/lib/pathspec/backend.py new file mode 100644 index 0000000..f1def28 --- /dev/null +++ b/lib/pathspec/backend.py @@ -0,0 +1,40 @@ +""" +This module defines the necessary classes and type hints for exposing the bare +minimum of the internal implementations for the pattern (regular expression) +matching backends. The exact structure of the backends is not solidified and is +subject to change. +""" + +from typing import ( + Literal, + Optional) + +BackendNamesHint = Literal['best', 'hyperscan', 're2', 'simple'] +""" +The supported backend values. +""" + + +class _Backend(object): + """ + .. warning:: This class is not part of the public API. It is subject to + change. + + The :class:`_Backend` class is the abstract base class defining how to match + files against patterns. + """ + + def match_file(self, file: str) -> tuple[Optional[bool], Optional[int]]: + """ + Check the file against the patterns. + + *file* (:class:`str`) is the normalized file path to check. + + Returns a :class:`tuple` containing whether to include *file* (:class:`bool` + or :data:`None`), and the index of the last matched pattern (:class:`int` or + :data:`None`). + """ + raise NotImplementedError(( + f"{self.__class__.__module__}.{self.__class__.__qualname__}.match_file() " + f"must be implemented." + )) # NotImplementedError diff --git a/lib/pathspec/gitignore.py b/lib/pathspec/gitignore.py new file mode 100644 index 0000000..93c3d76 --- /dev/null +++ b/lib/pathspec/gitignore.py @@ -0,0 +1,165 @@ +""" +This module provides :class:`.GitIgnoreSpec` which replicates *.gitignore* +behavior, and handles edge-cases where Git's behavior differs from what's +documented. Git allows including files from excluded directories which directly +contradicts the documentation. This uses :class:`.GitIgnoreSpecPattern` to fully +replicate Git's handling. +""" +from __future__ import annotations + +from collections.abc import ( + Iterable, + Sequence) +from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. + Optional, # Replaced by `X | None` in 3.10. + TypeVar, + Union, # Replaced by `X | Y` in 3.10. + cast, + overload) + +from pathspec.backend import ( + BackendNamesHint, + _Backend) +from pathspec._backends.agg import ( + make_gitignore_backend) +from pathspec.pathspec import ( + PathSpec) +from pathspec.pattern import ( + Pattern) +from pathspec.patterns.gitignore.basic import ( + GitIgnoreBasicPattern) +from pathspec.patterns.gitignore.spec import ( + GitIgnoreSpecPattern) +from pathspec._typing import ( + AnyStr, # Removed in 3.18. + override) # Added in 3.12. +from pathspec.util import ( + _is_iterable, + lookup_pattern) + +Self = TypeVar("Self", bound='GitIgnoreSpec') +""" +:class:`.GitIgnoreSpec` self type hint to support Python v<3.11 using PEP 673 +recommendation. +""" + + +class GitIgnoreSpec(PathSpec): + """ + The :class:`GitIgnoreSpec` class extends :class:`.PathSpec` to replicate + *gitignore* behavior. This is uses :class:`.GitIgnoreSpecPattern` to fully + replicate Git's handling. + """ + + def __eq__(self, other: object) -> bool: + """ + Tests the equality of this gitignore-spec with *other* (:class:`.GitIgnoreSpec`) + by comparing their :attr:`self.patterns <.PathSpec.patterns>` attributes. A + non-:class:`GitIgnoreSpec` will not compare equal. + """ + if isinstance(other, GitIgnoreSpec): + return super().__eq__(other) + elif isinstance(other, PathSpec): + return False + else: + return NotImplemented + + # Support reversed order of arguments from PathSpec. + @overload + @classmethod + def from_lines( + cls: type[Self], + pattern_factory: Union[str, Callable[[AnyStr], Pattern], None], + lines: Iterable[AnyStr], + *, + backend: Union[BackendNamesHint, str, None] = None, + _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + ) -> Self: + ... + + @overload + @classmethod + def from_lines( + cls: type[Self], + lines: Iterable[AnyStr], + pattern_factory: Union[str, Callable[[AnyStr], Pattern], None] = None, + *, + backend: Union[BackendNamesHint, str, None] = None, + _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + ) -> Self: + ... + + @override + @classmethod + def from_lines( + cls: type[Self], + lines: Iterable[AnyStr], + pattern_factory: Union[str, Callable[[AnyStr], Pattern], None] = None, + *, + backend: Union[BackendNamesHint, str, None] = None, + _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + ) -> Self: + """ + Compiles the pattern lines. + + *lines* (:class:`~collections.abc.Iterable`) yields each uncompiled pattern + (:class:`str`). This simply has to yield each line, so it can be a + :class:`io.TextIOBase` (e.g., from :func:`open` or :class:`io.StringIO`) or + the result from :meth:`str.splitlines`. + + *pattern_factory* does not need to be set for :class:`GitIgnoreSpec`. If + set, it should be either ``"gitignore"`` or :class:`.GitIgnoreSpecPattern`. + There is no guarantee it will work with any other pattern class. Default is + :data:`None` for :class:`.GitIgnoreSpecPattern`. + + *backend* (:class:`str` or :data:`None`) is the pattern (regular expression) + matching backend to use. Default is :data:`None` for "best" to use the best + available backend. Priority of backends is: "re2", "hyperscan", "simple". + The "simple" backend is always available. + + Returns the :class:`GitIgnoreSpec` instance. + """ + if (isinstance(lines, (str, bytes)) or callable(lines)) and _is_iterable(pattern_factory): + # Support reversed order of arguments from PathSpec. + pattern_factory, lines = lines, pattern_factory + + if pattern_factory is None: + pattern_factory = GitIgnoreSpecPattern + elif pattern_factory == 'gitignore': + # Force use of GitIgnoreSpecPattern for "gitignore" to handle edge-cases. + # This makes usage easier. + pattern_factory = GitIgnoreSpecPattern + + if isinstance(pattern_factory, str): + pattern_factory = lookup_pattern(pattern_factory) + + if issubclass(pattern_factory, GitIgnoreBasicPattern): + raise TypeError(( + f"{pattern_factory=!r} cannot be {GitIgnoreBasicPattern} because it " + f"will give unexpected results." + )) # TypeError + + self = super().from_lines(pattern_factory, lines, backend=backend, _test_backend_factory=_test_backend_factory) + return cast(Self, self) + + @override + @staticmethod + def _make_backend( + name: BackendNamesHint, + patterns: Sequence[Pattern], + ) -> _Backend: + """ + .. warning:: This method is not part of the public API. It is subject to + change. + + Create the backend for the patterns. + + *name* (:class:`str`) is the name of the backend. + + *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`) + contains the compiled patterns. + + Returns the backend (:class:`._Backend`). + """ + return make_gitignore_backend(name, patterns) diff --git a/lib/pathspec/pathspec.py b/lib/pathspec/pathspec.py new file mode 100644 index 0000000..bb88cbf --- /dev/null +++ b/lib/pathspec/pathspec.py @@ -0,0 +1,460 @@ +""" +This module provides :class:`.PathSpec` which is an object-oriented interface +for pattern matching of files. +""" +from __future__ import annotations + +from collections.abc import ( + Collection, + Iterable, + Iterator, + Sequence) +from itertools import ( + zip_longest) +from typing import ( + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. + Optional, # Replaced by `X | None` in 3.10. + TypeVar, + Union, # Replaced by `X | Y` in 3.10. + cast) + +Self = TypeVar("Self", bound='PathSpec') +""" +:class:`.PathSpec` self type hint to support Python v<3.11 using PEP 673 +recommendation. +""" + +from pathspec import util +from pathspec.backend import ( + _Backend, + BackendNamesHint) +from pathspec._backends.agg import ( + make_pathspec_backend) +from pathspec.pattern import ( + Pattern) +from pathspec._typing import ( + AnyStr, # Removed in 3.18. + deprecated) # Added in 3.13. +from pathspec.util import ( + CheckResult, + StrPath, + TStrPath, + TreeEntry, + _is_iterable, + normalize_file) + + +class PathSpec(object): + """ + The :class:`PathSpec` class is a wrapper around a list of compiled + :class:`.Pattern` instances. + """ + + def __init__( + self, + patterns: Union[Sequence[Pattern], Iterable[Pattern]], + *, + backend: Union[BackendNamesHint, str, None] = None, + _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + ) -> None: + """ + Initializes the :class:`.PathSpec` instance. + + *patterns* (:class:`~collections.abc.Sequence` or :class:`~collections.abc.Iterable`) + contains each compiled pattern (:class:`.Pattern`). If not a sequence, it + will be converted to a :class:`list`. + + *backend* (:class:`str` or :data:`None`) is the pattern (regular expression) + matching backend to use. Default is :data:`None` for "best" to use the best + available backend. Priority of backends is: "re2", "hyperscan", "simple". + The "simple" backend is always available. + """ + if not isinstance(patterns, Sequence): + patterns = list(patterns) + + if backend is None: + backend = 'best' + + backend = cast(BackendNamesHint, backend) + if _test_backend_factory is not None: + use_backend = _test_backend_factory(patterns) + else: + use_backend = self._make_backend(backend, patterns) + + self._backend: _Backend = use_backend + """ + *_backend* (:class:`._Backend`) is the pattern (regular expression) matching + backend. + """ + + self._backend_name: BackendNamesHint = backend + """ + *_backend_name* (:class:`str`) is the name of backend to use. + """ + + self.patterns: Sequence[Pattern] = patterns + """ + *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`) + contains the compiled patterns. + """ + + def __add__(self: Self, other: PathSpec) -> Self: + """ + Combines the :attr:`self.patterns <.PathSpec.patterns>` patterns from two + :class:`PathSpec` instances. + """ + if isinstance(other, PathSpec): + return self.__class__(self.patterns + other.patterns, backend=self._backend_name) + else: + return NotImplemented + + def __eq__(self, other: object) -> bool: + """ + Tests the equality of this path-spec with *other* (:class:`PathSpec`) by + comparing their :attr:`self.patterns <.PathSpec.patterns>` attributes. + """ + if isinstance(other, PathSpec): + paired_patterns = zip_longest(self.patterns, other.patterns) + return all(a == b for a, b in paired_patterns) + else: + return NotImplemented + + def __iadd__(self: Self, other: PathSpec) -> Self: + """ + Adds the :attr:`self.patterns <.PathSpec.patterns>` from *other* + (:class:`PathSpec`) to this instance. + """ + if isinstance(other, PathSpec): + self.patterns += other.patterns + self._backend = self._make_backend(self._backend_name, self.patterns) + return self + else: + return NotImplemented + + def __len__(self) -> int: + """ + Returns the number of :attr:`self.patterns <.PathSpec.patterns>` this + path-spec contains (:class:`int`). + """ + return len(self.patterns) + + def check_file( + self, + file: TStrPath, + separators: Optional[Collection[str]] = None, + ) -> CheckResult[TStrPath]: + """ + Check the files against this path-spec. + + *file* (:class:`str` or :class:`os.PathLike`) is the file path to be matched + against :attr:`self.patterns <.PathSpec.patterns>`. + + *separators* (:class:`~collections.abc.Collection` of :class:`str`; or + :data:`None`) optionally contains the path separators to normalize. See + :func:`.normalize_file` for more information. + + Returns the file check result (:class:`.CheckResult`). + """ + norm_file = normalize_file(file, separators) + include, index = self._backend.match_file(norm_file) + return CheckResult(file, include, index) + + def check_files( + self, + files: Iterable[TStrPath], + separators: Optional[Collection[str]] = None, + ) -> Iterator[CheckResult[TStrPath]]: + """ + Check the files against this path-spec. + + *files* (:class:`~collections.abc.Iterable` of :class:`str` or + :class:`os.PathLike`) contains the file paths to be checked against + :attr:`self.patterns <.PathSpec.patterns>`. + + *separators* (:class:`~collections.abc.Collection` of :class:`str`; or + :data:`None`) optionally contains the path separators to normalize. See + :func:`.normalize_file` for more information. + + Returns an :class:`~collections.abc.Iterator` yielding each file check + result (:class:`.CheckResult`). + """ + if not _is_iterable(files): + raise TypeError(f"files:{files!r} is not an iterable.") + + for orig_file in files: + norm_file = normalize_file(orig_file, separators) + include, index = self._backend.match_file(norm_file) + yield CheckResult(orig_file, include, index) + + def check_tree_files( + self, + root: StrPath, + on_error: Optional[Callable[[OSError], None]] = None, + follow_links: Optional[bool] = None, + ) -> Iterator[CheckResult[str]]: + """ + Walks the specified root path for all files and checks them against this + path-spec. + + *root* (:class:`str` or :class:`os.PathLike`) is the root directory to + search for files. + + *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally + is the error handler for file-system exceptions. It will be called with the + exception (:exc:`OSError`). Reraise the exception to abort the walk. Default + is :data:`None` to ignore file-system exceptions. + + *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk + symbolic links that resolve to directories. Default is :data:`None` for + :data:`True`. + + *negate* (:class:`bool` or :data:`None`) is whether to negate the match + results of the patterns. If :data:`True`, a pattern matching a file will + exclude the file rather than include it. Default is :data:`None` for + :data:`False`. + + Returns an :class:`~collections.abc.Iterator` yielding each file check + result (:class:`.CheckResult`). + """ + files = util.iter_tree_files(root, on_error=on_error, follow_links=follow_links) + yield from self.check_files(files) + + @classmethod + def from_lines( + cls: type[Self], + pattern_factory: Union[str, Callable[[AnyStr], Pattern]], + lines: Iterable[AnyStr], + *, + backend: Union[BackendNamesHint, str, None] = None, + _test_backend_factory: Optional[Callable[[Sequence[Pattern]], _Backend]] = None, + ) -> Self: + """ + Compiles the pattern lines. + + *pattern_factory* can be either the name of a registered pattern factory + (:class:`str`), or a :class:`~collections.abc.Callable` used to compile + patterns. It must accept an uncompiled pattern (:class:`str`) and return the + compiled pattern (:class:`.Pattern`). + + *lines* (:class:`~collections.abc.Iterable`) yields each uncompiled pattern + (:class:`str`). This simply has to yield each line so that it can be a + :class:`io.TextIOBase` (e.g., from :func:`open` or :class:`io.StringIO`) or + the result from :meth:`str.splitlines`. + + *backend* (:class:`str` or :data:`None`) is the pattern (or regular + expression) matching backend to use. Default is :data:`None` for "best" to + use the best available backend. Priority of backends is: "re2", "hyperscan", + "simple". The "simple" backend is always available. + + Returns the :class:`PathSpec` instance. + """ + if isinstance(pattern_factory, str): + pattern_factory = util.lookup_pattern(pattern_factory) + + if not callable(pattern_factory): + raise TypeError(f"pattern_factory:{pattern_factory!r} is not callable.") + + if not _is_iterable(lines): + raise TypeError(f"lines:{lines!r} is not an iterable.") + + patterns = [pattern_factory(line) for line in lines if line] + return cls(patterns, backend=backend, _test_backend_factory=_test_backend_factory) + + @staticmethod + def _make_backend( + name: BackendNamesHint, + patterns: Sequence[Pattern], + ) -> _Backend: + """ + .. warning:: This method is not part of the public API. It is subject to + change. + + Create the backend for the patterns. + + *name* (:class:`str`) is the name of the backend. + + *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`) + contains the compiled patterns. + + Returns the matcher (:class:`._Backend`). + """ + return make_pathspec_backend(name, patterns) + + def match_entries( + self, + entries: Iterable[TreeEntry], + separators: Optional[Collection[str]] = None, + *, + negate: Optional[bool] = None, + ) -> Iterator[TreeEntry]: + """ + Matches the entries to this path-spec. + + *entries* (:class:`~collections.abc.Iterable` of :class:`.TreeEntry`) + contains the entries to be matched against :attr:`self.patterns <.PathSpec.patterns>`. + + *separators* (:class:`~collections.abc.Collection` of :class:`str`; or + :data:`None`) optionally contains the path separators to normalize. See + :func:`.normalize_file` for more information. + + *negate* (:class:`bool` or :data:`None`) is whether to negate the match + results of the patterns. If :data:`True`, a pattern matching a file will + exclude the file rather than include it. Default is :data:`None` for + :data:`False`. + + Returns the matched entries (:class:`~collections.abc.Iterator` of + :class:`.TreeEntry`). + """ + if not _is_iterable(entries): + raise TypeError(f"entries:{entries!r} is not an iterable.") + + for entry in entries: + norm_file = normalize_file(entry.path, separators) + include, _index = self._backend.match_file(norm_file) + + if negate: + include = not include + + if include: + yield entry + + def match_file( + self, + file: StrPath, + separators: Optional[Collection[str]] = None, + ) -> bool: + """ + Matches the file to this path-spec. + + *file* (:class:`str` or :class:`os.PathLike`) is the file path to be matched + against :attr:`self.patterns <.PathSpec.patterns>`. + + *separators* (:class:`~collections.abc.Collection` of :class:`str`) + optionally contains the path separators to normalize. See + :func:`.normalize_file` for more information. + + Returns :data:`True` if *file* matched; otherwise, :data:`False`. + """ + norm_file = normalize_file(file, separators) + include, _index = self._backend.match_file(norm_file) + return bool(include) + + def match_files( + self, + files: Iterable[StrPath], + separators: Optional[Collection[str]] = None, + *, + negate: Optional[bool] = None, + ) -> Iterator[StrPath]: + """ + Matches the files to this path-spec. + + *files* (:class:`~collections.abc.Iterable` of :class:`str` or + :class:`os.PathLike`) contains the file paths to be matched against + :attr:`self.patterns <.PathSpec.patterns>`. + + *separators* (:class:`~collections.abc.Collection` of :class:`str`; or + :data:`None`) optionally contains the path separators to normalize. See + :func:`.normalize_file` for more information. + + *negate* (:class:`bool` or :data:`None`) is whether to negate the match + results of the patterns. If :data:`True`, a pattern matching a file will + exclude the file rather than include it. Default is :data:`None` for + :data:`False`. + + Returns the matched files (:class:`~collections.abc.Iterator` of + :class:`str` or :class:`os.PathLike`). + """ + if not _is_iterable(files): + raise TypeError(f"files:{files!r} is not an iterable.") + + for orig_file in files: + norm_file = normalize_file(orig_file, separators) + include, _index = self._backend.match_file(norm_file) + + if negate: + include = not include + + if include: + yield orig_file + + def match_tree_entries( + self, + root: StrPath, + on_error: Optional[Callable[[OSError], None]] = None, + follow_links: Optional[bool] = None, + *, + negate: Optional[bool] = None, + ) -> Iterator[TreeEntry]: + """ + Walks the specified root path for all files and matches them to this + path-spec. + + *root* (:class:`str` or :class:`os.PathLike`) is the root directory to + search. + + *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally + is the error handler for file-system exceptions. It will be called with the + exception (:exc:`OSError`). Reraise the exception to abort the walk. Default + is :data:`None` to ignore file-system exceptions. + + *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk + symbolic links that resolve to directories. Default is :data:`None` for + :data:`True`. + + *negate* (:class:`bool` or :data:`None`) is whether to negate the match + results of the patterns. If :data:`True`, a pattern matching a file will + exclude the file rather than include it. Default is :data:`None` for + :data:`False`. + + Returns the matched files (:class:`~collections.abc.Iterator` of + :class:`.TreeEntry`). + """ + entries = util.iter_tree_entries(root, on_error=on_error, follow_links=follow_links) + yield from self.match_entries(entries, negate=negate) + + # NOTICE: The deprecation warning was only added in 1.0.0 (from 2026-01-05). + @deprecated(( + "PathSpec.match_tree() is deprecated. Use .match_tree_files() instead." + )) + def match_tree(self, *args, **kw) -> Iterator[str]: + """ + .. version-deprecated:: 0.3.2 + This is an alias for the :meth:`self.match_tree_files <.PathSpec.match_tree_files>` + method. + """ + return self.match_tree_files(*args, **kw) + + def match_tree_files( + self, + root: StrPath, + on_error: Optional[Callable[[OSError], None]] = None, + follow_links: Optional[bool] = None, + *, + negate: Optional[bool] = None, + ) -> Iterator[str]: + """ + Walks the specified root path for all files and matches them to this + path-spec. + + *root* (:class:`str` or :class:`os.PathLike`) is the root directory to + search for files. + + *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally + is the error handler for file-system exceptions. It will be called with the + exception (:exc:`OSError`). Reraise the exception to abort the walk. Default + is :data:`None` to ignore file-system exceptions. + + *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk + symbolic links that resolve to directories. Default is :data:`None` for + :data:`True`. + + *negate* (:class:`bool` or :data:`None`) is whether to negate the match + results of the patterns. If :data:`True`, a pattern matching a file will + exclude the file rather than include it. Default is :data:`None` for + :data:`False`. + + Returns the matched files (:class:`~collections.abc.Iterable` of :class:`str`). + """ + files = util.iter_tree_files(root, on_error=on_error, follow_links=follow_links) + yield from self.match_files(files, negate=negate) diff --git a/lib/pathspec/pattern.py b/lib/pathspec/pattern.py new file mode 100644 index 0000000..a4b8a2c --- /dev/null +++ b/lib/pathspec/pattern.py @@ -0,0 +1,241 @@ +""" +This module provides the base definition for patterns. +""" +from __future__ import annotations + +import re +from collections.abc import ( + Iterable, + Iterator) +from dataclasses import ( + dataclass) +from typing import ( + Any, + Optional, # Replaced by `X | None` in 3.10. + TypeVar, + Union) # Replaced by `X | Y` in 3.10. + +from ._typing import ( + AnyStr, # Removed in 3.18. + deprecated, # Added in 3.13. + override) # Added in 3.12. + +RegexPatternSelf = TypeVar("RegexPatternSelf", bound='RegexPattern') +""" +:class:`.RegexPattern` self type hint to support Python v<3.11 using PEP 673 +recommendation. +""" + +class Pattern(object): + """ + The :class:`Pattern` class is the abstract definition of a pattern. + """ + + # Make the class dict-less. + __slots__ = ( + 'include', + ) + + def __init__(self, include: Optional[bool]) -> None: + """ + Initializes the :class:`Pattern` instance. + + *include* (:class:`bool` or :data:`None`) is whether the matched files + should be included (:data:`True`), excluded (:data:`False`), or is a + null-operation (:data:`None`). + """ + + self.include = include + """ + *include* (:class:`bool` or :data:`None`) is whether the matched files + should be included (:data:`True`), excluded (:data:`False`), or is a + null-operation (:data:`None`). + """ + + @deprecated(( + "Pattern.match() is deprecated. Use Pattern.match_file() with a loop for " + "similar results." + )) + def match(self, files: Iterable[str]) -> Iterator[str]: + """ + .. version-deprecated:: 0.10.0 + This method is no longer used. Use the :meth:`self.match_file <.Pattern.match_file>` + method with a loop for similar results. + + Matches this pattern against the specified files. + + *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains each + file relative to the root directory. + + Returns an :class:`~collections.abc.Iterable` yielding each matched file + path (:class:`str`). + """ + for file in files: + if self.match_file(file) is not None: + yield file + + def match_file(self, file: str) -> Optional[Any]: + """ + Matches this pattern against the specified file. + + *file* (:class:`str`) is the normalized file path to match against. + + Returns the match result if *file* matched; otherwise, :data:`None`. + """ + raise NotImplementedError(( + "{cls.__module__}.{cls.__qualname__} must override match_file()." + ).format(cls=self.__class__)) + + +class RegexPattern(Pattern): + """ + The :class:`RegexPattern` class is an implementation of a pattern using + regular expressions. + """ + + # Keep the class dict-less. + __slots__ = ( + 'pattern', + 'regex', + ) + + def __init__( + self, + pattern: Union[AnyStr, re.Pattern, None], + include: Optional[bool] = None, + ) -> None: + """ + Initializes the :class:`RegexPattern` instance. + + *pattern* (:class:`str`, :class:`bytes`, :class:`re.Pattern`, or + :data:`None`) is the pattern to compile into a regular expression. + + *include* (:class:`bool` or :data:`None`) must be :data:`None` unless + *pattern* is a precompiled regular expression (:class:`re.Pattern`) in which + case it is whether matched files should be included (:data:`True`), excluded + (:data:`False`), or is a null operation (:data:`None`). + + .. note:: Subclasses do not need to support the *include* parameter. + """ + + if isinstance(pattern, (str, bytes)): + assert include is None, ( + f"include:{include!r} must be null when pattern:{pattern!r} is a string." + ) + regex, include = self.pattern_to_regex(pattern) + # NOTE: Make sure to allow a null regular expression to be + # returned for a null-operation. + if include is not None: + regex = re.compile(regex) + + elif pattern is not None and hasattr(pattern, 'match'): + # Assume pattern is a precompiled regular expression. + # - NOTE: Used specified *include*. + regex = pattern + + elif pattern is None: + # NOTE: Make sure to allow a null pattern to be passed for a + # null-operation. + assert include is None, ( + f"include:{include!r} must be null when pattern:{pattern!r} is null." + ) + regex = None + + else: + raise TypeError(f"pattern:{pattern!r} is not a string, re.Pattern, or None.") + + super(RegexPattern, self).__init__(include) + + self.pattern: Union[AnyStr, re.Pattern, None] = pattern + """ + *pattern* (:class:`str`, :class:`bytes`, :class:`re.Pattern`, or + :data:`None`) is the uncompiled, input pattern. This is for reference. + """ + + self.regex: Optional[re.Pattern] = regex + """ + *regex* (:class:`re.Pattern` or :data:`None`) is the compiled regular + expression for the pattern. + """ + + def __copy__(self: RegexPatternSelf) -> RegexPatternSelf: + """ + Performa a shallow copy of the pattern. + + Returns the copy (:class:`RegexPattern`). + """ + other = self.__class__(self.regex, self.include) + other.pattern = self.pattern + return other + + def __eq__(self, other: RegexPattern) -> bool: + """ + Tests the equality of this regex pattern with *other* (:class:`RegexPattern`) + by comparing their :attr:`~Pattern.include` and :attr:`~RegexPattern.regex` + attributes. + """ + if isinstance(other, RegexPattern): + return self.include == other.include and self.regex == other.regex + else: + return NotImplemented + + @override + def match_file(self, file: AnyStr) -> Optional[RegexMatchResult]: + """ + Matches this pattern against the specified file. + + *file* (:class:`str` or :class:`bytes`) is the file path relative to the + root directory (e.g., "relative/path/to/file"). + + Returns the match result (:class:`.RegexMatchResult`) if *file* matched; + otherwise, :data:`None`. + """ + if self.include is not None: + match = self.regex.search(file) + if match is not None: + return RegexMatchResult(match) + + return None + + @classmethod + def pattern_to_regex( + cls, + pattern: AnyStr, + ) -> tuple[Optional[AnyStr], Optional[bool]]: + """ + Convert the pattern into an uncompiled regular expression. + + *pattern* (:class:`str` or :class:`bytes`) is the pattern to convert into a + regular expression. + + Returns a :class:`tuple` containing: + + - *pattern* (:class:`str`, :class:`bytes` or :data:`None`) is the + uncompiled regular expression . + + - *include* (:class:`bool` or :data:`None`) is whether matched files + should be included (:data:`True`), excluded (:data:`False`), or is a + null-operation (:data:`None`). + + .. note:: The default implementation simply returns *pattern* and + :data:`True`. + """ + return pattern, True + + +@dataclass() +class RegexMatchResult(object): + """ + The :class:`RegexMatchResult` data class is used to return information about + the matched regular expression. + """ + + # Keep the class dict-less. + __slots__ = ( + 'match', + ) + + match: re.Match + """ + *match* (:class:`re.Match`) is the regex match result. + """ diff --git a/lib/pathspec/patterns/__init__.py b/lib/pathspec/patterns/__init__.py new file mode 100644 index 0000000..f1738a5 --- /dev/null +++ b/lib/pathspec/patterns/__init__.py @@ -0,0 +1,12 @@ +""" +The *pathspec.patterns* package contains the pattern matching implementations. +""" + +# Load pattern implementations. +from .gitignore import basic as _ +from .gitignore import spec as _ + +# DEPRECATED: Deprecated since 0.11.0 (from 2023-01-24). Expose the +# GitWildMatchPattern class in this module for backward compatibility with +# 0.5.0 (from 2016-08-22). +from .gitwildmatch import GitWildMatchPattern diff --git a/lib/pathspec/patterns/__pycache__/__init__.cpython-314.pyc b/lib/pathspec/patterns/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6e37d0f7fff45f62f866bca256c086f61677b25 GIT binary patch literal 393 zcmXw$y-ve05XbE#Z6PWw42-p4YEcsw1|)>UL_2`03W*KNO{~Of;#kfh)J{AEFTtzu z2$7iBm_QX`(jpi?@gBMd&Xt>q-V%Ox8a0eNj?5+Q)Jx*jkilDWtvODYXQ>6!s1ZmwA= zAjy@^m?}*nONl90qf$^V4gey8bho>mFx&2FYZ zWxBG`x^)`Lt?#sp@h2=?kFx=WUdakQX|7@@O>C4Ig0Pu{;W;c!m7bl(g_^}?0ePk4 z?N$!fTg99!t$_2$ESjB*vPvHdd1<@Y5ucURhv>e1O6j-f(cmW}^yrxK zc~x1-ay>Dvp6nDU0dCJ++rjafcU`_3=ik9)ZYQf?yfBOp zVz(Fzm^F%TNa@!YHNhlZXaFZjEnT1C(pd;djc~bJO`dQnlOx*8QZnE zVT2;_g>H1K%(vU+w%cCdCTlr-;Tqp*2lktbWzXFxw|Dqf;Fph?6i%%7@+cE}dnd{p zhUxnO$2(;hk$`<#1^Q@45jPKK(EQznBao@KFU+Hx@u!b*h$#?`XhQQ-0Qvq^bOZ$- ziD=yJNOfq+fDc5Sl%YdX zNlA|6(D61S8JP$pdhdRe9dL$x;T~}1b#<`k0W#MH~x-GZV~5xmdf~4(icn z@tF%)6x5q*87NBTZgjX4;sVc%$lP)~4@1YwPr68#0Kq2F#J6B+DXFOLE9ZjQbTzrCVH%)|Kxvn>nmQH%gx;TZSusRPb7 zik%RH8@oHG<=wpmmm^Nvn!%{lJP9oTZi+I@1c&V`G%*2Q6H!{n;|LQRgkM@+nPKLTTmII!(E* zF)1m1LbKY=>MVj>M_T9(Fq3iU9Kb}Ybbl|BVi4&Gj8qZLSFAx}WAx)vM5!nQP#Wq< uwQ!iL^hWX{vuL&L|Dkfz9#a=%{OAC+1>G)wZY&JyJ2hybX`~n;!xrc2q6Y+0x^TLF2eROo*nEV>)FYj zS&ZF}PDq@Rh6q;z$&va&rG6xeD&@2NOVz)AIk6+G5g(RXDN4TY~ehv7ou8bx@Bu!+(V?vT_9`8t^wvTnapLBU@d52Pp!lZcGtf zVOY$O=c&RqlR`q_p>OCyd}waGvKI^Xfa5iL3BXO}h$|^^;4tBDS5KR6Z$R~{nqyRm zoc=<}X5~qZS*A7C-7N=F2-uZD99Gc<4w)+#uLgYErO|?^%P0DKvvRMC5Kd|InxVV0 zTa@HV)5kSy%*aY0QA6$y&g#Ch>E%~K!Qwe6)lnczHYe+9}0G5FLXz#M{;C@JMtbOd01^F7w9-u=tX)N)Cc=c4n)I$%K@-Y z9baUITVfW&6Ed7H!sQd1>({Glz(wBFT~&1uy4~|nce)dHr)}B?T#bzx?tvHGDZ7|| z;knL&IofGYx#LBv6Vzkeu?-yufu3|$Ri_i#&Dv9bN>w!rB(1rIs`~AlN-Db+W)~3P zv_SX0N1l+Uxb2Uy``~ℜ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 L_y3t-g=+G@4Ffrb literal 0 HcmV?d00001 diff --git a/lib/pathspec/patterns/gitignore/base.py b/lib/pathspec/patterns/gitignore/base.py new file mode 100644 index 0000000..0e1dd3c --- /dev/null +++ b/lib/pathspec/patterns/gitignore/base.py @@ -0,0 +1,176 @@ +""" +This module provides common classes for the gitignore patterns. +""" + +import re + +from pathspec.pattern import ( + RegexPattern) +from pathspec._typing import ( + AnyStr) # Removed in 3.18. + +_BYTES_ENCODING = 'latin1' +""" +The encoding to use when parsing a byte string pattern. +""" + + +class _GitIgnoreBasePattern(RegexPattern): + """ + .. warning:: This class is not part of the public API. It is subject to + change. + + The :class:`_GitIgnoreBasePattern` class is the base implementation for a + compiled gitignore pattern. + """ + + # Keep the dict-less class hierarchy. + __slots__ = () + + @staticmethod + def escape(s: AnyStr) -> AnyStr: + """ + Escape special characters in the given string. + + *s* (:class:`str` or :class:`bytes`) a filename or a string that you want to + escape, usually before adding it to a ".gitignore". + + Returns the escaped string (:class:`str` or :class:`bytes`). + """ + if isinstance(s, str): + return_type = str + string = s + elif isinstance(s, bytes): + return_type = bytes + string = s.decode(_BYTES_ENCODING) + else: + raise TypeError(f"s:{s!r} is not a unicode or byte string.") + + # Reference: https://git-scm.com/docs/gitignore#_pattern_format + out_string = ''.join((f"\\{x}" if x in '[]!*#?' else x) for x in string) + + if return_type is bytes: + return out_string.encode(_BYTES_ENCODING) + else: + return out_string + + @staticmethod + def _translate_segment_glob(pattern: str) -> str: + """ + Translates the glob pattern to a regular expression. This is used in the + constructor to translate a path segment glob pattern to its corresponding + regular expression. + + *pattern* (:class:`str`) is the glob pattern. + + Returns the regular expression (:class:`str`). + """ + # NOTE: This is derived from `fnmatch.translate()` and is similar to the + # POSIX function `fnmatch()` with the `FNM_PATHNAME` flag set. + + escape = False + regex = '' + i, end = 0, len(pattern) + while i < end: + # Get next character. + char = pattern[i] + i += 1 + + if escape: + # Escape the character. + escape = False + regex += re.escape(char) + + elif char == '\\': + # Escape character, escape next character. + escape = True + + elif char == '*': + # Multi-character wildcard. Match any string (except slashes), including + # an empty string. + regex += '[^/]*' + + elif char == '?': + # Single-character wildcard. Match any single character (except a + # slash). + regex += '[^/]' + + elif char == '[': + # Bracket expression wildcard. Except for the beginning exclamation + # mark, the whole bracket expression can be used directly as regex, but + # we have to find where the expression ends. + # - "[][!]" matches ']', '[' and '!'. + # - "[]-]" matches ']' and '-'. + # - "[!]a-]" matches any character except ']', 'a' and '-'. + j = i + + # Pass bracket expression negation. + if j < end and (pattern[j] == '!' or pattern[j] == '^'): + j += 1 + + # Pass first closing bracket if it is at the beginning of the + # expression. + if j < end and pattern[j] == ']': + j += 1 + + # Find closing bracket. Stop once we reach the end or find it. + while j < end and pattern[j] != ']': + j += 1 + + if j < end: + # Found end of bracket expression. Increment j to be one past the + # closing bracket: + # + # [...] + # ^ ^ + # i j + # + j += 1 + expr = '[' + + if pattern[i] == '!': + # Bracket expression needs to be negated. + expr += '^' + i += 1 + elif pattern[i] == '^': + # POSIX declares that the regex bracket expression negation "[^...]" + # is undefined in a glob pattern. Python's `fnmatch.translate()` + # escapes the caret ('^') as a literal. Git supports the using a + # caret for negation. Maintain consistency with Git because that is + # the expected behavior. + expr += '^' + i += 1 + + # Build regex bracket expression. Escape slashes so they are treated + # as literal slashes by regex as defined by POSIX. + expr += pattern[i:j].replace('\\', '\\\\') + + # Add regex bracket expression to regex result. + regex += expr + + # Set i to one past the closing bracket. + i = j + + else: + # Failed to find closing bracket, treat opening bracket as a bracket + # literal instead of as an expression. + regex += '\\[' + + else: + # Regular character, escape it for regex. + regex += re.escape(char) + + if escape: + raise ValueError(( + f"Escape character found with no next character to escape: {pattern!r}" + )) # ValueError + + return regex + + +class GitIgnorePatternError(ValueError): + """ + The :class:`GitIgnorePatternError` class indicates an invalid gitignore + pattern. + """ + pass diff --git a/lib/pathspec/patterns/gitignore/basic.py b/lib/pathspec/patterns/gitignore/basic.py new file mode 100644 index 0000000..95d7915 --- /dev/null +++ b/lib/pathspec/patterns/gitignore/basic.py @@ -0,0 +1,317 @@ +""" +This module provides :class:`GitIgnoreBasicPattern` which implements Git's +`gitignore`_ patterns as documented. This differs from how Git actually behaves +when including files in excluded directories. + +.. _`gitignore`: https://git-scm.com/docs/gitignore +""" + +from typing import ( + Optional) # Replaced by `X | None` in 3.10. + +from pathspec import util +from pathspec._typing import ( + AnyStr, # Removed in 3.18. + assert_unreachable, + override) # Added in 3.12. + +from .base import ( + GitIgnorePatternError, + _BYTES_ENCODING, + _GitIgnoreBasePattern) + + +class GitIgnoreBasicPattern(_GitIgnoreBasePattern): + """ + The :class:`GitIgnoreBasicPattern` class represents a compiled gitignore + pattern as documented. This is registered as "gitignore". + """ + + # Keep the dict-less class hierarchy. + __slots__ = () + + @staticmethod + def __normalize_segments( + is_dir_pattern: bool, + pattern_segs: list[str], + ) -> tuple[Optional[list[str]], Optional[str]]: + """ + Normalize the pattern segments to make processing easier. + + *is_dir_pattern* (:class:`bool`) is whether the pattern is a directory + pattern (i.e., ends with a slash '/'). + + *pattern_segs* (:class:`list` of :class:`str`) contains the pattern + segments. This may be modified in place. + + Returns a :class:`tuple` containing either: + + - The normalized segments (:class:`list` of :class:`str`; or :data:`None`). + + - The regular expression override (:class:`str` or :data:`None`). + """ + if not pattern_segs[0]: + # A pattern beginning with a slash ('/') should match relative to the root + # directory. Remove the empty first segment to make the pattern relative + # to root. + del pattern_segs[0] + + elif len(pattern_segs) == 1 or (len(pattern_segs) == 2 and not pattern_segs[1]): + # A single segment pattern with or without a trailing slash ('/') will + # match any descendant path. This is equivalent to "**/{pattern}". Prepend + # double-asterisk segment to make pattern relative to root. + if pattern_segs[0] != '**': + pattern_segs.insert(0, '**') + + else: + # A pattern without a beginning slash ('/') but contains at least one + # prepended directory (e.g., "dir/{pattern}") should match relative to the + # root directory. No segment modification is needed. + pass + + if not pattern_segs: + # After normalization, we end up with no pattern at all. This must be + # because the pattern is invalid. + raise ValueError("Pattern normalized to nothing.") + + if not pattern_segs[-1]: + # A pattern ending with a slash ('/') will match all descendant paths if + # it is a directory but not if it is a regular file. This is equivalent to + # "{pattern}/**". Set empty last segment to a double-asterisk to include + # all descendants. + pattern_segs[-1] = '**' + + # EDGE CASE: Collapse duplicate double-asterisk sequences (i.e., '**/**'). + # Iterate over the segments in reverse order and remove the duplicate double + # asterisks as we go. + for i in range(len(pattern_segs) - 1, 0, -1): + prev = pattern_segs[i-1] + seg = pattern_segs[i] + if prev == '**' and seg == '**': + del pattern_segs[i] + + seg_count = len(pattern_segs) + if seg_count == 1 and pattern_segs[0] == '**': + if is_dir_pattern: + # The pattern "**/" will be normalized to "**", but it should match + # everything except for files in the root. Special case this pattern. + return (None, '/') + else: + # The pattern "**" will match every path. Special case this pattern. + return (None, '.') + + elif ( + seg_count == 2 + and pattern_segs[0] == '**' + and pattern_segs[1] == '*' + ): + # The pattern "*" will be normalized to "**/*" and will match every + # path. Special case this pattern for efficiency. + return (None, '.') + + elif ( + seg_count == 3 + and pattern_segs[0] == '**' + and pattern_segs[1] == '*' + and pattern_segs[2] == '**' + ): + # The pattern "*/" will be normalized to "**/*/**" which will match every + # file not in the root directory. Special case this pattern for + # efficiency. + return (None, '/') + + # No regular expression override, return modified pattern segments. + return (pattern_segs, None) + + @override + @classmethod + def pattern_to_regex( + cls, + pattern: AnyStr, + ) -> tuple[Optional[AnyStr], Optional[bool]]: + """ + Convert the pattern into a regular expression. + + *pattern* (:class:`str` or :class:`bytes`) is the pattern to convert into a + regular expression. + + Returns a :class:`tuple` containing: + + - *pattern* (:class:`str`, :class:`bytes` or :data:`None`) is the + uncompiled regular expression. + + - *include* (:class:`bool` or :data:`None`) is whether matched files + should be included (:data:`True`), excluded (:data:`False`), or is a + null-operation (:data:`None`). + """ + if isinstance(pattern, str): + pattern_str = pattern + return_type = str + elif isinstance(pattern, bytes): + pattern_str = pattern.decode(_BYTES_ENCODING) + return_type = bytes + else: + raise TypeError(f"{pattern=!r} is not a unicode or byte string.") + + original_pattern = pattern_str + del pattern + + if pattern_str.endswith('\\ '): + # EDGE CASE: Spaces can be escaped with backslash. If a pattern that ends + # with a backslash is followed by a space, do not strip from the left. + pass + else: + # EDGE CASE: Leading spaces should be kept (only trailing spaces should be + # removed). + pattern_str = pattern_str.rstrip() + + regex: Optional[str] + include: Optional[bool] + + if not pattern_str: + # A blank pattern is a null-operation (neither includes nor excludes + # files). + return (None, None) + + elif pattern_str.startswith('#'): + # A pattern starting with a hash ('#') serves as a comment (neither + # includes nor excludes files). Escape the hash with a backslash to match + # a literal hash (i.e., '\#'). + return (None, None) + + if pattern_str.startswith('!'): + # A pattern starting with an exclamation mark ('!') negates the pattern + # (exclude instead of include). Escape the exclamation mark with a back + # slash to match a literal exclamation mark (i.e., '\!'). + include = False + # Remove leading exclamation mark. + pattern_str = pattern_str[1:] + else: + include = True + + # Split pattern into segments. + pattern_segs = pattern_str.split('/') + + # Check whether the pattern is specifically a directory pattern before + # normalization. + is_dir_pattern = not pattern_segs[-1] + + if pattern_str == '/': + # EDGE CASE: A single slash ('/') is not addressed by the gitignore + # documentation. Git treats it as a no-op (does not match any files). The + # straight forward interpretation is to treat it as a directory and match + # every descendant path (equivalent to '**'). Remove the directory pattern + # flag so that it is treated as '**' instead of '**/'. + is_dir_pattern = False + + # Normalize pattern to make processing easier. + try: + pattern_segs, override_regex = cls.__normalize_segments( + is_dir_pattern, pattern_segs, + ) + except ValueError as e: + raise GitIgnorePatternError(( + f"Invalid git pattern: {original_pattern!r}" + )) from e # GitIgnorePatternError + + if override_regex is not None: + # Use regex override. + regex = override_regex + + elif pattern_segs is not None: + # Build regular expression from pattern. + try: + regex_parts = cls.__translate_segments(pattern_segs) + except ValueError as e: + raise GitIgnorePatternError(( + f"Invalid git pattern: {original_pattern!r}" + )) from e # GitIgnorePatternError + + regex = ''.join(regex_parts) + + else: + assert_unreachable(( + f"{override_regex=} and {pattern_segs=} cannot both be null." + )) # assert_unreachable + + # Encode regex if needed. + out_regex: AnyStr + if regex is not None and return_type is bytes: + out_regex = regex.encode(_BYTES_ENCODING) + else: + out_regex = regex + + return (out_regex, include) + + @classmethod + def __translate_segments(cls, pattern_segs: list[str]) -> list[str]: + """ + Translate the pattern segments to regular expressions. + + *pattern_segs* (:class:`list` of :class:`str`) contains the pattern + segments. + + Returns the regular expression parts (:class:`list` of :class:`str`). + """ + # Build regular expression from pattern. + out_parts = [] + need_slash = False + end = len(pattern_segs) - 1 + for i, seg in enumerate(pattern_segs): + if seg == '**': + if i == 0: + # A normalized pattern beginning with double-asterisks ('**') will + # match any leading path segments. + # - NOTICE: '(?:^|/)' benchmarks slower using p15 (sm=0.9382, + # hs=0.9966, re2=0.9337). + out_parts.append('^(?:.+/)?') + + elif i < end: + # A pattern with inner double-asterisks ('**') will match multiple (or + # zero) inner path segments. + out_parts.append('(?:/.+)?') + need_slash = True + + else: + assert i == end, (i, end) + # A normalized pattern ending with double-asterisks ('**') will match + # any trailing path segments. + out_parts.append('/') + + else: + # Match path segment. + if i == 0: + # Anchor to root directory. + out_parts.append('^') + + if need_slash: + out_parts.append('/') + + if seg == '*': + # Match whole path segment. + out_parts.append('[^/]+') + + else: + # Match segment glob pattern. + out_parts.append(cls._translate_segment_glob(seg)) + + if i == end: + if seg == '*': + # A pattern ending with an asterisk ('*') will match a file or + # directory (without matching descendant paths). E.g., "foo/*" + # matches "foo/test.json", "foo/bar/", but not "foo/bar/hello.c". + out_parts.append('/?$') + + else: + # A pattern ending without a slash ('/') will match a file or a + # directory (with paths underneath it). E.g., "foo" matches "foo", + # "foo/bar", "foo/bar/baz", etc. + out_parts.append('(?:/|$)') + + need_slash = True + + return out_parts + + +# Register GitIgnoreBasicPattern as "gitignore". +util.register_pattern('gitignore', GitIgnoreBasicPattern) diff --git a/lib/pathspec/patterns/gitignore/spec.py b/lib/pathspec/patterns/gitignore/spec.py new file mode 100644 index 0000000..ee77457 --- /dev/null +++ b/lib/pathspec/patterns/gitignore/spec.py @@ -0,0 +1,335 @@ +""" +This module provides :class:`GitIgnoreSpecPattern` which implements Git's +`gitignore`_ patterns, and handles edge-cases where Git's behavior differs from +what's documented. Git allows including files from excluded directories which +appears to contradict the documentation. This is used by +:class:`~pathspec.gitignore.GitIgnoreSpec` to fully replicate Git's handling. + +.. _`gitignore`: https://git-scm.com/docs/gitignore +""" + +from typing import ( + Optional) # Replaced by `X | None` in 3.10. + +from pathspec._typing import ( + AnyStr, # Removed in 3.18. + assert_unreachable, + override) # Added in 3.12. + +from .base import ( + GitIgnorePatternError, + _BYTES_ENCODING, + _GitIgnoreBasePattern) + +_DIR_MARK = 'ps_d' +""" +The regex group name for the directory marker. This is only used by +:class:`GitIgnoreSpec`. +""" + +_DIR_MARK_CG = f'(?P<{_DIR_MARK}>/)' +""" +This regular expression matches the directory marker. +""" + +_DIR_MARK_OPT = f'(?:{_DIR_MARK_CG}|$)' +""" +This regular expression matches the optional directory marker and sub-path. +""" + + +class GitIgnoreSpecPattern(_GitIgnoreBasePattern): + """ + The :class:`GitIgnoreSpecPattern` class represents a compiled gitignore + pattern with special handling for edge-cases to replicate Git's behavior. + + This is registered under the deprecated name "gitwildmatch" for backward + compatibility with v0.12. The registered name will be removed in a future + version. + """ + + # Keep the dict-less class hierarchy. + __slots__ = () + + @staticmethod + def __normalize_segments( + is_dir_pattern: bool, + pattern_segs: list[str], + ) -> tuple[Optional[list[str]], Optional[str]]: + """ + Normalize the pattern segments to make processing easier. + + *is_dir_pattern* (:class:`bool`) is whether the pattern is a directory + pattern (i.e., ends with a slash '/'). + + *pattern_segs* (:class:`list` of :class:`str`) contains the pattern + segments. This may be modified in place. + + Returns a :class:`tuple` containing either: + + - The normalized segments (:class:`list` of :class:`str`; or :data:`None`). + + - The regular expression override (:class:`str` or :data:`None`). + """ + if not pattern_segs[0]: + # A pattern beginning with a slash ('/') should match relative to the root + # directory. Remove the empty first segment to make the pattern relative + # to root. + del pattern_segs[0] + + elif len(pattern_segs) == 1 or (len(pattern_segs) == 2 and not pattern_segs[1]): + # A single segment pattern with or without a trailing slash ('/') will + # match any descendant path. This is equivalent to "**/{pattern}". Prepend + # double-asterisk segment to make pattern relative to root. + if pattern_segs[0] != '**': + pattern_segs.insert(0, '**') + + else: + # A pattern without a beginning slash ('/') but contains at least one + # prepended directory (e.g., "dir/{pattern}") should match relative to the + # root directory. No segment modification is needed. + pass + + if not pattern_segs: + # After normalization, we end up with no pattern at all. This must be + # because the pattern is invalid. + raise ValueError("Pattern normalized to nothing.") + + if not pattern_segs[-1]: + # A pattern ending with a slash ('/') will match all descendant paths if + # it is a directory but not if it is a regular file. This is equivalent to + # "{pattern}/**". Set empty last segment to a double-asterisk to include + # all descendants. + pattern_segs[-1] = '**' + + # EDGE CASE: Collapse duplicate double-asterisk sequences (i.e., '**/**'). + # Iterate over the segments in reverse order and remove the duplicate double + # asterisks as we go. + for i in range(len(pattern_segs) - 1, 0, -1): + prev = pattern_segs[i-1] + seg = pattern_segs[i] + if prev == '**' and seg == '**': + del pattern_segs[i] + + seg_count = len(pattern_segs) + if seg_count == 1 and pattern_segs[0] == '**': + if is_dir_pattern: + # The pattern "**/" will be normalized to "**", but it should match + # everything except for files in the root. Special case this pattern. + return (None, _DIR_MARK_CG) + else: + # The pattern "**" will match every path. Special case this pattern. + return (None, '.') + + elif ( + seg_count == 2 + and pattern_segs[0] == '**' + and pattern_segs[1] == '*' + ): + # The pattern "*" will be normalized to "**/*" and will match every + # path. Special case this pattern for efficiency. + return (None, '.') + + elif ( + seg_count == 3 + and pattern_segs[0] == '**' + and pattern_segs[1] == '*' + and pattern_segs[2] == '**' + ): + # The pattern "*/" will be normalized to "**/*/**" which will match every + # file not in the root directory. Special case this pattern for + # efficiency. + if is_dir_pattern: + return (None, _DIR_MARK_CG) + else: + return (None, '/') + + # No regular expression override, return modified pattern segments. + return (pattern_segs, None) + + @override + @classmethod + def pattern_to_regex( + cls, + pattern: AnyStr, + ) -> tuple[Optional[AnyStr], Optional[bool]]: + """ + Convert the pattern into a regular expression. + + *pattern* (:class:`str` or :class:`bytes`) is the pattern to convert into a + regular expression. + + Returns a :class:`tuple` containing: + + - *pattern* (:class:`str`, :class:`bytes` or :data:`None`) is the + uncompiled regular expression. + + - *include* (:class:`bool` or :data:`None`) is whether matched files + should be included (:data:`True`), excluded (:data:`False`), or is a + null-operation (:data:`None`). + """ + if isinstance(pattern, str): + pattern_str = pattern + return_type = str + elif isinstance(pattern, bytes): + pattern_str = pattern.decode(_BYTES_ENCODING) + return_type = bytes + else: + raise TypeError(f"{pattern=!r} is not a unicode or byte string.") + + original_pattern = pattern_str + del pattern + + if pattern_str.endswith('\\ '): + # EDGE CASE: Spaces can be escaped with backslash. If a pattern that ends + # with a backslash is followed by a space, do not strip from the left. + pass + else: + # EDGE CASE: Leading spaces should be kept (only trailing spaces should be + # removed). Git does not remove leading spaces. + pattern_str = pattern_str.rstrip() + + regex: Optional[str] + include: Optional[bool] + + if not pattern_str: + # A blank pattern is a null-operation (neither includes nor excludes + # files). + return (None, None) + + elif pattern_str.startswith('#'): + # A pattern starting with a hash ('#') serves as a comment (neither + # includes nor excludes files). Escape the hash with a backslash to match + # a literal hash (i.e., '\#'). + return (None, None) + + elif pattern_str == '/': + # EDGE CASE: According to `git check-ignore` (v2.4.1), a single '/' does + # not match any file. + return (None, None) + + if pattern_str.startswith('!'): + # A pattern starting with an exclamation mark ('!') negates the pattern + # (exclude instead of include). Escape the exclamation mark with a back + # slash to match a literal exclamation mark (i.e., '\!'). + include = False + # Remove leading exclamation mark. + pattern_str = pattern_str[1:] + else: + include = True + + # Split pattern into segments. + pattern_segs = pattern_str.split('/') + + # Check whether the pattern is specifically a directory pattern before + # normalization. + is_dir_pattern = not pattern_segs[-1] + + # Normalize pattern to make processing easier. + try: + pattern_segs, override_regex = cls.__normalize_segments( + is_dir_pattern, pattern_segs, + ) + except ValueError as e: + raise GitIgnorePatternError(( + f"Invalid git pattern: {original_pattern!r}" + )) from e # GitIgnorePatternError + + if override_regex is not None: + # Use regex override. + regex = override_regex + + elif pattern_segs is not None: + # Build regular expression from pattern. + try: + regex_parts = cls.__translate_segments(is_dir_pattern, pattern_segs) + except ValueError as e: + raise GitIgnorePatternError(( + f"Invalid git pattern: {original_pattern!r}" + )) from e # GitIgnorePatternError + + regex = ''.join(regex_parts) + + else: + assert_unreachable(( + f"{override_regex=} and {pattern_segs=} cannot both be null." + )) # assert_unreachable + + # Encode regex if needed. + out_regex: AnyStr + if regex is not None and return_type is bytes: + out_regex = regex.encode(_BYTES_ENCODING) + else: + out_regex = regex + + return (out_regex, include) + + @classmethod + def __translate_segments( + cls, + is_dir_pattern: bool, + pattern_segs: list[str], + ) -> list[str]: + """ + Translate the pattern segments to regular expressions. + + *is_dir_pattern* (:class:`bool`) is whether the pattern is a directory + pattern (i.e., ends with a slash '/'). + + *pattern_segs* (:class:`list` of :class:`str`) contains the pattern + segments. + + Returns the regular expression parts (:class:`list` of :class:`str`). + """ + # Build regular expression from pattern. + out_parts = [] + need_slash = False + end = len(pattern_segs) - 1 + for i, seg in enumerate(pattern_segs): + if seg == '**': + if i == 0: + # A normalized pattern beginning with double-asterisks ('**') will + # match any leading path segments. + out_parts.append('^(?:.+/)?') + + elif i < end: + # A pattern with inner double-asterisks ('**') will match multiple (or + # zero) inner path segments. + out_parts.append('(?:/.+)?') + need_slash = True + + else: + assert i == end, (i, end) + # A normalized pattern ending with double-asterisks ('**') will match + # any trailing path segments. + if is_dir_pattern: + out_parts.append(_DIR_MARK_CG) + else: + out_parts.append('/') + + else: + # Match path segment. + if i == 0: + # Anchor to root directory. + out_parts.append('^') + + if need_slash: + out_parts.append('/') + + if seg == '*': + # Match whole path segment. + out_parts.append('[^/]+') + + else: + # Match segment glob pattern. + out_parts.append(cls._translate_segment_glob(seg)) + + if i == end: + # A pattern ending without a slash ('/') will match a file or a + # directory (with paths underneath it). E.g., "foo" matches "foo", + # "foo/bar", "foo/bar/baz", etc. + out_parts.append(_DIR_MARK_OPT) + + need_slash = True + + return out_parts diff --git a/lib/pathspec/patterns/gitwildmatch.py b/lib/pathspec/patterns/gitwildmatch.py new file mode 100644 index 0000000..b44d961 --- /dev/null +++ b/lib/pathspec/patterns/gitwildmatch.py @@ -0,0 +1,52 @@ +""" +.. version-deprecated: 1.0.0 + This module is superseded by :module:`pathspec.patterns.gitignore`. +""" + +from pathspec import util +from pathspec._typing import ( + deprecated, # Added in 3.13. + override) # Added in 3.12. + +from .gitignore.spec import ( + GitIgnoreSpecPattern) + +# DEPRECATED: Deprecated since version 1.0.0. Expose GitWildMatchPatternError +# in this module for backward compatibility. +from .gitignore import ( + GitIgnorePatternError as GitWildMatchPatternError) + + +class GitWildMatchPattern(GitIgnoreSpecPattern): + """ + .. version-deprecated:: 1.0.0 + This class is superseded by :class:`GitIgnoreSpecPattern` and + :class:`~pathspec.patterns.gitignore.basic.GitIgnoreBasicPattern`. + """ + + @deprecated(( + "GitWildMatchPattern ('gitwildmatch') is deprecated. Use 'gitignore' for " + "GitIgnoreBasicPattern or GitIgnoreSpecPattern instead." + )) + def __init__(self, *args, **kw) -> None: + """ + Warn about deprecation. + """ + super().__init__(*args, **kw) + + @override + @classmethod + @deprecated(( + "GitWildMatchPattern ('gitwildmatch') is deprecated. Use 'gitignore' for " + "GitIgnoreBasicPattern or GitIgnoreSpecPattern instead." + )) + def pattern_to_regex(cls, *args, **kw): + """ + Warn about deprecation. + """ + return super().pattern_to_regex(*args, **kw) + + +# DEPRECATED: Deprecated since version 1.0.0. Register GitWildMatchPattern as +# "gitwildmatch" for backward compatibility. +util.register_pattern('gitwildmatch', GitWildMatchPattern) diff --git a/lib/pathspec/py.typed b/lib/pathspec/py.typed new file mode 100644 index 0000000..b01eaaf --- /dev/null +++ b/lib/pathspec/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. The pathspec package uses inline types. diff --git a/lib/pathspec/util.py b/lib/pathspec/util.py new file mode 100644 index 0000000..ea2dbee --- /dev/null +++ b/lib/pathspec/util.py @@ -0,0 +1,847 @@ +""" +This module provides utility methods for dealing with path-specs. +""" + +import os +import os.path +import pathlib +import posixpath +import stat +from collections.abc import ( + Collection, + Iterable, + Iterator, + Sequence) +from dataclasses import ( + dataclass) +from typing import ( + Any, + Callable, # Replaced by `collections.abc.Callable` in 3.9.2. + Generic, + Optional, # Replaced by `X | None` in 3.10. + TypeVar, + Union) # Replaced by `X | Y` in 3.10. + +from .pattern import ( + Pattern) +from ._typing import ( + AnyStr, # Removed in 3.18. + deprecated) # Added in 3.13. + +StrPath = Union[str, os.PathLike[str]] + +TStrPath = TypeVar("TStrPath", bound=StrPath) +""" +Type variable for :class:`str` or :class:`os.PathLike`. +""" + +NORMALIZE_PATH_SEPS = [ + __sep + for __sep in [os.sep, os.altsep] + if __sep and __sep != posixpath.sep +] +""" +*NORMALIZE_PATH_SEPS* (:class:`list` of :class:`str`) contains the path +separators that need to be normalized to the POSIX separator for the current +operating system. The separators are determined by examining :data:`os.sep` and +:data:`os.altsep`. +""" + +_registered_patterns = {} +""" +*_registered_patterns* (:class:`dict`) maps a name (:class:`str`) to the +registered pattern factory (:class:`~collections.abc.Callable`). +""" + + +def append_dir_sep(path: pathlib.Path) -> str: + """ + Appends the path separator to the path if the path is a directory. This can be + used to aid in distinguishing between directories and files on the file-system + by relying on the presence of a trailing path separator. + + *path* (:class:`pathlib.Path`) is the path to use. + + Returns the path (:class:`str`). + """ + str_path = str(path) + if path.is_dir(): + str_path += os.sep + + return str_path + + +def check_match_file( + patterns: Iterable[tuple[int, Pattern]], + file: str, + is_reversed: Optional[bool] = None, +) -> tuple[Optional[bool], Optional[int]]: + """ + Check the file against the patterns. + + *patterns* (:class:`~collections.abc.Iterable`) yields each indexed pattern + (:class:`tuple`) which contains the pattern index (:class:`int`) and actua + pattern (:class:`.Pattern`). + + *file* (:class:`str`) is the normalized file path to be matched against + *patterns*. + + *is_reversed* (:class:`bool` or :data:`None`) is whether the order of the + patterns has been reversed. Default is :data:`None` for :data:`False`. + Reversing the order of the patterns is an optimization. + + Returns a :class:`tuple` containing whether to include *file* (:class:`bool` + or :data:`None`), and the index of the last matched pattern (:class:`int` or + :data:`None`). + """ + if is_reversed: + # Check patterns in reverse order. The first pattern that matches takes + # precedence. + for index, pattern in patterns: + if pattern.include is not None and pattern.match_file(file) is not None: + return pattern.include, index + + return None, None + + else: + # Check all patterns. The last pattern that matches takes precedence. + out_include: Optional[bool] = None + out_index: Optional[int] = None + for index, pattern in patterns: + if pattern.include is not None and pattern.match_file(file) is not None: + out_include = pattern.include + out_index = index + + return out_include, out_index + + +def detailed_match_files( + patterns: Iterable[Pattern], + files: Iterable[str], + all_matches: Optional[bool] = None, +) -> dict[str, 'MatchDetail']: + """ + Matches the files to the patterns, and returns which patterns matched the + files. + + *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains + the patterns to use. + + *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains the + normalized file paths to be matched against *patterns*. + + *all_matches* (:class:`bool` or :data:`None`) is whether to return all matches + patterns (:data:`True`), or only the last matched pattern (:data:`False`). + Default is :data:`None` for :data:`False`. + + Returns the matched files (:class:`dict`) which maps each matched file + (:class:`str`) to the patterns that matched in order (:class:`.MatchDetail`). + """ + all_files = files if isinstance(files, Collection) else list(files) + return_files = {} + for pattern in patterns: + if pattern.include is not None: + result_files = pattern.match(all_files) # TODO: Replace with `.match_file()`. + if pattern.include: + # Add files and record pattern. + for result_file in result_files: + if result_file in return_files: + if all_matches: + return_files[result_file].patterns.append(pattern) + else: + return_files[result_file].patterns[0] = pattern + else: + return_files[result_file] = MatchDetail([pattern]) + + else: + # Remove files. + for file in result_files: + del return_files[file] + + return return_files + + +def _filter_check_patterns( + patterns: Iterable[Pattern], +) -> list[tuple[int, Pattern]]: + """ + Filters out null-patterns. + + *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains + the patterns. + + Returns a :class:`list` containing each indexed pattern (:class:`tuple`) which + contains the pattern index (:class:`int`) and the actual pattern + (:class:`.Pattern`). + """ + return [ + (__index, __pat) + for __index, __pat in enumerate(patterns) + if __pat.include is not None + ] + + +def _is_iterable(value: Any) -> bool: + """ + Check whether the value is an iterable (excludes strings). + + *value* is the value to check, + + Returns whether *value* is an iterable (:class:`bool`). + """ + return isinstance(value, Iterable) and not isinstance(value, (str, bytes)) + + +@deprecated(( + "pathspec.util.iter_tree() is deprecated. Use iter_tree_files() instead." +)) +def iter_tree(root, on_error=None, follow_links=None): + """ + .. version-deprecated:: 0.10.0 + This is an alias for the :func:`.iter_tree_files` function. + """ + return iter_tree_files(root, on_error=on_error, follow_links=follow_links) + + +def iter_tree_entries( + root: StrPath, + on_error: Optional[Callable[[OSError], None]] = None, + follow_links: Optional[bool] = None, +) -> Iterator['TreeEntry']: + """ + Walks the specified directory for all files and directories. + + *root* (:class:`str` or :class:`os.PathLike`) is the root directory to search. + + *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is + the error handler for file-system exceptions. It will be called with the + exception (:exc:`OSError`). Reraise the exception to abort the walk. Default + is :data:`None` to ignore file-system exceptions. + + *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk + symbolic links that resolve to directories. Default is :data:`None` for + :data:`True`. + + Raises :exc:`.RecursionError` if recursion is detected. + + Returns an :class:`~collections.abc.Iterator` yielding each file or directory + entry (:class:`.TreeEntry`) relative to *root*. + """ + if on_error is not None and not callable(on_error): + raise TypeError(f"on_error:{on_error!r} is not callable.") + + if follow_links is None: + follow_links = True + + yield from _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links) + + +def _iter_tree_entries_next( + root_full: str, + dir_rel: str, + memo: dict[str, str], + on_error: Callable[[OSError], None], + follow_links: bool, +) -> Iterator['TreeEntry']: + """ + Scan the directory for all descendant files. + + *root_full* (:class:`str`) the absolute path to the root directory. + + *dir_rel* (:class:`str`) the path to the directory to scan relative to + *root_full*. + + *memo* (:class:`dict`) keeps track of ancestor directories encountered. Maps + each ancestor real path (:class:`str`) to relative path (:class:`str`). + + *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is + the error handler for file-system exceptions. + + *follow_links* (:class:`bool`) is whether to walk symbolic links that resolve + to directories. + + Yields each entry (:class:`.TreeEntry`). + """ + dir_full = os.path.join(root_full, dir_rel) + dir_real = os.path.realpath(dir_full) + + # Remember each encountered ancestor directory and its canonical (real) path. + # If a canonical path is encountered more than once, recursion has occurred. + if dir_real not in memo: + memo[dir_real] = dir_rel + else: + raise RecursionError(real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel) + + with os.scandir(dir_full) as scan_iter: + node_ent: os.DirEntry + for node_ent in scan_iter: + node_rel = os.path.join(dir_rel, node_ent.name) + + # Inspect child node. + try: + node_lstat = node_ent.stat(follow_symlinks=False) + except OSError as e: + if on_error is not None: + on_error(e) + continue + + if node_ent.is_symlink(): + # Child node is a link, inspect the target node. + try: + node_stat = node_ent.stat() + except OSError as e: + if on_error is not None: + on_error(e) + continue + else: + node_stat = node_lstat + + if node_ent.is_dir(follow_symlinks=follow_links): + # Child node is a directory, recurse into it and yield its descendant + # files. + yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat) + + yield from _iter_tree_entries_next(root_full, node_rel, memo, on_error, follow_links) + + elif node_ent.is_file() or node_ent.is_symlink(): + # Child node is either a file or an unfollowed link, yield it. + yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat) + + # NOTE: Make sure to remove the canonical (real) path of the directory from + # the ancestors memo once we are done with it. This allows the same directory + # to appear multiple times. If this is not done, the second occurrence of the + # directory will be incorrectly interpreted as a recursion. See + # . + del memo[dir_real] + + +def iter_tree_files( + root: StrPath, + on_error: Optional[Callable[[OSError], None]] = None, + follow_links: Optional[bool] = None, +) -> Iterator[str]: + """ + Walks the specified directory for all files. + + *root* (:class:`str` or :class:`os.PathLike`) is the root directory to search + for files. + + *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is + the error handler for file-system exceptions. It will be called with the + exception (:exc:`OSError`). Reraise the exception to abort the walk. Default + is :data:`None` to ignore file-system exceptions. + + *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk + symbolic links that resolve to directories. Default is :data:`None` for + :data:`True`. + + Raises :exc:`.RecursionError` if recursion is detected. + + Returns an :class:`~collections.abc.Iterator` yielding the path to each file + (:class:`str`) relative to *root*. + """ + if on_error is not None and not callable(on_error): + raise TypeError(f"on_error:{on_error!r} is not callable.") + + if follow_links is None: + follow_links = True + + yield from _iter_tree_files_next(os.path.abspath(root), '', {}, on_error, follow_links) + + +def _iter_tree_files_next( + root_full: str, + dir_rel: str, + memo: dict[str, str], + on_error: Callable[[OSError], None], + follow_links: bool, +) -> Iterator[str]: + """ + Scan the directory for all descendant files. + + *root_full* (:class:`str`) the absolute path to the root directory. + + *dir_rel* (:class:`str`) the path to the directory to scan relative to + *root_full*. + + *memo* (:class:`dict`) keeps track of ancestor directories encountered. Maps + each ancestor real path (:class:`str`) to relative path (:class:`str`). + + *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is + the error handler for file-system exceptions. + + *follow_links* (:class:`bool`) is whether to walk symbolic links that resolve + to directories. + + Yields each file path (:class:`str`). + """ + dir_full = os.path.join(root_full, dir_rel) + dir_real = os.path.realpath(dir_full) + + # Remember each encountered ancestor directory and its canonical (real) path. + # If a canonical path is encountered more than once, recursion has occurred. + if dir_real not in memo: + memo[dir_real] = dir_rel + else: + raise RecursionError(real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel) + + with os.scandir(dir_full) as scan_iter: + node_ent: os.DirEntry + for node_ent in scan_iter: + node_rel = os.path.join(dir_rel, node_ent.name) + + if node_ent.is_dir(follow_symlinks=follow_links): + # Child node is a directory, recurse into it and yield its descendant + # files. + yield from _iter_tree_files_next(root_full, node_rel, memo, on_error, follow_links) + + elif node_ent.is_file(): + # Child node is a file, yield it. + yield node_rel + + elif not follow_links and node_ent.is_symlink(): + # Child node is an unfollowed link, yield it. + yield node_rel + + # NOTE: Make sure to remove the canonical (real) path of the directory from + # the ancestors memo once we are done with it. This allows the same directory + # to appear multiple times. If this is not done, the second occurrence of the + # directory will be incorrectly interpreted as a recursion. See + # . + del memo[dir_real] + + +def lookup_pattern(name: str) -> Callable[[AnyStr], Pattern]: + """ + Lookups a registered pattern factory by name. + + *name* (:class:`str`) is the name of the pattern factory. + + Returns the registered pattern factory (:class:`~collections.abc.Callable`). + If no pattern factory is registered, raises :exc:`KeyError`. + """ + return _registered_patterns[name] + + +def match_file(patterns: Iterable[Pattern], file: str) -> bool: + """ + Matches the file to the patterns. + + *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains + the patterns to use. + + *file* (:class:`str`) is the normalized file path to be matched against + *patterns*. + + Returns :data:`True` if *file* matched; otherwise, :data:`False`. + """ + matched = False + for pattern in patterns: + if pattern.include is not None and pattern.match_file(file) is not None: + matched = pattern.include + + return matched + + +@deprecated(( + "pathspec.util.match_files() is deprecated. Use match_file() with a loop for " + "better results." +)) +def match_files( + patterns: Iterable[Pattern], + files: Iterable[str], +) -> set[str]: + """ + .. version-deprecated:: 0.10.0 + This function is no longer used. Use the :func:`.match_file` function with a + loop for better results. + + Matches the files to the patterns. + + *patterns* (:class:`~collections.abc.Iterable` of :class:`.Pattern`) contains + the patterns to use. + + *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains the + normalized file paths to be matched against *patterns*. + + Returns the matched files (:class:`set` of :class:`str`). + """ + use_patterns = [__pat for __pat in patterns if __pat.include is not None] + + return_files = set() + for file in files: + if match_file(use_patterns, file): + return_files.add(file) + + return return_files + + +def normalize_file( + file: StrPath, + separators: Optional[Collection[str]] = None, +) -> str: + """ + Normalizes the file path to use the POSIX path separator (i.e., ``"/"``), and + make the paths relative (remove leading ``"/"``). + + *file* (:class:`str` or :class:`os.PathLike`) is the file path. + + *separators* (:class:`~collections.abc.Collection` of :class:`str`; or + :data:`None`) optionally contains the path separators to normalize. This does + not need to include the POSIX path separator (``"/"``), but including it will + not affect the results. Default is ``None`` for :data:`.NORMALIZE_PATH_SEPS`. + To prevent normalization, pass an empty container (e.g., an empty tuple + ``()``). + + Returns the normalized file path (:class:`str`). + """ + # Normalize path separators. + if separators is None: + separators = NORMALIZE_PATH_SEPS + + # Convert path object to string. + norm_file: str = os.fspath(file) + + for sep in separators: + norm_file = norm_file.replace(sep, posixpath.sep) + + if norm_file.startswith('/'): + # Make path relative. + norm_file = norm_file[1:] + + elif norm_file.startswith('./'): + # Remove current directory prefix. + norm_file = norm_file[2:] + + return norm_file + + +@deprecated(( + "pathspec.util.normalize_files() is deprecated. Use normalize_file() with a " + "loop for better results." +)) +def normalize_files( + files: Iterable[StrPath], + separators: Optional[Collection[str]] = None, +) -> dict[str, list[StrPath]]: + """ + .. version-deprecated:: 0.10.0 + This function is no longer used. Use the :func:`.normalize_file` function + with a loop for better results. + + Normalizes the file paths to use the POSIX path separator. + + *files* (:class:`~collections.abc.Iterable` of :class:`str` or + :class:`os.PathLike`) contains the file paths to be normalized. + + *separators* (:class:`~collections.abc.Collection` of :class:`str`; or + :data:`None`) optionally contains the path separators to normalize. See + :func:`.normalize_file` for more information. + + Returns a :class:`dict` mapping each normalized file path (:class:`str`) to + the original file paths (:class:`list` of :class:`str` or + :class:`os.PathLike`). + """ + norm_files = {} + for path in files: + norm_file = normalize_file(path, separators=separators) + if norm_file in norm_files: + norm_files[norm_file].append(path) + else: + norm_files[norm_file] = [path] + + return norm_files + + +def register_pattern( + name: str, + pattern_factory: Callable[[AnyStr], Pattern], + override: Optional[bool] = None, +) -> None: + """ + Registers the specified pattern factory. + + *name* (:class:`str`) is the name to register the pattern factory under. + + *pattern_factory* (:class:`~collections.abc.Callable`) is used to compile + patterns. It must accept an uncompiled pattern (:class:`str`) and return the + compiled pattern (:class:`.Pattern`). + + *override* (:class:`bool` or :data:`None`) optionally is whether to allow + overriding an already registered pattern under the same name (:data:`True`), + instead of raising an :exc:`.AlreadyRegisteredError` (:data:`False`). Default + is :data:`None` for :data:`False`. + """ + if not isinstance(name, str): + raise TypeError(f"name:{name!r} is not a string.") + + if not callable(pattern_factory): + raise TypeError(f"pattern_factory:{pattern_factory!r} is not callable.") + + if name in _registered_patterns and not override: + raise AlreadyRegisteredError(name, _registered_patterns[name]) + + _registered_patterns[name] = pattern_factory + + +class AlreadyRegisteredError(Exception): + """ + The :exc:`AlreadyRegisteredError` exception is raised when a pattern factory + is registered under a name already in use. + """ + + def __init__( + self, + name: str, + pattern_factory: Callable[[AnyStr], Pattern], + ) -> None: + """ + Initializes the :exc:`AlreadyRegisteredError` instance. + + *name* (:class:`str`) is the name of the registered pattern. + + *pattern_factory* (:class:`~collections.abc.Callable`) is the registered + pattern factory. + """ + super().__init__(name, pattern_factory) + + @property + def message(self) -> str: + """ + *message* (:class:`str`) is the error message. + """ + return ( + f"{self.name!r} is already registered for pattern factory=" + f"{self.pattern_factory!r}." + ) + + @property + def name(self) -> str: + """ + *name* (:class:`str`) is the name of the registered pattern. + """ + return self.args[0] + + @property + def pattern_factory(self) -> Callable[[AnyStr], Pattern]: + """ + *pattern_factory* (:class:`~collections.abc.Callable`) is the registered + pattern factory. + """ + return self.args[1] + + +class RecursionError(Exception): + """ + The :exc:`RecursionError` exception is raised when recursion is detected. + """ + + def __init__( + self, + real_path: str, + first_path: str, + second_path: str, + ) -> None: + """ + Initializes the :exc:`RecursionError` instance. + + *real_path* (:class:`str`) is the real path that recursion was encountered + on. + + *first_path* (:class:`str`) is the first path encountered for *real_path*. + + *second_path* (:class:`str`) is the second path encountered for *real_path*. + """ + super().__init__(real_path, first_path, second_path) + + @property + def first_path(self) -> str: + """ + *first_path* (:class:`str`) is the first path encountered for + :attr:`self.real_path `. + """ + return self.args[1] + + @property + def message(self) -> str: + """ + *message* (:class:`str`) is the error message. + """ + return ( + f"Real path {self.real_path!r} was encountered at {self.first_path!r} " + f"and then {self.second_path!r}." + ) + + @property + def real_path(self) -> str: + """ + *real_path* (:class:`str`) is the real path that recursion was + encountered on. + """ + return self.args[0] + + @property + def second_path(self) -> str: + """ + *second_path* (:class:`str`) is the second path encountered for + :attr:`self.real_path `. + """ + return self.args[2] + + +@dataclass(frozen=True) +class CheckResult(Generic[TStrPath]): + """ + The :class:`CheckResult` class contains information about the file and which + pattern matched it. + """ + + # Make the class dict-less. + __slots__ = ( + 'file', + 'include', + 'index', + ) + + file: TStrPath + """ + *file* (:class:`str` or :class:`os.PathLike`) is the file path. + """ + + include: Optional[bool] + """ + *include* (:class:`bool` or :data:`None`) is whether to include or exclude the + file. If :data:`None`, no pattern matched. + """ + + index: Optional[int] + """ + *index* (:class:`int` or :data:`None`) is the index of the last pattern that + matched. If :data:`None`, no pattern matched. + """ + + +class MatchDetail(object): + """ + The :class:`.MatchDetail` class contains information about + """ + + # Make the class dict-less. + __slots__ = ('patterns',) + + def __init__(self, patterns: Sequence[Pattern]) -> None: + """ + Initialize the :class:`.MatchDetail` instance. + + *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`) + contains the patterns that matched the file in the order they were encountered. + """ + + self.patterns = patterns + """ + *patterns* (:class:`~collections.abc.Sequence` of :class:`.Pattern`) + contains the patterns that matched the file in the order they were + encountered. + """ + + +class TreeEntry(object): + """ + The :class:`TreeEntry` class contains information about a file-system entry. + """ + + # Make the class dict-less. + __slots__ = ('_lstat', 'name', 'path', '_stat') + + def __init__( + self, + name: str, + path: str, + lstat: os.stat_result, + stat: os.stat_result, + ) -> None: + """ + Initialize the :class:`TreeEntry` instance. + + *name* (:class:`str`) is the base name of the entry. + + *path* (:class:`str`) is the relative path of the entry. + + *lstat* (:class:`os.stat_result`) is the stat result of the direct entry. + + *stat* (:class:`os.stat_result`) is the stat result of the entry, + potentially linked. + """ + + self._lstat: os.stat_result = lstat + """ + *_lstat* (:class:`os.stat_result`) is the stat result of the direct entry. + """ + + self.name: str = name + """ + *name* (:class:`str`) is the base name of the entry. + """ + + self.path: str = path + """ + *path* (:class:`str`) is the path of the entry. + """ + + self._stat: os.stat_result = stat + """ + *_stat* (:class:`os.stat_result`) is the stat result of the linked entry. + """ + + def is_dir(self, follow_links: Optional[bool] = None) -> bool: + """ + Get whether the entry is a directory. + + *follow_links* (:class:`bool` or :data:`None`) is whether to follow symbolic + links. If this is :data:`True`, a symlink to a directory will result in + :data:`True`. Default is :data:`None` for :data:`True`. + + Returns whether the entry is a directory (:class:`bool`). + """ + if follow_links is None: + follow_links = True + + node_stat = self._stat if follow_links else self._lstat + return stat.S_ISDIR(node_stat.st_mode) + + def is_file(self, follow_links: Optional[bool] = None) -> bool: + """ + Get whether the entry is a regular file. + + *follow_links* (:class:`bool` or :data:`None`) is whether to follow symbolic + links. If this is :data:`True`, a symlink to a regular file will result in + :data:`True`. Default is :data:`None` for :data:`True`. + + Returns whether the entry is a regular file (:class:`bool`). + """ + if follow_links is None: + follow_links = True + + node_stat = self._stat if follow_links else self._lstat + return stat.S_ISREG(node_stat.st_mode) + + def is_symlink(self) -> bool: + """ + Returns whether the entry is a symbolic link (:class:`bool`). + """ + return stat.S_ISLNK(self._lstat.st_mode) + + def stat(self, follow_links: Optional[bool] = None) -> os.stat_result: + """ + Get the cached stat result for the entry. + + *follow_links* (:class:`bool` or :data:`None`) is whether to follow symbolic + links. If this is :data:`True`, the stat result of the linked file will be + returned. Default is :data:`None` for :data:`True`. + + Returns that stat result (:class:`os.stat_result`). + """ + if follow_links is None: + follow_links = True + + return self._stat if follow_links else self._lstat diff --git a/newplan?.md b/newplan?.md new file mode 100644 index 0000000..303abd6 --- /dev/null +++ b/newplan?.md @@ -0,0 +1,14 @@ +1. Dir Scanner + +2. file filter + +3. Hasher + +4. Manifest + + + + + + +1.