pathspec-0.9.0/0000755000232200023220000000000014074672614013710 5ustar debalancedebalancepathspec-0.9.0/README.rst0000644000232200023220000001055713614170723015400 0ustar debalancedebalance *pathspec*: Path Specification ============================== *pathspec* is a utility library for pattern matching of file paths. So far this only includes Git's wildmatch pattern matching which itself is derived from Rsync's wildmatch. Git uses wildmatch for its `gitignore`_ files. .. _`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:: >>> import pathspec >>> # The gitignore-style patterns for files to select, but we're including >>> # instead of ignoring. >>> spec = """ ... ... # 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 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/ ... ... """ We want to use the ``GitWildMatchPattern`` class to compile our patterns. The ``PathSpec`` class provides an interface around pattern implementations:: >>> spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, spec.splitlines()) That may be a mouthful but it allows for additional patterns to be implemented in the future without them having to deal with anything but matching the paths sent to them. ``GitWildMatchPattern`` is the implementation of the actual pattern which internally gets converted into a regular expression. ``PathSpec`` is a simple wrapper around a list of compiled patterns. To make things simpler, we can use the registered name for a pattern class instead of always having to provide a reference to the class itself. The ``GitWildMatchPattern`` class is registered as **gitwildmatch**:: >>> spec = pathspec.PathSpec.from_lines('gitwildmatch', spec.splitlines()) If we wanted to manually compile the patterns we can just do the following:: >>> patterns = map(pathspec.patterns.GitWildMatchPattern, spec.splitlines()) >>> spec = PathSpec(patterns) ``PathSpec.from_lines()`` is simply a class method which does just that. If you want to load the patterns from file, you can pass the file instance directly as well:: >>> with open('patterns.list', 'r') as fh: >>> spec = pathspec.PathSpec.from_lines('gitwildmatch', fh) You can perform matching on a whole directory tree with:: >>> matches = spec.match_tree('path/to/directory') Or you can perform matching on a specific set of file paths with:: >>> matches = spec.match_files(file_paths) Or check to see if an individual file matches:: >>> is_matched = spec.match_file(file_path) 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-path-specification`_. .. _`cpburnz/python-path-specification`: https://github.com/cpburnz/python-path-specification Installation ------------ *pathspec* requires the following packages: - `setuptools`_ *pathspec* can be installed from source with:: python setup.py install *pathspec* is also available for install through `PyPI`_:: pip install pathspec .. _`setuptools`: https://pypi.python.org/pypi/setuptools .. _`PyPI`: http://pypi.python.org/pypi/pathspec Documentation ------------- Documentation for *pathspec* is available on `Read the Docs`_. .. _`Read the Docs`: http://python-path-specification.readthedocs.io Other Languages --------------- *pathspec* is also available as a `Ruby gem`_. .. _`Ruby gem`: https://github.com/highb/pathspec-ruby pathspec-0.9.0/pathspec.egg-info/0000755000232200023220000000000014074672614017211 5ustar debalancedebalancepathspec-0.9.0/pathspec.egg-info/SOURCES.txt0000644000232200023220000000102014074672614021066 0ustar debalancedebalanceCHANGES.rst LICENSE MANIFEST.in README.rst pathspec_meta.py setup.cfg setup.py tox_pip_install.py pathspec/__init__.py pathspec/_meta.py pathspec/compat.py pathspec/pathspec.py pathspec/pattern.py pathspec/util.py pathspec.egg-info/PKG-INFO pathspec.egg-info/SOURCES.txt pathspec.egg-info/dependency_links.txt pathspec.egg-info/top_level.txt pathspec/patterns/__init__.py pathspec/patterns/gitwildmatch.py pathspec/tests/__init__.py pathspec/tests/test_gitwildmatch.py pathspec/tests/test_pathspec.py pathspec/tests/test_util.pypathspec-0.9.0/pathspec.egg-info/dependency_links.txt0000644000232200023220000000000114074672614023257 0ustar debalancedebalance pathspec-0.9.0/pathspec.egg-info/PKG-INFO0000644000232200023220000003553014074672614020314 0ustar debalancedebalanceMetadata-Version: 2.1 Name: pathspec Version: 0.9.0 Summary: Utility library for gitignore style pattern matching of file paths. Home-page: https://github.com/cpburnz/python-path-specification Author: Caleb P. Burns Author-email: cpburnz@gmail.com License: MPL 2.0 Description: *pathspec*: Path Specification ============================== *pathspec* is a utility library for pattern matching of file paths. So far this only includes Git's wildmatch pattern matching which itself is derived from Rsync's wildmatch. Git uses wildmatch for its `gitignore`_ files. .. _`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:: >>> import pathspec >>> # The gitignore-style patterns for files to select, but we're including >>> # instead of ignoring. >>> spec = """ ... ... # 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 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/ ... ... """ We want to use the ``GitWildMatchPattern`` class to compile our patterns. The ``PathSpec`` class provides an interface around pattern implementations:: >>> spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, spec.splitlines()) That may be a mouthful but it allows for additional patterns to be implemented in the future without them having to deal with anything but matching the paths sent to them. ``GitWildMatchPattern`` is the implementation of the actual pattern which internally gets converted into a regular expression. ``PathSpec`` is a simple wrapper around a list of compiled patterns. To make things simpler, we can use the registered name for a pattern class instead of always having to provide a reference to the class itself. The ``GitWildMatchPattern`` class is registered as **gitwildmatch**:: >>> spec = pathspec.PathSpec.from_lines('gitwildmatch', spec.splitlines()) If we wanted to manually compile the patterns we can just do the following:: >>> patterns = map(pathspec.patterns.GitWildMatchPattern, spec.splitlines()) >>> spec = PathSpec(patterns) ``PathSpec.from_lines()`` is simply a class method which does just that. If you want to load the patterns from file, you can pass the file instance directly as well:: >>> with open('patterns.list', 'r') as fh: >>> spec = pathspec.PathSpec.from_lines('gitwildmatch', fh) You can perform matching on a whole directory tree with:: >>> matches = spec.match_tree('path/to/directory') Or you can perform matching on a specific set of file paths with:: >>> matches = spec.match_files(file_paths) Or check to see if an individual file matches:: >>> is_matched = spec.match_file(file_path) 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-path-specification`_. .. _`cpburnz/python-path-specification`: https://github.com/cpburnz/python-path-specification Installation ------------ *pathspec* requires the following packages: - `setuptools`_ *pathspec* can be installed from source with:: python setup.py install *pathspec* is also available for install through `PyPI`_:: pip install pathspec .. _`setuptools`: https://pypi.python.org/pypi/setuptools .. _`PyPI`: http://pypi.python.org/pypi/pathspec Documentation ------------- Documentation for *pathspec* is available on `Read the Docs`_. .. _`Read the Docs`: http://python-path-specification.readthedocs.io Other Languages --------------- *pathspec* is also available as a `Ruby gem`_. .. _`Ruby gem`: https://github.com/highb/pathspec-ruby Change History ============== 0.9.0 (2021-07-17) ------------------ - `Issue #44`_/`Issue #50`_: Raise `GitWildMatchPatternError` for invalid git patterns. - `Issue #45`_: Fix for duplicate leading double-asterisk, and edge cases. - `Issue #46`_: Fix matching absolute paths. - API change: `util.normalize_files()` now returns a `Dict[str, List[pathlike]]` instead of a `Dict[str, pathlike]`. - Added type hinting. .. _`Issue #44`: https://github.com/cpburnz/python-path-specification/issues/44 .. _`Issue #45`: https://github.com/cpburnz/python-path-specification/pull/45 .. _`Issue #46`: https://github.com/cpburnz/python-path-specification/issues/46 .. _`Issue #50`: https://github.com/cpburnz/python-path-specification/pull/50 0.8.1 (2020-11-07) ------------------ - `Issue #43`_: Add support for addition operator. .. _`Issue #43`: https://github.com/cpburnz/python-path-specification/pull/43 0.8.0 (2020-04-09) ------------------ - `Issue #30`_: Expose what patterns matched paths. Added `util.detailed_match_files()`. - `Issue #31`_: `match_tree()` doesn't return symlinks. - `Issue #34`_: Support `pathlib.Path`\ s. - Add `PathSpec.match_tree_entries` and `util.iter_tree_entries()` to support directories and symlinks. - API change: `match_tree()` has been renamed to `match_tree_files()`. The old name `match_tree()` is still available as an alias. - API change: `match_tree_files()` now returns symlinks. This is a bug fix but it will change the returned results. .. _`Issue #30`: https://github.com/cpburnz/python-path-specification/issues/30 .. _`Issue #31`: https://github.com/cpburnz/python-path-specification/issues/31 .. _`Issue #34`: https://github.com/cpburnz/python-path-specification/issues/34 0.7.0 (2019-12-27) ------------------ - `Issue #28`_: Add support for Python 3.8, and drop Python 3.4. - `Issue #29`_: Publish bdist wheel. .. _`Issue #28`: https://github.com/cpburnz/python-path-specification/pull/28 .. _`Issue #29`: https://github.com/cpburnz/python-path-specification/pull/29 0.6.0 (2019-10-03) ------------------ - `Issue #24`_: Drop support for Python 2.6, 3.2, and 3.3. - `Issue #25`_: Update README.rst. - `Issue #26`_: Method to escape gitwildmatch. .. _`Issue #24`: https://github.com/cpburnz/python-path-specification/pull/24 .. _`Issue #25`: https://github.com/cpburnz/python-path-specification/pull/25 .. _`Issue #26`: https://github.com/cpburnz/python-path-specification/pull/26 0.5.9 (2018-09-15) ------------------ - Fixed file system error handling. 0.5.8 (2018-09-15) ------------------ - Improved type checking. - Created scripts to test Python 2.6 because Tox removed support for it. - Improved byte string handling in Python 3. - `Issue #22`_: Handle dangling symlinks. .. _`Issue #22`: https://github.com/cpburnz/python-path-specification/issues/22 0.5.7 (2018-08-14) ------------------ - `Issue #21`_: Fix collections deprecation warning. .. _`Issue #21`: https://github.com/cpburnz/python-path-specification/issues/21 0.5.6 (2018-04-06) ------------------ - Improved unit tests. - Improved type checking. - `Issue #20`_: Support current directory prefix. .. _`Issue #20`: https://github.com/cpburnz/python-path-specification/issues/20 0.5.5 (2017-09-09) ------------------ - Add documentation link to README. 0.5.4 (2017-09-09) ------------------ - `Issue #17`_: Add link to Ruby implementation of *pathspec*. - Add sphinx documentation. .. _`Issue #17`: https://github.com/cpburnz/python-path-specification/pull/17 0.5.3 (2017-07-01) ------------------ - `Issue #14`_: Fix byte strings for Python 3. - `Issue #15`_: Include "LICENSE" in source package. - `Issue #16`_: Support Python 2.6. .. _`Issue #14`: https://github.com/cpburnz/python-path-specification/issues/14 .. _`Issue #15`: https://github.com/cpburnz/python-path-specification/pull/15 .. _`Issue #16`: https://github.com/cpburnz/python-path-specification/issues/16 0.5.2 (2017-04-04) ------------------ - Fixed change log. 0.5.1 (2017-04-04) ------------------ - `Issue #13`_: Add equality methods to `PathSpec` and `RegexPattern`. .. _`Issue #13`: https://github.com/cpburnz/python-path-specification/pull/13 0.5.0 (2016-08-22) ------------------ - `Issue #12`_: Add `PathSpec.match_file()`. - Renamed `gitignore.GitIgnorePattern` to `patterns.gitwildmatch.GitWildMatchPattern`. - Deprecated `gitignore.GitIgnorePattern`. .. _`Issue #12`: https://github.com/cpburnz/python-path-specification/issues/12 0.4.0 (2016-07-15) ------------------ - `Issue #11`_: Support converting patterns into regular expressions without compiling them. - API change: Subclasses of `RegexPattern` should implement `pattern_to_regex()`. .. _`Issue #11`: https://github.com/cpburnz/python-path-specification/issues/11 0.3.4 (2015-08-24) ------------------ - `Issue #7`_: Fixed non-recursive links. - `Issue #8`_: Fixed edge cases in gitignore patterns. - `Issue #9`_: Fixed minor usage documentation. - Fixed recursion detection. - Fixed trivial incompatibility with Python 3.2. .. _`Issue #7`: https://github.com/cpburnz/python-path-specification/pull/7 .. _`Issue #8`: https://github.com/cpburnz/python-path-specification/pull/8 .. _`Issue #9`: https://github.com/cpburnz/python-path-specification/pull/9 0.3.3 (2014-11-21) ------------------ - Improved documentation. 0.3.2 (2014-11-08) ------------------ - `Issue #5`_: Use tox for testing. - `Issue #6`_: Fixed matching Windows paths. - Improved documentation. - API change: `spec.match_tree()` and `spec.match_files()` now return iterators instead of sets. .. _`Issue #5`: https://github.com/cpburnz/python-path-specification/pull/5 .. _`Issue #6`: https://github.com/cpburnz/python-path-specification/issues/6 0.3.1 (2014-09-17) ------------------ - Updated README. 0.3.0 (2014-09-17) ------------------ - `Issue #3`_: Fixed trailing slash in gitignore patterns. - `Issue #4`_: Fixed test for trailing slash in gitignore patterns. - Added registered patterns. .. _`Issue #3`: https://github.com/cpburnz/python-path-specification/pull/3 .. _`Issue #4`: https://github.com/cpburnz/python-path-specification/pull/4 0.2.2 (2013-12-17) ------------------ - Fixed setup.py. 0.2.1 (2013-12-17) ------------------ - Added tests. - Fixed comment gitignore patterns. - Fixed relative path gitignore patterns. 0.2.0 (2013-12-07) ------------------ - Initial release. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta 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 :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7 Description-Content-Type: text/x-rst pathspec-0.9.0/pathspec.egg-info/top_level.txt0000644000232200023220000000001114074672614021733 0ustar debalancedebalancepathspec pathspec-0.9.0/pathspec/0000755000232200023220000000000014074672614015517 5ustar debalancedebalancepathspec-0.9.0/pathspec/util.py0000644000232200023220000004666114061167332017053 0ustar debalancedebalance# encoding: utf-8 """ This module provides utility methods for dealing with path-specs. """ import os import os.path import posixpath import stat try: from typing import ( Any, AnyStr, Callable, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Text, Union) except ImportError: pass try: # Python 3.6+ type hints. from os import PathLike from typing import Collection except ImportError: pass from .compat import ( CollectionType, IterableType, string_types, unicode) from .pattern import Pattern 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 detailed_match_files(patterns, files, all_matches=None): # type: (Iterable[Pattern], Iterable[Text], Optional[bool]) -> Dict[Text, 'MatchDetail'] """ Matches the files to the patterns, and returns which patterns matched the files. *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.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:`boot` 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, CollectionType) else list(files) return_files = {} for pattern in patterns: if pattern.include is not None: result_files = pattern.match(all_files) 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 _is_iterable(value): # type: (Any) -> bool """ Check whether the value is an iterable (excludes strings). *value* is the value to check, Returns whether *value* is a iterable (:class:`bool`). """ return isinstance(value, IterableType) and not isinstance(value, (unicode, bytes)) def iter_tree_entries(root, on_error=None, follow_links=None): # type: (Text, Optional[Callable], Optional[bool]) -> Iterator['TreeEntry'] """ Walks the specified directory for all files and directories. *root* (:class:`str`) 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("on_error:{!r} is not callable.".format(on_error)) if follow_links is None: follow_links = True for entry in _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links): yield entry def iter_tree_files(root, on_error=None, follow_links=None): # type: (Text, Optional[Callable], Optional[bool]) -> Iterator[Text] """ Walks the specified directory for all files. *root* (:class:`str`) 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("on_error:{!r} is not callable.".format(on_error)) if follow_links is None: follow_links = True for entry in _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links): if not entry.is_dir(follow_links): yield entry.path # Alias `iter_tree_files()` as `iter_tree()`. iter_tree = iter_tree_files def _iter_tree_entries_next(root_full, dir_rel, memo, on_error, follow_links): # type: (Text, Text, Dict[Text, Text], Callable, 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) for node_name in os.listdir(dir_full): node_rel = os.path.join(dir_rel, node_name) node_full = os.path.join(root_full, node_rel) # Inspect child node. try: node_lstat = os.lstat(node_full) except OSError as e: if on_error is not None: on_error(e) continue if stat.S_ISLNK(node_lstat.st_mode): # Child node is a link, inspect the target node. is_link = True try: node_stat = os.stat(node_full) except OSError as e: if on_error is not None: on_error(e) continue else: is_link = False node_stat = node_lstat if stat.S_ISDIR(node_stat.st_mode) and (follow_links or not is_link): # Child node is a directory, recurse into it and yield its # descendant files. yield TreeEntry(node_name, node_rel, node_lstat, node_stat) for entry in _iter_tree_entries_next(root_full, node_rel, memo, on_error, follow_links): yield entry elif stat.S_ISREG(node_stat.st_mode) or is_link: # Child node is either a file or an unfollowed link, yield it. yield TreeEntry(node_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 lookup_pattern(name): # type: (Text) -> 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, file): # type: (Iterable[Pattern], Text) -> bool """ Matches the file to the patterns. *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.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: if file in pattern.match((file,)): matched = pattern.include return matched def match_files(patterns, files): # type: (Iterable[Pattern], Iterable[Text]) -> Set[Text] """ Matches the files to the patterns. *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.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`). """ all_files = files if isinstance(files, CollectionType) else list(files) return_files = set() for pattern in patterns: if pattern.include is not None: result_files = pattern.match(all_files) if pattern.include: return_files.update(result_files) else: return_files.difference_update(result_files) return return_files def _normalize_entries(entries, separators=None): # type: (Iterable['TreeEntry'], Optional[Collection[Text]]) -> Dict[Text, 'TreeEntry'] """ Normalizes the entry paths to use the POSIX path separator. *entries* (:class:`~collections.abc.Iterable` of :class:`.TreeEntry`) contains the entries 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 the each normalized file path (:class:`str`) to the entry (:class:`.TreeEntry`) """ norm_files = {} for entry in entries: norm_files[normalize_file(entry.path, separators=separators)] = entry return norm_files def normalize_file(file, separators=None): # type: (Union[Text, PathLike], Optional[Collection[Text]]) -> Text """ Normalizes the file path to use the POSIX path separator (i.e., ``'/'``), and make the paths relative (remove leading ``'/'``). *file* (:class:`str` or :class:`pathlib.PurePath`) 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 :data:`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(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 def normalize_files(files, separators=None): # type: (Iterable[Union[str, PathLike]], Optional[Collection[Text]]) -> Dict[Text, List[Union[str, PathLike]]] """ Normalizes the file paths to use the POSIX path separator. *files* (:class:`~collections.abc.Iterable` of :class:`str` or :class:`pathlib.PurePath`) 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 the each normalized file path (:class:`str`) to the original file paths (:class:`list` of :class:`str` or :class:`pathlib.PurePath`). """ 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, pattern_factory, override=None): # type: (Text, Callable[[AnyStr], Pattern], Optional[bool]) -> 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, string_types): raise TypeError("name:{!r} is not a string.".format(name)) if not callable(pattern_factory): raise TypeError("pattern_factory:{!r} is not callable.".format(pattern_factory)) 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, pattern_factory): # type: (Text, 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(AlreadyRegisteredError, self).__init__(name, pattern_factory) @property def message(self): # type: () -> Text """ *message* (:class:`str`) is the error message. """ return "{name!r} is already registered for pattern factory:{pattern_factory!r}.".format( name=self.name, pattern_factory=self.pattern_factory, ) @property def name(self): # type: () -> Text """ *name* (:class:`str`) is the name of the registered pattern. """ return self.args[0] @property def pattern_factory(self): # type: () -> 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, first_path, second_path): # type: (Text, Text, Text) -> 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(RecursionError, self).__init__(real_path, first_path, second_path) @property def first_path(self): # type: () -> Text """ *first_path* (:class:`str`) is the first path encountered for :attr:`self.real_path `. """ return self.args[1] @property def message(self): # type: () -> Text """ *message* (:class:`str`) is the error message. """ return "Real path {real!r} was encountered at {first!r} and then {second!r}.".format( real=self.real_path, first=self.first_path, second=self.second_path, ) @property def real_path(self): # type: () -> Text """ *real_path* (:class:`str`) is the real path that recursion was encountered on. """ return self.args[0] @property def second_path(self): # type: () -> Text """ *second_path* (:class:`str`) is the second path encountered for :attr:`self.real_path `. """ return self.args[2] class MatchDetail(object): """ The :class:`.MatchDetail` class contains information about """ #: Make the class dict-less. __slots__ = ('patterns',) def __init__(self, patterns): # type: (Sequence[Pattern]) -> None """ Initialize the :class:`.MatchDetail` instance. *patterns* (:class:`~collections.abc.Sequence` of :class:`~pathspec.pattern.Pattern`) contains the patterns that matched the file in the order they were encountered. """ self.patterns = patterns """ *patterns* (:class:`~collections.abc.Sequence` of :class:`~pathspec.pattern.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, path, lstat, stat): # type: (Text, Text, os.stat_result, 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 = lstat """ *_lstat* (:class:`~os.stat_result`) is the stat result of the direct entry. """ self.name = name """ *name* (:class:`str`) is the base name of the entry. """ self.path = path """ *path* (:class:`str`) is the path of the entry. """ self._stat = stat """ *_stat* (:class:`~os.stat_result`) is the stat result of the linked entry. """ def is_dir(self, follow_links=None): # type: (Optional[bool]) -> 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=None): # type: (Optional[bool]) -> 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): # type: () -> bool """ Returns whether the entry is a symbolic link (:class:`bool`). """ return stat.S_ISLNK(self._lstat.st_mode) def stat(self, follow_links=None): # type: (Optional[bool]) -> 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 pathspec-0.9.0/pathspec/compat.py0000644000232200023220000000147614061162457017357 0ustar debalancedebalance# encoding: utf-8 """ This module provides compatibility between Python 2 and 3. Hardly anything is used by this project to constitute including `six`_. .. _`six`: http://pythonhosted.org/six """ import sys if sys.version_info[0] < 3: # Python 2. unicode = unicode string_types = (basestring,) from collections import Iterable from itertools import izip_longest def iterkeys(mapping): return mapping.iterkeys() else: # Python 3. unicode = str string_types = (unicode,) from collections.abc import Iterable from itertools import zip_longest as izip_longest def iterkeys(mapping): return mapping.keys() try: # Python 3.6+. from collections.abc import Collection except ImportError: # Python 2.7 - 3.5. from collections import Container as Collection CollectionType = Collection IterableType = Iterable pathspec-0.9.0/pathspec/pathspec.py0000644000232200023220000001756014061173675017710 0ustar debalancedebalance# encoding: utf-8 """ This module provides an object oriented interface for pattern matching of files. """ try: from typing import ( Any, AnyStr, Callable, Iterable, Iterator, Optional, Text, Union) except ImportError: pass try: # Python 3.6+ type hints. from os import PathLike from typing import Collection except ImportError: pass from . import util from .compat import ( CollectionType, iterkeys, izip_longest, string_types) from .pattern import Pattern from .util import TreeEntry class PathSpec(object): """ The :class:`PathSpec` class is a wrapper around a list of compiled :class:`.Pattern` instances. """ def __init__(self, patterns): # type: (Iterable[Pattern]) -> None """ Initializes the :class:`PathSpec` instance. *patterns* (:class:`~collections.abc.Collection` or :class:`~collections.abc.Iterable`) yields each compiled pattern (:class:`.Pattern`). """ self.patterns = patterns if isinstance(patterns, CollectionType) else list(patterns) """ *patterns* (:class:`~collections.abc.Collection` of :class:`.Pattern`) contains the compiled patterns. """ def __eq__(self, other): # type: (PathSpec) -> bool """ Tests the equality of this path-spec with *other* (:class:`PathSpec`) by comparing their :attr:`~PathSpec.patterns` attributes. """ if isinstance(other, PathSpec): paired_patterns = izip_longest(self.patterns, other.patterns) return all(a == b for a, b in paired_patterns) else: return NotImplemented def __len__(self): """ Returns the number of compiled patterns this path-spec contains (:class:`int`). """ return len(self.patterns) def __add__(self, other): # type: (PathSpec) -> PathSpec """ Combines the :attr:`Pathspec.patterns` patterns from two :class:`PathSpec` instances. """ if isinstance(other, PathSpec): return PathSpec(self.patterns + other.patterns) else: return NotImplemented def __iadd__(self, other): # type: (PathSpec) -> PathSpec """ Adds the :attr:`Pathspec.patterns` patterns from one :class:`PathSpec` instance to this instance. """ if isinstance(other, PathSpec): self.patterns += other.patterns return self else: return NotImplemented @classmethod def from_lines(cls, pattern_factory, lines): # type: (Union[Text, Callable[[AnyStr], Pattern]], Iterable[AnyStr]) -> PathSpec """ 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 it can be a :class:`file` (e.g., from :func:`open` or :class:`io.StringIO`) or the result from :meth:`str.splitlines`. Returns the :class:`PathSpec` instance. """ if isinstance(pattern_factory, string_types): pattern_factory = util.lookup_pattern(pattern_factory) if not callable(pattern_factory): raise TypeError("pattern_factory:{!r} is not callable.".format(pattern_factory)) if not util._is_iterable(lines): raise TypeError("lines:{!r} is not an iterable.".format(lines)) patterns = [pattern_factory(line) for line in lines if line] return cls(patterns) def match_file(self, file, separators=None): # type: (Union[Text, PathLike], Optional[Collection[Text]]) -> bool """ Matches the file to this path-spec. *file* (:class:`str` or :class:`~pathlib.PurePath`) is the file path to be matched against :attr:`self.patterns `. *separators* (:class:`~collections.abc.Collection` of :class:`str`) optionally contains the path separators to normalize. See :func:`~pathspec.util.normalize_file` for more information. Returns :data:`True` if *file* matched; otherwise, :data:`False`. """ norm_file = util.normalize_file(file, separators=separators) return util.match_file(self.patterns, norm_file) def match_entries(self, entries, separators=None): # type: (Iterable[TreeEntry], Optional[Collection[Text]]) -> Iterator[TreeEntry] """ Matches the entries to this path-spec. *entries* (:class:`~collections.abc.Iterable` of :class:`~util.TreeEntry`) contains the entries to be matched against :attr:`self.patterns `. *separators* (:class:`~collections.abc.Collection` of :class:`str`; or :data:`None`) optionally contains the path separators to normalize. See :func:`~pathspec.util.normalize_file` for more information. Returns the matched entries (:class:`~collections.abc.Iterator` of :class:`~util.TreeEntry`). """ if not util._is_iterable(entries): raise TypeError("entries:{!r} is not an iterable.".format(entries)) entry_map = util._normalize_entries(entries, separators=separators) match_paths = util.match_files(self.patterns, iterkeys(entry_map)) for path in match_paths: yield entry_map[path] def match_files(self, files, separators=None): # type: (Iterable[Union[Text, PathLike]], Optional[Collection[Text]]) -> Iterator[Union[Text, PathLike]] """ Matches the files to this path-spec. *files* (:class:`~collections.abc.Iterable` of :class:`str; or :class:`pathlib.PurePath`) contains the file paths to be matched against :attr:`self.patterns `. *separators* (:class:`~collections.abc.Collection` of :class:`str`; or :data:`None`) optionally contains the path separators to normalize. See :func:`~pathspec.util.normalize_file` for more information. Returns the matched files (:class:`~collections.abc.Iterator` of :class:`str` or :class:`pathlib.PurePath`). """ if not util._is_iterable(files): raise TypeError("files:{!r} is not an iterable.".format(files)) file_map = util.normalize_files(files, separators=separators) matched_files = util.match_files(self.patterns, iterkeys(file_map)) for norm_file in matched_files: for orig_file in file_map[norm_file]: yield orig_file def match_tree_entries(self, root, on_error=None, follow_links=None): # type: (Text, Optional[Callable], Optional[bool]) -> Iterator[TreeEntry] """ Walks the specified root path for all files and matches them to this path-spec. *root* (:class:`str`; or :class:`pathlib.PurePath`) is the root directory to search. *on_error* (:class:`~collections.abc.Callable` or :data:`None`) optionally is the error handler for file-system exceptions. See :func:`~pathspec.util.iter_tree_entries` for more information. *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk symbolic links that resolve to directories. See :func:`~pathspec.util.iter_tree_files` for more information. 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) return self.match_entries(entries) def match_tree_files(self, root, on_error=None, follow_links=None): # type: (Text, Optional[Callable], Optional[bool]) -> Iterator[Text] """ Walks the specified root path for all files and matches them to this path-spec. *root* (:class:`str`; or :class:`pathlib.PurePath`) 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. See :func:`~pathspec.util.iter_tree_files` for more information. *follow_links* (:class:`bool` or :data:`None`) optionally is whether to walk symbolic links that resolve to directories. See :func:`~pathspec.util.iter_tree_files` for more information. 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) return self.match_files(files) # Alias `match_tree_files()` as `match_tree()`. match_tree = match_tree_files pathspec-0.9.0/pathspec/pattern.py0000644000232200023220000001131314061170053017527 0ustar debalancedebalance# encoding: utf-8 """ This module provides the base definition for patterns. """ import re try: from typing import ( AnyStr, Iterable, Iterator, Optional, Pattern as RegexHint, Text, Tuple, Union) except ImportError: pass from .compat import unicode 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): # type: (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`). """ def match(self, files): # type: (Iterable[Text]) -> Iterator[Text] """ Matches this pattern against the specified files. *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains each file relative to the root directory (e.g., ``"relative/path/to/file"``). Returns an :class:`~collections.abc.Iterable` yielding each matched file path (:class:`str`). """ raise NotImplementedError("{}.{} must override match().".format(self.__class__.__module__, self.__class__.__name__)) class RegexPattern(Pattern): """ The :class:`RegexPattern` class is an implementation of a pattern using regular expressions. """ # Make the class dict-less. __slots__ = ('regex',) def __init__(self, pattern, include=None): # type: (Union[AnyStr, RegexHint], Optional[bool]) -> None """ Initializes the :class:`RegexPattern` instance. *pattern* (:class:`unicode`, :class:`bytes`, :class:`re.RegexObject`, 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.RegexObject`) 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. """ self.regex = None """ *regex* (:class:`re.RegexObject`) is the regular expression for the pattern. """ if isinstance(pattern, (unicode, bytes)): assert include is None, "include:{!r} must be null when pattern:{!r} is a string.".format(include, pattern) 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, "include:{!r} must be null when pattern:{!r} is null.".format(include, pattern) else: raise TypeError("pattern:{!r} is not a string, RegexObject, or None.".format(pattern)) super(RegexPattern, self).__init__(include) self.regex = regex def __eq__(self, other): # type: (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 def match(self, files): # type: (Iterable[Text]) -> Iterable[Text] """ Matches this pattern against the specified files. *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains each file relative to the root directory (e.g., "relative/path/to/file"). Returns an :class:`~collections.abc.Iterable` yielding each matched file path (:class:`str`). """ if self.include is not None: for path in files: if self.regex.match(path) is not None: yield path @classmethod def pattern_to_regex(cls, pattern): # type: (Text) -> Tuple[Text, bool] """ Convert the pattern into an uncompiled regular expression. *pattern* (:class:`str`) is the pattern to convert into a regular expression. Returns the uncompiled regular expression (:class:`str` or :data:`None`), and 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 pathspec-0.9.0/pathspec/tests/0000755000232200023220000000000014074672614016661 5ustar debalancedebalancepathspec-0.9.0/pathspec/tests/test_pathspec.py0000644000232200023220000001135514061160155022072 0ustar debalancedebalance# encoding: utf-8 """ This script tests ``PathSpec``. """ import unittest import pathspec class PathSpecTest(unittest.TestCase): """ The ``PathSpecTest`` class tests the ``PathSpec`` class. """ def test_01_absolute_dir_paths_1(self): """ Tests that absolute paths will be properly normalized and matched. """ spec = pathspec.PathSpec.from_lines('gitwildmatch', [ 'foo', ]) results = set(spec.match_files([ '/a.py', '/foo/a.py', '/x/a.py', '/x/foo/a.py', 'a.py', 'foo/a.py', 'x/a.py', 'x/foo/a.py', ])) self.assertEqual(results, { '/foo/a.py', '/x/foo/a.py', 'foo/a.py', 'x/foo/a.py', }) def test_01_absolute_dir_paths_2(self): """ Tests that absolute paths will be properly normalized and matched. """ spec = pathspec.PathSpec.from_lines('gitwildmatch', [ '/foo', ]) results = set(spec.match_files([ '/a.py', '/foo/a.py', '/x/a.py', '/x/foo/a.py', 'a.py', 'foo/a.py', 'x/a.py', 'x/foo/a.py', ])) self.assertEqual(results, { '/foo/a.py', 'foo/a.py', }) def test_01_current_dir_paths(self): """ Tests that paths referencing the current directory will be properly normalized and matched. """ spec = pathspec.PathSpec.from_lines('gitwildmatch', [ '*.txt', '!test1/', ]) results = set(spec.match_files([ './src/test1/a.txt', './src/test1/b.txt', './src/test1/c/c.txt', './src/test2/a.txt', './src/test2/b.txt', './src/test2/c/c.txt', ])) self.assertEqual(results, { './src/test2/a.txt', './src/test2/b.txt', './src/test2/c/c.txt', }) def test_01_match_files(self): """ Tests that matching files one at a time yields the same results as matching multiples files at once. """ spec = pathspec.PathSpec.from_lines('gitwildmatch', [ '*.txt', '!test1/', ]) test_files = [ 'src/test1/a.txt', 'src/test1/b.txt', 'src/test1/c/c.txt', 'src/test2/a.txt', 'src/test2/b.txt', 'src/test2/c/c.txt', ] single_results = set(filter(spec.match_file, test_files)) multi_results = set(spec.match_files(test_files)) self.assertEqual(single_results, multi_results) def test_01_windows_current_dir_paths(self): """ Tests that paths referencing the current directory will be properly normalized and matched. """ spec = pathspec.PathSpec.from_lines('gitwildmatch', [ '*.txt', '!test1/', ]) results = set(spec.match_files([ '.\\src\\test1\\a.txt', '.\\src\\test1\\b.txt', '.\\src\\test1\\c\\c.txt', '.\\src\\test2\\a.txt', '.\\src\\test2\\b.txt', '.\\src\\test2\\c\\c.txt', ], separators=('\\',))) self.assertEqual(results, { '.\\src\\test2\\a.txt', '.\\src\\test2\\b.txt', '.\\src\\test2\\c\\c.txt', }) def test_01_windows_paths(self): """ Tests that Windows paths will be properly normalized and matched. """ spec = pathspec.PathSpec.from_lines('gitwildmatch', [ '*.txt', '!test1/', ]) results = set(spec.match_files([ 'src\\test1\\a.txt', 'src\\test1\\b.txt', 'src\\test1\\c\\c.txt', 'src\\test2\\a.txt', 'src\\test2\\b.txt', 'src\\test2\\c\\c.txt', ], separators=('\\',))) self.assertEqual(results, { 'src\\test2\\a.txt', 'src\\test2\\b.txt', 'src\\test2\\c\\c.txt', }) def test_02_eq(self): """ Tests equality. """ first_spec = pathspec.PathSpec.from_lines('gitwildmatch', [ '*.txt', '!test1/', ]) second_spec = pathspec.PathSpec.from_lines('gitwildmatch', [ '*.txt', '!test1/', ]) self.assertEqual(first_spec, second_spec) def test_02_ne(self): """ Tests equality. """ first_spec = pathspec.PathSpec.from_lines('gitwildmatch', [ '*.txt', ]) second_spec = pathspec.PathSpec.from_lines('gitwildmatch', [ '!*.txt', ]) self.assertNotEqual(first_spec, second_spec) def test_01_addition(self): """ Test pattern addition using + operator """ first_spec = pathspec.PathSpec.from_lines('gitwildmatch', [ 'test.txt', 'test.png' ]) second_spec = pathspec.PathSpec.from_lines('gitwildmatch', [ 'test.html', 'test.jpg' ]) combined_spec = first_spec + second_spec results = set(combined_spec.match_files([ 'test.txt', 'test.png', 'test.html', 'test.jpg' ], separators=('\\',))) self.assertEqual(results, { 'test.txt', 'test.png', 'test.html', 'test.jpg' }) def test_02_addition(self): """ Test pattern addition using += operator """ spec = pathspec.PathSpec.from_lines('gitwildmatch', [ 'test.txt', 'test.png' ]) spec += pathspec.PathSpec.from_lines('gitwildmatch', [ 'test.html', 'test.jpg' ]) results = set(spec.match_files([ 'test.txt', 'test.png', 'test.html', 'test.jpg' ], separators=('\\',))) self.assertEqual(results, { 'test.txt', 'test.png', 'test.html', 'test.jpg' }) pathspec-0.9.0/pathspec/tests/test_gitwildmatch.py0000644000232200023220000003547014074671253022761 0ustar debalancedebalance# encoding: utf-8 """ This script tests ``GitWildMatchPattern``. """ from __future__ import unicode_literals import re import sys import unittest import pathspec.patterns.gitwildmatch import pathspec.util from pathspec.patterns.gitwildmatch import GitWildMatchPattern, GitWildMatchPatternError if sys.version_info[0] >= 3: unichr = chr class GitWildMatchTest(unittest.TestCase): """ The ``GitWildMatchTest`` class tests the ``GitWildMatchPattern`` implementation. """ def test_00_empty(self): """ Tests an empty pattern. """ regex, include = GitWildMatchPattern.pattern_to_regex('') self.assertIsNone(include) self.assertIsNone(regex) def test_01_absolute(self): """ Tests an absolute path pattern. This should match: an/absolute/file/path an/absolute/file/path/foo This should NOT match: foo/an/absolute/file/path """ regex, include = GitWildMatchPattern.pattern_to_regex('/an/absolute/file/path') self.assertTrue(include) self.assertEqual(regex, '^an/absolute/file/path(?:/.*)?$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match([ 'an/absolute/file/path', 'an/absolute/file/path/foo', 'foo/an/absolute/file/path', ])) self.assertEqual(results, { 'an/absolute/file/path', 'an/absolute/file/path/foo', }) def test_01_absolute_root(self): """ Tests a single root absolute path pattern. This should NOT match any file (according to git check-ignore (v2.4.1)). """ regex, include = GitWildMatchPattern.pattern_to_regex('/') self.assertIsNone(include) self.assertIsNone(regex) def test_01_relative(self): """ Tests a relative path pattern. This should match: spam spam/ foo/spam spam/foo foo/spam/bar """ regex, include = GitWildMatchPattern.pattern_to_regex('spam') self.assertTrue(include) self.assertEqual(regex, '^(?:.+/)?spam(?:/.*)?$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match([ 'spam', 'spam/', 'foo/spam', 'spam/foo', 'foo/spam/bar', ])) self.assertEqual(results, { 'spam', 'spam/', 'foo/spam', 'spam/foo', 'foo/spam/bar', }) def test_01_relative_nested(self): """ Tests a relative nested path pattern. This should match: foo/spam foo/spam/bar This should **not** match (according to git check-ignore (v2.4.1)): bar/foo/spam """ regex, include = GitWildMatchPattern.pattern_to_regex('foo/spam') self.assertTrue(include) self.assertEqual(regex, '^foo/spam(?:/.*)?$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match([ 'foo/spam', 'foo/spam/bar', 'bar/foo/spam', ])) self.assertEqual(results, { 'foo/spam', 'foo/spam/bar', }) def test_02_comment(self): """ Tests a comment pattern. """ regex, include = GitWildMatchPattern.pattern_to_regex('# Cork soakers.') self.assertIsNone(include) self.assertIsNone(regex) def test_02_ignore(self): """ Tests an exclude pattern. This should NOT match (according to git check-ignore (v2.4.1)): temp/foo """ regex, include = GitWildMatchPattern.pattern_to_regex('!temp') self.assertIsNotNone(include) self.assertFalse(include) self.assertEqual(regex, '^(?:.+/)?temp$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match(['temp/foo'])) self.assertEqual(results, set()) def test_03_child_double_asterisk(self): """ Tests a directory name with a double-asterisk child directory. This should match: spam/bar This should **not** match (according to git check-ignore (v2.4.1)): foo/spam/bar """ regex, include = GitWildMatchPattern.pattern_to_regex('spam/**') self.assertTrue(include) self.assertEqual(regex, '^spam/.*$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match([ 'spam/bar', 'foo/spam/bar', ])) self.assertEqual(results, {'spam/bar'}) def test_03_inner_double_asterisk(self): """ Tests a path with an inner double-asterisk directory. This should match: left/right left/bar/right left/foo/bar/right left/bar/right/foo This should **not** match (according to git check-ignore (v2.4.1)): foo/left/bar/right """ regex, include = GitWildMatchPattern.pattern_to_regex('left/**/right') self.assertTrue(include) self.assertEqual(regex, '^left(?:/.+)?/right(?:/.*)?$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match([ 'left/right', 'left/bar/right', 'left/foo/bar/right', 'left/bar/right/foo', 'foo/left/bar/right', ])) self.assertEqual(results, { 'left/right', 'left/bar/right', 'left/foo/bar/right', 'left/bar/right/foo', }) def test_03_only_double_asterisk(self): """ Tests a double-asterisk pattern which matches everything. """ regex, include = GitWildMatchPattern.pattern_to_regex('**') self.assertTrue(include) self.assertEqual(regex, '^.+$') def test_03_parent_double_asterisk(self): """ Tests a file name with a double-asterisk parent directory. This should match: spam foo/spam foo/spam/bar """ regex, include = GitWildMatchPattern.pattern_to_regex('**/spam') self.assertTrue(include) self.assertEqual(regex, '^(?:.+/)?spam(?:/.*)?$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match([ 'spam', 'foo/spam', 'foo/spam/bar', ])) self.assertEqual(results, { 'spam', 'foo/spam', 'foo/spam/bar', }) def test_03_duplicate_leading_double_asterisk_edge_case(self): """ Regression test for duplicate leading **/ bug. """ regex, include = GitWildMatchPattern.pattern_to_regex('**') self.assertTrue(include) self.assertEqual(regex, '^.+$') equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/**') self.assertTrue(include) self.assertEqual(equivalent_regex, regex) equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/**/**') self.assertTrue(include) self.assertEqual(equivalent_regex, regex) regex, include = GitWildMatchPattern.pattern_to_regex('**/api') self.assertTrue(include) self.assertEqual(regex, '^(?:.+/)?api(?:/.*)?$') equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/**/api') self.assertTrue(include) self.assertEqual(equivalent_regex, regex) regex, include = GitWildMatchPattern.pattern_to_regex('**/api/') self.assertTrue(include) self.assertEqual(regex, '^(?:.+/)?api/.*$') equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/api/**') self.assertTrue(include) self.assertEqual(equivalent_regex, regex) equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/**/api/**/**') self.assertTrue(include) self.assertEqual(equivalent_regex, regex) def test_03_double_asterisk_trailing_slash_edge_case(self): """ Tests the edge-case **/ pattern. This should match everything except individual files in the root directory. """ regex, include = GitWildMatchPattern.pattern_to_regex('**/') self.assertTrue(include) self.assertEqual(regex, '^.+/.*$') equivalent_regex, include = GitWildMatchPattern.pattern_to_regex('**/**/') self.assertTrue(include) self.assertEqual(equivalent_regex, regex) def test_04_infix_wildcard(self): """ Tests a pattern with an infix wildcard. This should match: foo--bar foo-hello-bar a/foo-hello-bar foo-hello-bar/b a/foo-hello-bar/b """ regex, include = GitWildMatchPattern.pattern_to_regex('foo-*-bar') self.assertTrue(include) self.assertEqual(regex, '^(?:.+/)?foo\\-[^/]*\\-bar(?:/.*)?$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match([ 'foo--bar', 'foo-hello-bar', 'a/foo-hello-bar', 'foo-hello-bar/b', 'a/foo-hello-bar/b', ])) self.assertEqual(results, { 'foo--bar', 'foo-hello-bar', 'a/foo-hello-bar', 'foo-hello-bar/b', 'a/foo-hello-bar/b', }) def test_04_postfix_wildcard(self): """ Tests a pattern with a postfix wildcard. This should match: ~temp- ~temp-foo ~temp-foo/bar foo/~temp-bar foo/~temp-bar/baz """ regex, include = GitWildMatchPattern.pattern_to_regex('~temp-*') self.assertTrue(include) self.assertEqual(regex, '^(?:.+/)?\\~temp\\-[^/]*(?:/.*)?$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match([ '~temp-', '~temp-foo', '~temp-foo/bar', 'foo/~temp-bar', 'foo/~temp-bar/baz', ])) self.assertEqual(results, { '~temp-', '~temp-foo', '~temp-foo/bar', 'foo/~temp-bar', 'foo/~temp-bar/baz', }) def test_04_prefix_wildcard(self): """ Tests a pattern with a prefix wildcard. This should match: bar.py bar.py/ foo/bar.py foo/bar.py/baz """ regex, include = GitWildMatchPattern.pattern_to_regex('*.py') self.assertTrue(include) self.assertEqual(regex, '^(?:.+/)?[^/]*\\.py(?:/.*)?$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match([ 'bar.py', 'bar.py/', 'foo/bar.py', 'foo/bar.py/baz', ])) self.assertEqual(results, { 'bar.py', 'bar.py/', 'foo/bar.py', 'foo/bar.py/baz', }) def test_05_directory(self): """ Tests a directory pattern. This should match: dir/ foo/dir/ foo/dir/bar This should **not** match: dir """ regex, include = GitWildMatchPattern.pattern_to_regex('dir/') self.assertTrue(include) self.assertEqual(regex, '^(?:.+/)?dir/.*$') pattern = GitWildMatchPattern(re.compile(regex), include) results = set(pattern.match([ 'dir/', 'foo/dir/', 'foo/dir/bar', 'dir', ])) self.assertEqual(results, { 'dir/', 'foo/dir/', 'foo/dir/bar', }) def test_06_registered(self): """ Tests that the pattern is registered. """ self.assertIs(pathspec.util.lookup_pattern('gitwildmatch'), GitWildMatchPattern) def test_06_access_deprecated(self): """ Tests that the pattern is accessible from the root module using the deprecated alias. """ self.assertTrue(hasattr(pathspec, 'GitIgnorePattern')) self.assertTrue(issubclass(pathspec.GitIgnorePattern, GitWildMatchPattern)) def test_06_registered_deprecated(self): """ Tests that the pattern is registered under the deprecated alias. """ self.assertIs(pathspec.util.lookup_pattern('gitignore'), pathspec.GitIgnorePattern) def test_07_encode_bytes(self): """ Test encoding bytes. """ encoded = "".join(map(unichr, range(0,256))).encode(pathspec.patterns.gitwildmatch._BYTES_ENCODING) expected = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" self.assertEqual(encoded, expected) def test_07_decode_bytes(self): """ Test decoding bytes. """ decoded = bytes(bytearray(range(0,256))).decode(pathspec.patterns.gitwildmatch._BYTES_ENCODING) expected = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" self.assertEqual(decoded, expected) def test_07_match_bytes_and_bytes(self): """ Test byte string patterns matching byte string paths. """ pattern = GitWildMatchPattern(b'*.py') results = set(pattern.match([b'a.py'])) self.assertEqual(results, {b'a.py'}) def test_07_match_bytes_and_bytes_complete(self): """ Test byte string patterns matching byte string paths. """ encoded = bytes(bytearray(range(0,256))) escaped = b"".join(b"\\" + encoded[i:i+1] for i in range(len(encoded))) pattern = GitWildMatchPattern(escaped) results = set(pattern.match([encoded])) self.assertEqual(results, {encoded}) @unittest.skipIf(sys.version_info[0] >= 3, "Python 3 is strict") def test_07_match_bytes_and_unicode(self): """ Test byte string patterns matching byte string paths. """ pattern = GitWildMatchPattern(b'*.py') results = set(pattern.match(['a.py'])) self.assertEqual(results, {'a.py'}) @unittest.skipIf(sys.version_info[0] == 2, "Python 2 is lenient") def test_07_match_bytes_and_unicode_fail(self): """ Test byte string patterns matching byte string paths. """ pattern = GitWildMatchPattern(b'*.py') with self.assertRaises(TypeError): for _ in pattern.match(['a.py']): pass @unittest.skipIf(sys.version_info[0] >= 3, "Python 3 is strict") def test_07_match_unicode_and_bytes(self): """ Test unicode patterns with byte paths. """ pattern = GitWildMatchPattern('*.py') results = set(pattern.match([b'a.py'])) self.assertEqual(results, {b'a.py'}) @unittest.skipIf(sys.version_info[0] == 2, "Python 2 is lenient") def test_07_match_unicode_and_bytes_fail(self): """ Test unicode patterns with byte paths. """ pattern = GitWildMatchPattern('*.py') with self.assertRaises(TypeError): for _ in pattern.match([b'a.py']): pass def test_07_match_unicode_and_unicode(self): """ Test unicode patterns with unicode paths. """ pattern = GitWildMatchPattern('*.py') results = set(pattern.match(['a.py'])) self.assertEqual(results, {'a.py'}) def test_08_escape(self): """ Test escaping a string with meta-characters """ fname = "file!with*weird#naming_[1].t?t" escaped = r"file\!with\*weird\#naming_\[1\].t\?t" result = GitWildMatchPattern.escape(fname) self.assertEqual(result, escaped) def test_09_single_escape_fail(self): """ Test an escape on a line by itself. """ self._check_invalid_pattern("\\") def test_09_single_exclamation_mark_fail(self): """ Test an escape on a line by itself. """ self._check_invalid_pattern("!") def _check_invalid_pattern(self, git_ignore_pattern): expected_message_pattern = re.escape(repr(git_ignore_pattern)) with self.assertRaisesRegexp(GitWildMatchPatternError, expected_message_pattern): GitWildMatchPattern(git_ignore_pattern) pathspec-0.9.0/pathspec/tests/__init__.py0000644000232200023220000000000013615711002020741 0ustar debalancedebalancepathspec-0.9.0/pathspec/tests/test_util.py0000644000232200023220000001734213642761677021266 0ustar debalancedebalance# encoding: utf-8 """ This script tests utility functions. """ import errno import os import os.path import shutil import sys import tempfile import unittest from pathspec.util import iter_tree_entries, iter_tree_files, RecursionError, normalize_file class IterTreeTest(unittest.TestCase): """ The ``IterTreeTest`` class tests `pathspec.util.iter_tree_files()`. """ def make_dirs(self, dirs): """ Create the specified directories. """ for dir in dirs: os.mkdir(os.path.join(self.temp_dir, self.ospath(dir))) def make_files(self, files): """ Create the specified files. """ for file in files: self.mkfile(os.path.join(self.temp_dir, self.ospath(file))) def make_links(self, links): """ Create the specified links. """ for link, node in links: os.symlink(os.path.join(self.temp_dir, self.ospath(node)), os.path.join(self.temp_dir, self.ospath(link))) @staticmethod def mkfile(file): """ Creates an empty file. """ with open(file, 'wb'): pass @staticmethod def ospath(path): """ Convert the POSIX path to a native OS path. """ return os.path.join(*path.split('/')) def require_realpath(self): """ Skips the test if `os.path.realpath` does not properly support symlinks. """ if self.broken_realpath: raise unittest.SkipTest("`os.path.realpath` is broken.") def require_symlink(self): """ Skips the test if `os.symlink` is not supported. """ if self.no_symlink: raise unittest.SkipTest("`os.symlink` is not supported.") def setUp(self): """ Called before each test. """ self.temp_dir = tempfile.mkdtemp() def tearDown(self): """ Called after each test. """ shutil.rmtree(self.temp_dir) def test_1_files(self): """ Tests to make sure all files are found. """ self.make_dirs([ 'Empty', 'Dir', 'Dir/Inner', ]) self.make_files([ 'a', 'b', 'Dir/c', 'Dir/d', 'Dir/Inner/e', 'Dir/Inner/f', ]) results = set(iter_tree_files(self.temp_dir)) self.assertEqual(results, set(map(self.ospath, [ 'a', 'b', 'Dir/c', 'Dir/d', 'Dir/Inner/e', 'Dir/Inner/f', ]))) def test_2_0_check_symlink(self): """ Tests whether links can be created. """ # NOTE: Windows does not support `os.symlink` for Python 2. Windows # Vista and greater supports `os.symlink` for Python 3.2+. no_symlink = None try: file = os.path.join(self.temp_dir, 'file') link = os.path.join(self.temp_dir, 'link') self.mkfile(file) try: os.symlink(file, link) except (AttributeError, NotImplementedError): no_symlink = True raise no_symlink = False finally: self.__class__.no_symlink = no_symlink def test_2_1_check_realpath(self): """ Tests whether `os.path.realpath` works properly with symlinks. """ # NOTE: Windows does not follow symlinks with `os.path.realpath` # which is what we use to detect recursion. See # for details. broken_realpath = None try: self.require_symlink() file = os.path.join(self.temp_dir, 'file') link = os.path.join(self.temp_dir, 'link') self.mkfile(file) os.symlink(file, link) try: self.assertEqual(os.path.realpath(file), os.path.realpath(link)) except AssertionError: broken_realpath = True raise broken_realpath = False finally: self.__class__.broken_realpath = broken_realpath def test_2_2_links(self): """ Tests to make sure links to directories and files work. """ self.require_symlink() self.make_dirs([ 'Dir', ]) self.make_files([ 'a', 'b', 'Dir/c', 'Dir/d', ]) self.make_links([ ('ax', 'a'), ('bx', 'b'), ('Dir/cx', 'Dir/c'), ('Dir/dx', 'Dir/d'), ('DirX', 'Dir'), ]) results = set(iter_tree_files(self.temp_dir)) self.assertEqual(results, set(map(self.ospath, [ 'a', 'ax', 'b', 'bx', 'Dir/c', 'Dir/cx', 'Dir/d', 'Dir/dx', 'DirX/c', 'DirX/cx', 'DirX/d', 'DirX/dx', ]))) def test_2_3_sideways_links(self): """ Tests to make sure the same directory can be encountered multiple times via links. """ self.require_symlink() self.make_dirs([ 'Dir', 'Dir/Target', ]) self.make_files([ 'Dir/Target/file', ]) self.make_links([ ('Ax', 'Dir'), ('Bx', 'Dir'), ('Cx', 'Dir/Target'), ('Dx', 'Dir/Target'), ('Dir/Ex', 'Dir/Target'), ('Dir/Fx', 'Dir/Target'), ]) results = set(iter_tree_files(self.temp_dir)) self.assertEqual(results, set(map(self.ospath, [ 'Ax/Ex/file', 'Ax/Fx/file', 'Ax/Target/file', 'Bx/Ex/file', 'Bx/Fx/file', 'Bx/Target/file', 'Cx/file', 'Dx/file', 'Dir/Ex/file', 'Dir/Fx/file', 'Dir/Target/file', ]))) def test_2_4_recursive_links(self): """ Tests detection of recursive links. """ self.require_symlink() self.require_realpath() self.make_dirs([ 'Dir', ]) self.make_files([ 'Dir/file', ]) self.make_links([ ('Dir/Self', 'Dir'), ]) with self.assertRaises(RecursionError) as context: set(iter_tree_files(self.temp_dir)) self.assertEqual(context.exception.first_path, 'Dir') self.assertEqual(context.exception.second_path, self.ospath('Dir/Self')) def test_2_5_recursive_circular_links(self): """ Tests detection of recursion through circular links. """ self.require_symlink() self.require_realpath() self.make_dirs([ 'A', 'B', 'C', ]) self.make_files([ 'A/d', 'B/e', 'C/f', ]) self.make_links([ ('A/Bx', 'B'), ('B/Cx', 'C'), ('C/Ax', 'A'), ]) with self.assertRaises(RecursionError) as context: set(iter_tree_files(self.temp_dir)) self.assertIn(context.exception.first_path, ('A', 'B', 'C')) self.assertEqual(context.exception.second_path, { 'A': self.ospath('A/Bx/Cx/Ax'), 'B': self.ospath('B/Cx/Ax/Bx'), 'C': self.ospath('C/Ax/Bx/Cx'), }[context.exception.first_path]) def test_2_6_detect_broken_links(self): """ Tests that broken links are detected. """ def reraise(e): raise e self.require_symlink() self.make_links([ ('A', 'DOES_NOT_EXIST'), ]) with self.assertRaises(OSError) as context: set(iter_tree_files(self.temp_dir, on_error=reraise)) self.assertEqual(context.exception.errno, errno.ENOENT) def test_2_7_ignore_broken_links(self): """ Tests that broken links are ignored. """ self.require_symlink() self.make_links([ ('A', 'DOES_NOT_EXIST'), ]) results = set(iter_tree_files(self.temp_dir)) self.assertEqual(results, set()) def test_2_8_no_follow_links(self): """ Tests to make sure directory links can be ignored. """ self.require_symlink() self.make_dirs([ 'Dir', ]) self.make_files([ 'A', 'B', 'Dir/C', 'Dir/D', ]) self.make_links([ ('Ax', 'A'), ('Bx', 'B'), ('Dir/Cx', 'Dir/C'), ('Dir/Dx', 'Dir/D'), ('DirX', 'Dir'), ]) results = set(iter_tree_files(self.temp_dir, follow_links=False)) self.assertEqual(results, set(map(self.ospath, [ 'A', 'Ax', 'B', 'Bx', 'Dir/C', 'Dir/Cx', 'Dir/D', 'Dir/Dx', 'DirX', ]))) def test_3_entries(self): """ Tests to make sure all files are found. """ self.make_dirs([ 'Empty', 'Dir', 'Dir/Inner', ]) self.make_files([ 'a', 'b', 'Dir/c', 'Dir/d', 'Dir/Inner/e', 'Dir/Inner/f', ]) results = {entry.path for entry in iter_tree_entries(self.temp_dir)} self.assertEqual(results, set(map(self.ospath, [ 'a', 'b', 'Dir', 'Dir/c', 'Dir/d', 'Dir/Inner', 'Dir/Inner/e', 'Dir/Inner/f', 'Empty', ]))) @unittest.skipIf(sys.version_info < (3, 4), "pathlib entered stdlib in Python 3.4") def test_4_normalizing_pathlib_path(self): """ Tests passing pathlib.Path as argument. """ from pathlib import Path first_spec = normalize_file(Path('a.txt')) second_spec = normalize_file('a.txt') self.assertEqual(first_spec, second_spec) pathspec-0.9.0/pathspec/__init__.py0000644000232200023220000000207514056050446017625 0ustar debalancedebalance# encoding: utf-8 """ The *pathspec* package provides pattern matching for file paths. So far this only includes Git's wildmatch pattern matching (the style used for ".gitignore" files). The following classes are imported and made available from the root of the `pathspec` package: - :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.iter_tree` - :func:`pathspec.util.lookup_pattern` - :func:`pathspec.util.match_files` """ from __future__ import unicode_literals from .pathspec import PathSpec from .pattern import Pattern, RegexPattern from .util import iter_tree, lookup_pattern, match_files, RecursionError from ._meta import ( __author__, __copyright__, __credits__, __license__, __version__, ) # Load pattern implementations. from . import patterns # Expose `GitIgnorePattern` class in the root module for backward # compatibility with v0.4. from .patterns.gitwildmatch import GitIgnorePattern pathspec-0.9.0/pathspec/_meta.py0000644000232200023220000000312114074671016017146 0ustar debalancedebalance# encoding: utf-8 """ This module contains the project meta-data. """ __author__ = "Caleb P. Burns" __copyright__ = "Copyright © 2013-2021 Caleb P. Burns" __credits__ = [ "dahlia ", "highb ", "029xue ", "mikexstudios ", "nhumrich ", "davidfraser ", "demurgos ", "ghickman ", "nvie ", "adrienverge ", "AndersBlomdell ", "highb ", "thmxv ", "wimglenn ", "hugovk ", "dcecile ", "mroutis ", "jdufresne ", "groodt ", "ftrofin ", "pykong ", "nhhollander ", "KOLANICH ", "JonjonHays ", "Isaac0616 ", "SebastiaanZ ", "RoelAdriaans ", "raviselker ", "johanvergeer ", "danjer ", "jhbuhrman ", "WPDOrdina ", ] __license__ = "MPL 2.0" __version__ = "0.9.0" pathspec-0.9.0/pathspec/patterns/0000755000232200023220000000000014074672614017357 5ustar debalancedebalancepathspec-0.9.0/pathspec/patterns/__init__.py0000644000232200023220000000027013615711002021450 0ustar debalancedebalance# encoding: utf-8 """ The *pathspec.patterns* package contains the pattern matching implementations. """ # Load pattern implementations. from .gitwildmatch import GitWildMatchPattern pathspec-0.9.0/pathspec/patterns/gitwildmatch.py0000644000232200023220000002717314074670065022421 0ustar debalancedebalance# encoding: utf-8 """ This module implements Git's wildmatch pattern matching which itself is derived from Rsync's wildmatch. Git uses wildmatch for its ".gitignore" files. """ from __future__ import unicode_literals import re import warnings try: from typing import ( AnyStr, Optional, Text, Tuple) except ImportError: pass from .. import util from ..compat import unicode from ..pattern import RegexPattern #: The encoding to use when parsing a byte string pattern. _BYTES_ENCODING = 'latin1' class GitWildMatchPatternError(ValueError): """ The :class:`GitWildMatchPatternError` indicates an invalid git wild match pattern. """ pass class GitWildMatchPattern(RegexPattern): """ The :class:`GitWildMatchPattern` class represents a compiled Git wildmatch pattern. """ # Keep the dict-less class hierarchy. __slots__ = () @classmethod def pattern_to_regex(cls, pattern): # type: (AnyStr) -> Tuple[Optional[AnyStr], Optional[bool]] """ Convert the pattern into a regular expression. *pattern* (:class:`unicode` or :class:`bytes`) is the pattern to convert into a regular expression. Returns the uncompiled regular expression (:class:`unicode`, :class:`bytes`, or :data:`None`), and whether matched files should be included (:data:`True`), excluded (:data:`False`), or if it is a null-operation (:data:`None`). """ if isinstance(pattern, unicode): return_type = unicode elif isinstance(pattern, bytes): return_type = bytes pattern = pattern.decode(_BYTES_ENCODING) else: raise TypeError("pattern:{!r} is not a unicode or byte string.".format(pattern)) original_pattern = pattern pattern = pattern.strip() if pattern.startswith('#'): # A pattern starting with a hash ('#') serves as a comment # (neither includes nor excludes files). Escape the hash with a # back-slash to match a literal hash (i.e., '\#'). regex = None include = None elif pattern == '/': # EDGE CASE: According to `git check-ignore` (v2.4.1), a single # '/' does not match any file. regex = None include = None elif pattern: if pattern.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 = pattern[1:] else: include = True if pattern.startswith('\\'): # Remove leading back-slash escape for escaped hash ('#') or # exclamation mark ('!'). pattern = pattern[1:] # Allow a regex override for edge cases that cannot be handled # through normalization. override_regex = None # Split pattern into segments. pattern_segs = pattern.split('/') # Normalize pattern to make processing easier. # EDGE CASE: Deal with duplicate double-asterisk sequences. # Collapse each sequence down to one double-asterisk. Iterate over # the segments in reverse 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] if len(pattern_segs) == 2 and pattern_segs[0] == '**' and not pattern_segs[1]: # EDGE CASE: The '**/' pattern should match everything except # individual files in the root directory. This case cannot be # adequately handled through normalization. Use the override. override_regex = '^.+/.*$' if not pattern_segs[0]: # A pattern beginning with a slash ('/') will only match paths # directly on the root directory instead of any descendant # paths. So, remove empty first segment to make 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 pattern without a beginning slash ('/') will match # any descendant path. This is equivalent to "**/{pattern}". So, # prepend with double-asterisks to make pattern relative to # root. # EDGE CASE: This also holds for a single pattern with a # trailing slash (e.g. dir/). if pattern_segs[0] != '**': pattern_segs.insert(0, '**') else: # EDGE CASE: A pattern without a beginning slash ('/') but # contains at least one prepended directory (e.g. # "dir/{pattern}") should not match "**/dir/{pattern}", # according to `git check-ignore` (v2.4.1). pass if not pattern_segs: # After resolving the edge cases, we end up with no # pattern at all. This must be because the pattern is # invalid. raise GitWildMatchPatternError("Invalid git pattern: %r" % (original_pattern,)) if not pattern_segs[-1] and len(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}/**". So, set # last segment to a double-asterisk to include all # descendants. pattern_segs[-1] = '**' if override_regex is None: # Build regular expression from pattern. output = ['^'] need_slash = False end = len(pattern_segs) - 1 for i, seg in enumerate(pattern_segs): if seg == '**': if i == 0 and i == end: # A pattern consisting solely of double-asterisks ('**') # will match every path. output.append('.+') elif i == 0: # A normalized pattern beginning with double-asterisks # ('**') will match any leading path segments. output.append('(?:.+/)?') need_slash = False elif i == end: # A normalized pattern ending with double-asterisks ('**') # will match any trailing path segments. output.append('/.*') else: # A pattern with inner double-asterisks ('**') will match # multiple (or zero) inner path segments. output.append('(?:/.+)?') need_slash = True elif seg == '*': # Match single path segment. if need_slash: output.append('/') output.append('[^/]+') need_slash = True else: # Match segment glob pattern. if need_slash: output.append('/') output.append(cls._translate_segment_glob(seg)) if i == end and include is True: # 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. # EDGE CASE: However, this does not hold for exclusion cases # according to `git check-ignore` (v2.4.1). output.append('(?:/.*)?') need_slash = True output.append('$') regex = ''.join(output) else: # Use regex override. regex = override_regex else: # A blank pattern is a null-operation (neither includes nor # excludes files). regex = None include = None if regex is not None and return_type is bytes: regex = regex.encode(_BYTES_ENCODING) return regex, include @staticmethod def _translate_segment_glob(pattern): # type: (Text) -> Text """ 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 brack expression negation. if j < end and 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] == '!': # Braket 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. To maintain consistency with undefined behavior, # I am escaping the '^' as well. 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) return regex @staticmethod def escape(s): # type: (AnyStr) -> AnyStr """ Escape special characters in the given string. *s* (:class:`unicode` 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:`unicode` or :class:`bytes`) """ if isinstance(s, unicode): return_type = unicode string = s elif isinstance(s, bytes): return_type = bytes string = s.decode(_BYTES_ENCODING) else: raise TypeError("s:{!r} is not a unicode or byte string.".format(s)) # Reference: https://git-scm.com/docs/gitignore#_pattern_format meta_characters = r"[]!*#?" out_string = "".join("\\" + x if x in meta_characters else x for x in string) if return_type is bytes: return out_string.encode(_BYTES_ENCODING) else: return out_string util.register_pattern('gitwildmatch', GitWildMatchPattern) class GitIgnorePattern(GitWildMatchPattern): """ The :class:`GitIgnorePattern` class is deprecated by :class:`GitWildMatchPattern`. This class only exists to maintain compatibility with v0.4. """ def __init__(self, *args, **kw): """ Warn about deprecation. """ self._deprecated() super(GitIgnorePattern, self).__init__(*args, **kw) @staticmethod def _deprecated(): """ Warn about deprecation. """ warnings.warn("GitIgnorePattern ('gitignore') is deprecated. Use GitWildMatchPattern ('gitwildmatch') instead.", DeprecationWarning, stacklevel=3) @classmethod def pattern_to_regex(cls, *args, **kw): """ Warn about deprecation. """ cls._deprecated() return super(GitIgnorePattern, cls).pattern_to_regex(*args, **kw) # Register `GitIgnorePattern` as "gitignore" for backward compatibility # with v0.4. util.register_pattern('gitignore', GitIgnorePattern) pathspec-0.9.0/MANIFEST.in0000644000232200023220000000005314056050446015435 0ustar debalancedebalanceinclude *.py include *.rst include LICENSE pathspec-0.9.0/setup.cfg0000644000232200023220000000246114074672614015534 0ustar debalancedebalance[metadata] author = Caleb P. Burns author_email = cpburnz@gmail.com classifiers = Development Status :: 4 - Beta Intended Audience :: Developers License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Libraries :: Python Modules Topic :: Utilities description = Utility library for gitignore style pattern matching of file paths. license = MPL 2.0 long_description = file: README.rst, CHANGES.rst long_description_content_type = text/x-rst name = pathspec version = attr: pathspec_meta.__version__ url = https://github.com/cpburnz/python-path-specification [options] packages = find: python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* setup_requires = setuptools >=39.2.0 test_suite = pathspec.tests [bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 pathspec-0.9.0/PKG-INFO0000644000232200023220000003553014074672614015013 0ustar debalancedebalanceMetadata-Version: 2.1 Name: pathspec Version: 0.9.0 Summary: Utility library for gitignore style pattern matching of file paths. Home-page: https://github.com/cpburnz/python-path-specification Author: Caleb P. Burns Author-email: cpburnz@gmail.com License: MPL 2.0 Description: *pathspec*: Path Specification ============================== *pathspec* is a utility library for pattern matching of file paths. So far this only includes Git's wildmatch pattern matching which itself is derived from Rsync's wildmatch. Git uses wildmatch for its `gitignore`_ files. .. _`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:: >>> import pathspec >>> # The gitignore-style patterns for files to select, but we're including >>> # instead of ignoring. >>> spec = """ ... ... # 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 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/ ... ... """ We want to use the ``GitWildMatchPattern`` class to compile our patterns. The ``PathSpec`` class provides an interface around pattern implementations:: >>> spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, spec.splitlines()) That may be a mouthful but it allows for additional patterns to be implemented in the future without them having to deal with anything but matching the paths sent to them. ``GitWildMatchPattern`` is the implementation of the actual pattern which internally gets converted into a regular expression. ``PathSpec`` is a simple wrapper around a list of compiled patterns. To make things simpler, we can use the registered name for a pattern class instead of always having to provide a reference to the class itself. The ``GitWildMatchPattern`` class is registered as **gitwildmatch**:: >>> spec = pathspec.PathSpec.from_lines('gitwildmatch', spec.splitlines()) If we wanted to manually compile the patterns we can just do the following:: >>> patterns = map(pathspec.patterns.GitWildMatchPattern, spec.splitlines()) >>> spec = PathSpec(patterns) ``PathSpec.from_lines()`` is simply a class method which does just that. If you want to load the patterns from file, you can pass the file instance directly as well:: >>> with open('patterns.list', 'r') as fh: >>> spec = pathspec.PathSpec.from_lines('gitwildmatch', fh) You can perform matching on a whole directory tree with:: >>> matches = spec.match_tree('path/to/directory') Or you can perform matching on a specific set of file paths with:: >>> matches = spec.match_files(file_paths) Or check to see if an individual file matches:: >>> is_matched = spec.match_file(file_path) 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-path-specification`_. .. _`cpburnz/python-path-specification`: https://github.com/cpburnz/python-path-specification Installation ------------ *pathspec* requires the following packages: - `setuptools`_ *pathspec* can be installed from source with:: python setup.py install *pathspec* is also available for install through `PyPI`_:: pip install pathspec .. _`setuptools`: https://pypi.python.org/pypi/setuptools .. _`PyPI`: http://pypi.python.org/pypi/pathspec Documentation ------------- Documentation for *pathspec* is available on `Read the Docs`_. .. _`Read the Docs`: http://python-path-specification.readthedocs.io Other Languages --------------- *pathspec* is also available as a `Ruby gem`_. .. _`Ruby gem`: https://github.com/highb/pathspec-ruby Change History ============== 0.9.0 (2021-07-17) ------------------ - `Issue #44`_/`Issue #50`_: Raise `GitWildMatchPatternError` for invalid git patterns. - `Issue #45`_: Fix for duplicate leading double-asterisk, and edge cases. - `Issue #46`_: Fix matching absolute paths. - API change: `util.normalize_files()` now returns a `Dict[str, List[pathlike]]` instead of a `Dict[str, pathlike]`. - Added type hinting. .. _`Issue #44`: https://github.com/cpburnz/python-path-specification/issues/44 .. _`Issue #45`: https://github.com/cpburnz/python-path-specification/pull/45 .. _`Issue #46`: https://github.com/cpburnz/python-path-specification/issues/46 .. _`Issue #50`: https://github.com/cpburnz/python-path-specification/pull/50 0.8.1 (2020-11-07) ------------------ - `Issue #43`_: Add support for addition operator. .. _`Issue #43`: https://github.com/cpburnz/python-path-specification/pull/43 0.8.0 (2020-04-09) ------------------ - `Issue #30`_: Expose what patterns matched paths. Added `util.detailed_match_files()`. - `Issue #31`_: `match_tree()` doesn't return symlinks. - `Issue #34`_: Support `pathlib.Path`\ s. - Add `PathSpec.match_tree_entries` and `util.iter_tree_entries()` to support directories and symlinks. - API change: `match_tree()` has been renamed to `match_tree_files()`. The old name `match_tree()` is still available as an alias. - API change: `match_tree_files()` now returns symlinks. This is a bug fix but it will change the returned results. .. _`Issue #30`: https://github.com/cpburnz/python-path-specification/issues/30 .. _`Issue #31`: https://github.com/cpburnz/python-path-specification/issues/31 .. _`Issue #34`: https://github.com/cpburnz/python-path-specification/issues/34 0.7.0 (2019-12-27) ------------------ - `Issue #28`_: Add support for Python 3.8, and drop Python 3.4. - `Issue #29`_: Publish bdist wheel. .. _`Issue #28`: https://github.com/cpburnz/python-path-specification/pull/28 .. _`Issue #29`: https://github.com/cpburnz/python-path-specification/pull/29 0.6.0 (2019-10-03) ------------------ - `Issue #24`_: Drop support for Python 2.6, 3.2, and 3.3. - `Issue #25`_: Update README.rst. - `Issue #26`_: Method to escape gitwildmatch. .. _`Issue #24`: https://github.com/cpburnz/python-path-specification/pull/24 .. _`Issue #25`: https://github.com/cpburnz/python-path-specification/pull/25 .. _`Issue #26`: https://github.com/cpburnz/python-path-specification/pull/26 0.5.9 (2018-09-15) ------------------ - Fixed file system error handling. 0.5.8 (2018-09-15) ------------------ - Improved type checking. - Created scripts to test Python 2.6 because Tox removed support for it. - Improved byte string handling in Python 3. - `Issue #22`_: Handle dangling symlinks. .. _`Issue #22`: https://github.com/cpburnz/python-path-specification/issues/22 0.5.7 (2018-08-14) ------------------ - `Issue #21`_: Fix collections deprecation warning. .. _`Issue #21`: https://github.com/cpburnz/python-path-specification/issues/21 0.5.6 (2018-04-06) ------------------ - Improved unit tests. - Improved type checking. - `Issue #20`_: Support current directory prefix. .. _`Issue #20`: https://github.com/cpburnz/python-path-specification/issues/20 0.5.5 (2017-09-09) ------------------ - Add documentation link to README. 0.5.4 (2017-09-09) ------------------ - `Issue #17`_: Add link to Ruby implementation of *pathspec*. - Add sphinx documentation. .. _`Issue #17`: https://github.com/cpburnz/python-path-specification/pull/17 0.5.3 (2017-07-01) ------------------ - `Issue #14`_: Fix byte strings for Python 3. - `Issue #15`_: Include "LICENSE" in source package. - `Issue #16`_: Support Python 2.6. .. _`Issue #14`: https://github.com/cpburnz/python-path-specification/issues/14 .. _`Issue #15`: https://github.com/cpburnz/python-path-specification/pull/15 .. _`Issue #16`: https://github.com/cpburnz/python-path-specification/issues/16 0.5.2 (2017-04-04) ------------------ - Fixed change log. 0.5.1 (2017-04-04) ------------------ - `Issue #13`_: Add equality methods to `PathSpec` and `RegexPattern`. .. _`Issue #13`: https://github.com/cpburnz/python-path-specification/pull/13 0.5.0 (2016-08-22) ------------------ - `Issue #12`_: Add `PathSpec.match_file()`. - Renamed `gitignore.GitIgnorePattern` to `patterns.gitwildmatch.GitWildMatchPattern`. - Deprecated `gitignore.GitIgnorePattern`. .. _`Issue #12`: https://github.com/cpburnz/python-path-specification/issues/12 0.4.0 (2016-07-15) ------------------ - `Issue #11`_: Support converting patterns into regular expressions without compiling them. - API change: Subclasses of `RegexPattern` should implement `pattern_to_regex()`. .. _`Issue #11`: https://github.com/cpburnz/python-path-specification/issues/11 0.3.4 (2015-08-24) ------------------ - `Issue #7`_: Fixed non-recursive links. - `Issue #8`_: Fixed edge cases in gitignore patterns. - `Issue #9`_: Fixed minor usage documentation. - Fixed recursion detection. - Fixed trivial incompatibility with Python 3.2. .. _`Issue #7`: https://github.com/cpburnz/python-path-specification/pull/7 .. _`Issue #8`: https://github.com/cpburnz/python-path-specification/pull/8 .. _`Issue #9`: https://github.com/cpburnz/python-path-specification/pull/9 0.3.3 (2014-11-21) ------------------ - Improved documentation. 0.3.2 (2014-11-08) ------------------ - `Issue #5`_: Use tox for testing. - `Issue #6`_: Fixed matching Windows paths. - Improved documentation. - API change: `spec.match_tree()` and `spec.match_files()` now return iterators instead of sets. .. _`Issue #5`: https://github.com/cpburnz/python-path-specification/pull/5 .. _`Issue #6`: https://github.com/cpburnz/python-path-specification/issues/6 0.3.1 (2014-09-17) ------------------ - Updated README. 0.3.0 (2014-09-17) ------------------ - `Issue #3`_: Fixed trailing slash in gitignore patterns. - `Issue #4`_: Fixed test for trailing slash in gitignore patterns. - Added registered patterns. .. _`Issue #3`: https://github.com/cpburnz/python-path-specification/pull/3 .. _`Issue #4`: https://github.com/cpburnz/python-path-specification/pull/4 0.2.2 (2013-12-17) ------------------ - Fixed setup.py. 0.2.1 (2013-12-17) ------------------ - Added tests. - Fixed comment gitignore patterns. - Fixed relative path gitignore patterns. 0.2.0 (2013-12-07) ------------------ - Initial release. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta 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 :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7 Description-Content-Type: text/x-rst pathspec-0.9.0/pathspec_meta.py0000644000232200023220000000312114074671016017067 0ustar debalancedebalance# encoding: utf-8 """ This module contains the project meta-data. """ __author__ = "Caleb P. Burns" __copyright__ = "Copyright © 2013-2021 Caleb P. Burns" __credits__ = [ "dahlia ", "highb ", "029xue ", "mikexstudios ", "nhumrich ", "davidfraser ", "demurgos ", "ghickman ", "nvie ", "adrienverge ", "AndersBlomdell ", "highb ", "thmxv ", "wimglenn ", "hugovk ", "dcecile ", "mroutis ", "jdufresne ", "groodt ", "ftrofin ", "pykong ", "nhhollander ", "KOLANICH ", "JonjonHays ", "Isaac0616 ", "SebastiaanZ ", "RoelAdriaans ", "raviselker ", "johanvergeer ", "danjer ", "jhbuhrman ", "WPDOrdina ", ] __license__ = "MPL 2.0" __version__ = "0.9.0" pathspec-0.9.0/CHANGES.rst0000644000232200023220000001437514074671035015520 0ustar debalancedebalance Change History ============== 0.9.0 (2021-07-17) ------------------ - `Issue #44`_/`Issue #50`_: Raise `GitWildMatchPatternError` for invalid git patterns. - `Issue #45`_: Fix for duplicate leading double-asterisk, and edge cases. - `Issue #46`_: Fix matching absolute paths. - API change: `util.normalize_files()` now returns a `Dict[str, List[pathlike]]` instead of a `Dict[str, pathlike]`. - Added type hinting. .. _`Issue #44`: https://github.com/cpburnz/python-path-specification/issues/44 .. _`Issue #45`: https://github.com/cpburnz/python-path-specification/pull/45 .. _`Issue #46`: https://github.com/cpburnz/python-path-specification/issues/46 .. _`Issue #50`: https://github.com/cpburnz/python-path-specification/pull/50 0.8.1 (2020-11-07) ------------------ - `Issue #43`_: Add support for addition operator. .. _`Issue #43`: https://github.com/cpburnz/python-path-specification/pull/43 0.8.0 (2020-04-09) ------------------ - `Issue #30`_: Expose what patterns matched paths. Added `util.detailed_match_files()`. - `Issue #31`_: `match_tree()` doesn't return symlinks. - `Issue #34`_: Support `pathlib.Path`\ s. - Add `PathSpec.match_tree_entries` and `util.iter_tree_entries()` to support directories and symlinks. - API change: `match_tree()` has been renamed to `match_tree_files()`. The old name `match_tree()` is still available as an alias. - API change: `match_tree_files()` now returns symlinks. This is a bug fix but it will change the returned results. .. _`Issue #30`: https://github.com/cpburnz/python-path-specification/issues/30 .. _`Issue #31`: https://github.com/cpburnz/python-path-specification/issues/31 .. _`Issue #34`: https://github.com/cpburnz/python-path-specification/issues/34 0.7.0 (2019-12-27) ------------------ - `Issue #28`_: Add support for Python 3.8, and drop Python 3.4. - `Issue #29`_: Publish bdist wheel. .. _`Issue #28`: https://github.com/cpburnz/python-path-specification/pull/28 .. _`Issue #29`: https://github.com/cpburnz/python-path-specification/pull/29 0.6.0 (2019-10-03) ------------------ - `Issue #24`_: Drop support for Python 2.6, 3.2, and 3.3. - `Issue #25`_: Update README.rst. - `Issue #26`_: Method to escape gitwildmatch. .. _`Issue #24`: https://github.com/cpburnz/python-path-specification/pull/24 .. _`Issue #25`: https://github.com/cpburnz/python-path-specification/pull/25 .. _`Issue #26`: https://github.com/cpburnz/python-path-specification/pull/26 0.5.9 (2018-09-15) ------------------ - Fixed file system error handling. 0.5.8 (2018-09-15) ------------------ - Improved type checking. - Created scripts to test Python 2.6 because Tox removed support for it. - Improved byte string handling in Python 3. - `Issue #22`_: Handle dangling symlinks. .. _`Issue #22`: https://github.com/cpburnz/python-path-specification/issues/22 0.5.7 (2018-08-14) ------------------ - `Issue #21`_: Fix collections deprecation warning. .. _`Issue #21`: https://github.com/cpburnz/python-path-specification/issues/21 0.5.6 (2018-04-06) ------------------ - Improved unit tests. - Improved type checking. - `Issue #20`_: Support current directory prefix. .. _`Issue #20`: https://github.com/cpburnz/python-path-specification/issues/20 0.5.5 (2017-09-09) ------------------ - Add documentation link to README. 0.5.4 (2017-09-09) ------------------ - `Issue #17`_: Add link to Ruby implementation of *pathspec*. - Add sphinx documentation. .. _`Issue #17`: https://github.com/cpburnz/python-path-specification/pull/17 0.5.3 (2017-07-01) ------------------ - `Issue #14`_: Fix byte strings for Python 3. - `Issue #15`_: Include "LICENSE" in source package. - `Issue #16`_: Support Python 2.6. .. _`Issue #14`: https://github.com/cpburnz/python-path-specification/issues/14 .. _`Issue #15`: https://github.com/cpburnz/python-path-specification/pull/15 .. _`Issue #16`: https://github.com/cpburnz/python-path-specification/issues/16 0.5.2 (2017-04-04) ------------------ - Fixed change log. 0.5.1 (2017-04-04) ------------------ - `Issue #13`_: Add equality methods to `PathSpec` and `RegexPattern`. .. _`Issue #13`: https://github.com/cpburnz/python-path-specification/pull/13 0.5.0 (2016-08-22) ------------------ - `Issue #12`_: Add `PathSpec.match_file()`. - Renamed `gitignore.GitIgnorePattern` to `patterns.gitwildmatch.GitWildMatchPattern`. - Deprecated `gitignore.GitIgnorePattern`. .. _`Issue #12`: https://github.com/cpburnz/python-path-specification/issues/12 0.4.0 (2016-07-15) ------------------ - `Issue #11`_: Support converting patterns into regular expressions without compiling them. - API change: Subclasses of `RegexPattern` should implement `pattern_to_regex()`. .. _`Issue #11`: https://github.com/cpburnz/python-path-specification/issues/11 0.3.4 (2015-08-24) ------------------ - `Issue #7`_: Fixed non-recursive links. - `Issue #8`_: Fixed edge cases in gitignore patterns. - `Issue #9`_: Fixed minor usage documentation. - Fixed recursion detection. - Fixed trivial incompatibility with Python 3.2. .. _`Issue #7`: https://github.com/cpburnz/python-path-specification/pull/7 .. _`Issue #8`: https://github.com/cpburnz/python-path-specification/pull/8 .. _`Issue #9`: https://github.com/cpburnz/python-path-specification/pull/9 0.3.3 (2014-11-21) ------------------ - Improved documentation. 0.3.2 (2014-11-08) ------------------ - `Issue #5`_: Use tox for testing. - `Issue #6`_: Fixed matching Windows paths. - Improved documentation. - API change: `spec.match_tree()` and `spec.match_files()` now return iterators instead of sets. .. _`Issue #5`: https://github.com/cpburnz/python-path-specification/pull/5 .. _`Issue #6`: https://github.com/cpburnz/python-path-specification/issues/6 0.3.1 (2014-09-17) ------------------ - Updated README. 0.3.0 (2014-09-17) ------------------ - `Issue #3`_: Fixed trailing slash in gitignore patterns. - `Issue #4`_: Fixed test for trailing slash in gitignore patterns. - Added registered patterns. .. _`Issue #3`: https://github.com/cpburnz/python-path-specification/pull/3 .. _`Issue #4`: https://github.com/cpburnz/python-path-specification/pull/4 0.2.2 (2013-12-17) ------------------ - Fixed setup.py. 0.2.1 (2013-12-17) ------------------ - Added tests. - Fixed comment gitignore patterns. - Fixed relative path gitignore patterns. 0.2.0 (2013-12-07) ------------------ - Initial release. pathspec-0.9.0/setup.py0000644000232200023220000000007114056050446015411 0ustar debalancedebalance# encoding: utf-8 from setuptools import setup setup() pathspec-0.9.0/LICENSE0000644000232200023220000004052613614170652014716 0ustar debalancedebalanceMozilla 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. pathspec-0.9.0/tox_pip_install.py0000644000232200023220000000216114056050446017463 0ustar debalancedebalance#!/usr/bin/env python # This file is part of packagename # Made available under CC0 1.0 Universal, see LICENSE.txt # Copyright 2019-2020 Kevin Locke """ Script to reinstall pip before running `pip install`. Workaround for https://bugs.debian.org/962654 """ import os import sys # Must be invoked with pip package (optionally version-constrained) as first # argument, install options+packages as subsequent options. if len(sys.argv) < 3 or not sys.argv[1].startswith('pip'): sys.stderr.write( 'Usage: ' + sys.argv[0] + ' [options] \n' ) sys.exit(1) # Workaround is only needed on Debian (and derivatives) if os.path.exists('/etc/debian_version'): pip_exit_code = os.spawnl( os.P_WAIT, sys.executable, sys.executable, '-m', 'pip', 'install', '--force-reinstall', '--no-compile', sys.argv[1], ) if pip_exit_code != 0: sys.exit(pip_exit_code) os.execv( sys.executable, [sys.executable, '-m', 'pip', 'install'] + sys.argv[2:] )