pathspec-0.3.4/0000755000175000017500000000000012566612333013472 5ustar calebcaleb00000000000000pathspec-0.3.4/pathspec.egg-info/0000755000175000017500000000000012566612333016773 5ustar calebcaleb00000000000000pathspec-0.3.4/pathspec.egg-info/dependency_links.txt0000644000175000017500000000000112566612332023040 0ustar calebcaleb00000000000000 pathspec-0.3.4/pathspec.egg-info/top_level.txt0000644000175000017500000000001112566612332021514 0ustar calebcaleb00000000000000pathspec pathspec-0.3.4/pathspec.egg-info/SOURCES.txt0000644000175000017500000000064512566612333020664 0ustar calebcaleb00000000000000CHANGES.rst MANIFEST.in README.rst setup.cfg setup.py pathspec/__init__.py pathspec/compat.py pathspec/gitignore.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/tests/__init__.py pathspec/tests/test_gitignore.py pathspec/tests/test_pathspec.py pathspec/tests/test_util.pypathspec-0.3.4/pathspec.egg-info/PKG-INFO0000644000175000017500000001712512566612332020075 0ustar calebcaleb00000000000000Metadata-Version: 1.1 Name: pathspec Version: 0.3.4 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 `gitignore`_ style pattern matching which itself incorporates POSIX `glob`_ patterns. .. _`gitignore`: http://git-scm.com/docs/gitignore .. _`glob`: http://man7.org/linux/man-pages/man7/glob.7.html 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/q ... ... """ We want to use the ``GitIgnorePattern`` class to compile our patterns, and the ``PathSpec`` to provide an iterface around them:: >>> spec = pathspec.PathSpec.from_lines(pathspec.GitIgnorePattern, 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. ``GitIgnorePattern`` 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 ``GitIgnorePattern`` class is registered as **gitignore**:: >>> spec = pathspec.PathSpec.from_lines('gitignore', spec.splitlines()) If we wanted to manually compile the patterns we can just do the following:: >>> patterns = map(pathspec.GitIgnorePattern, spec.splitlines()) >>> spec = PathSpec(patterns) ``PathSpec.from_lines()`` is simply a simple class method to do just that. If you want to load the patterns from file, you can pass the instance directly as well:: >>> with open('patterns.list', 'r') as fh: >>> spec = pathspec.PathSpec.from_lines('gitignore', 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) 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 .. image:: https://d2weczhvl823v0.cloudfront.net/cpburnz/python-path-specification/trend.png :alt: Bitdeli badge :target: https://bitdeli.com/free Change History ============== 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. 0.3.3 (2014-11-21) ------------------ - Improved documentation. 0.3.2 (2014-11-08) ------------------ - Improved documentation. - Issue #6: Fixed matching Windows paths. - API change: `spec.match_tree` and `spec.match_files` now return iterators instead of sets 0.3.1 (2014-09-17) ------------------ - Updated README. 0.3.0 (2014-09-17) ------------------ - Added registered patterns. - Issue #3: Fixed trailing slash in gitignore patterns. - Issue #4: Fixed test for trailing slash in gitignore patterns. 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.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities pathspec-0.3.4/CHANGES.rst0000644000175000017500000000204012566612104015264 0ustar calebcaleb00000000000000 Change History ============== 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. 0.3.3 (2014-11-21) ------------------ - Improved documentation. 0.3.2 (2014-11-08) ------------------ - Improved documentation. - Issue #6: Fixed matching Windows paths. - API change: `spec.match_tree` and `spec.match_files` now return iterators instead of sets 0.3.1 (2014-09-17) ------------------ - Updated README. 0.3.0 (2014-09-17) ------------------ - Added registered patterns. - Issue #3: Fixed trailing slash in gitignore patterns. - Issue #4: Fixed test for trailing slash in gitignore patterns. 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.3.4/setup.cfg0000644000175000017500000000007312566612333015313 0ustar calebcaleb00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pathspec-0.3.4/README.rst0000644000175000017500000001012112566173361015157 0ustar calebcaleb00000000000000 *pathspec*: Path Specification ============================== *pathspec* is a utility library for pattern matching of file paths. So far this only includes `gitignore`_ style pattern matching which itself incorporates POSIX `glob`_ patterns. .. _`gitignore`: http://git-scm.com/docs/gitignore .. _`glob`: http://man7.org/linux/man-pages/man7/glob.7.html 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/q ... ... """ We want to use the ``GitIgnorePattern`` class to compile our patterns, and the ``PathSpec`` to provide an iterface around them:: >>> spec = pathspec.PathSpec.from_lines(pathspec.GitIgnorePattern, 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. ``GitIgnorePattern`` 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 ``GitIgnorePattern`` class is registered as **gitignore**:: >>> spec = pathspec.PathSpec.from_lines('gitignore', spec.splitlines()) If we wanted to manually compile the patterns we can just do the following:: >>> patterns = map(pathspec.GitIgnorePattern, spec.splitlines()) >>> spec = PathSpec(patterns) ``PathSpec.from_lines()`` is simply a simple class method to do just that. If you want to load the patterns from file, you can pass the instance directly as well:: >>> with open('patterns.list', 'r') as fh: >>> spec = pathspec.PathSpec.from_lines('gitignore', 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) 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 .. image:: https://d2weczhvl823v0.cloudfront.net/cpburnz/python-path-specification/trend.png :alt: Bitdeli badge :target: https://bitdeli.com/free pathspec-0.3.4/setup.py0000644000175000017500000000245012566201004015172 0ustar calebcaleb00000000000000# encoding: utf-8 import io from setuptools import setup, find_packages from pathspec import __author__, __email__, __license__, __project__, __version__ # Read readme and changes files. with io.open('README.rst', mode='r', encoding='UTF-8') as fh: readme = fh.read().strip() with io.open('CHANGES.rst', mode='r', encoding='UTF-8') as fh: changes = fh.read().strip() setup( name=__project__, version=__version__, author=__author__, author_email=__email__, url="https://github.com/cpburnz/python-path-specification", description="Utility library for gitignore style pattern matching of file paths.", long_description=readme + "\n\n" + changes, 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.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Utilities", ], license=__license__, packages=find_packages(), test_suite='pathspec.tests', ) pathspec-0.3.4/MANIFEST.in0000644000175000017500000000001612254075612015222 0ustar calebcaleb00000000000000include *.rst pathspec-0.3.4/PKG-INFO0000644000175000017500000001712512566612333014575 0ustar calebcaleb00000000000000Metadata-Version: 1.1 Name: pathspec Version: 0.3.4 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 `gitignore`_ style pattern matching which itself incorporates POSIX `glob`_ patterns. .. _`gitignore`: http://git-scm.com/docs/gitignore .. _`glob`: http://man7.org/linux/man-pages/man7/glob.7.html 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/q ... ... """ We want to use the ``GitIgnorePattern`` class to compile our patterns, and the ``PathSpec`` to provide an iterface around them:: >>> spec = pathspec.PathSpec.from_lines(pathspec.GitIgnorePattern, 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. ``GitIgnorePattern`` 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 ``GitIgnorePattern`` class is registered as **gitignore**:: >>> spec = pathspec.PathSpec.from_lines('gitignore', spec.splitlines()) If we wanted to manually compile the patterns we can just do the following:: >>> patterns = map(pathspec.GitIgnorePattern, spec.splitlines()) >>> spec = PathSpec(patterns) ``PathSpec.from_lines()`` is simply a simple class method to do just that. If you want to load the patterns from file, you can pass the instance directly as well:: >>> with open('patterns.list', 'r') as fh: >>> spec = pathspec.PathSpec.from_lines('gitignore', 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) 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 .. image:: https://d2weczhvl823v0.cloudfront.net/cpburnz/python-path-specification/trend.png :alt: Bitdeli badge :target: https://bitdeli.com/free Change History ============== 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. 0.3.3 (2014-11-21) ------------------ - Improved documentation. 0.3.2 (2014-11-08) ------------------ - Improved documentation. - Issue #6: Fixed matching Windows paths. - API change: `spec.match_tree` and `spec.match_files` now return iterators instead of sets 0.3.1 (2014-09-17) ------------------ - Updated README. 0.3.0 (2014-09-17) ------------------ - Added registered patterns. - Issue #3: Fixed trailing slash in gitignore patterns. - Issue #4: Fixed test for trailing slash in gitignore patterns. 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.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities pathspec-0.3.4/pathspec/0000755000175000017500000000000012566612333015301 5ustar calebcaleb00000000000000pathspec-0.3.4/pathspec/gitignore.py0000644000175000017500000001723212525650717017652 0ustar calebcaleb00000000000000# encoding: utf-8 """ This module implements gitignore style pattern matching which incorporates POSIX glob patterns. """ import re from . import util from .compat import string_types from .pattern import RegexPattern class GitIgnorePattern(RegexPattern): """ The ``GitIgnorePattern`` class represents a compiled gitignore pattern. """ # Keep the dict-less class hierarchy. __slots__ = () def __init__(self, pattern): """ Initializes the ``GitIgnorePattern`` instance. *pattern* (``str``) is the gitignore pattern. """ if not isinstance(pattern, string_types): raise TypeError("pattern:{!r} is not a string.".format(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:] # Split pattern into segments. pattern_segs = pattern.split('/') # Normalize pattern to make processing easier. 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[-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 equivilent to "{pattern}/**". So, set last segment to # double asterisks to include all descendants. pattern_segs[-1] = '**' # Build regular expression from pattern. regex = ['^'] 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. regex.append('.+') elif i == 0: # A normalized pattern beginning with double-asterisks # ('**') will match any leading path segments. regex.append('(?:.+/)?') need_slash = False elif i == end: # A normalized pattern ending with double-asterisks ('**') # will match any trailing path segments. regex.append('/.*') else: # A pattern with inner double-asterisks ('**') will match # multiple (or zero) inner path segments. regex.append('(?:/.+)?') need_slash = True elif seg == '*': # Match single path segment. if need_slash: regex.append('/') regex.append('[^/]+') need_slash = True else: # Match segment glob pattern. if need_slash: regex.append('/') regex.append(self._translate_segment_glob(seg)) if i == end and include == 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). regex.append('(?:/.*)?') need_slash = True regex.append('$') regex = ''.join(regex) else: # A blank pattern is a null-operation (neither includes nor # excludes files). regex = None include = None super(GitIgnorePattern, self).__init__(regex, include) @staticmethod def _translate_segment_glob(pattern): """ 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* (``str``) is the glob pattern. Returns the regular expression (``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 == '[': # Braket expression wildcard. Except for the beginning # exclamation mark, the whole braket expression can be used # directly as regex but we have to find where the expression # ends. # - "[][!]" matchs ']', '[' and '!'. # - "[]-]" matchs ']' and '-'. # - "[!]a-]" matchs any character except ']', 'a' and '-'. j = i # Pass brack expression negation. if j < end and pattern[j] == '!': j += 1 # Pass first closing braket if it is at the beginning of the # expression. if j < end and pattern[j] == ']': j += 1 # Find closing braket. Stop once we reach the end or find it. while j < end and pattern[j] != ']': j += 1 if j < end: # Found end of braket expression. Increment j to be one past # the closing braket: # # [...] # ^ ^ # 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 braket 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 braket expression. Escape slashes so they are # treated as literal slashes by regex as defined by POSIX. expr += pattern[i:j].replace('\\', '\\\\') # Add regex braket expression to regex result. regex += expr # Set i to one past the closing braket. i = j else: # Failed to find closing braket, treat opening braket as a # braket literal instead of as an expression. regex += '\\[' else: # Regular character, escape it for regex. regex += re.escape(char) return regex util.register_pattern('gitignore', GitIgnorePattern) pathspec-0.3.4/pathspec/util.py0000644000175000017500000001624612566173361016644 0ustar calebcaleb00000000000000# encoding: utf-8 """ This module provides utility methods for dealing with path-specs. """ import collections import os import os.path import posixpath import stat from .compat import string_types NORMALIZE_PATH_SEPS = [sep for sep in [os.sep, os.altsep] if sep and sep != posixpath.sep] """ *NORMALIZE_PATH_SEPS* (``list`` of ``str``) contains the path separators that need to be normalized to the POSIX separator for the current operating system. """ _registered_patterns = {} """ *_registered_patterns* (``dict``) maps a name (``str``) to the registered pattern factory (``callable``). """ def iter_tree(root): """ Walks the specified directory for all files. *root* (``str``) is the root directory to search for files. Raises ``RecursionError`` if recursion is detected. Returns an ``Iterable`` yielding the path to each file (``str``) relative to *root*. """ for file_rel in _iter_tree_next(os.path.abspath(root), '', {}): yield file_rel def _iter_tree_next(root_full, dir_rel, memo): """ Scan the directory for all descendant files. *root_full* (``str``) the absolute path to the root directory. *dir_rel* (``str``) the path to the directory to scan relative to *root_full*. *memo* (``dict``) keeps track of ancestor directories encountered. Maps each ancestor real path (``str``) to relative path (``str``). """ dir_full = os.path.join(root_full, dir_rel) dir_real = os.path.realpath(dir_full) # Remember each encountered ancestor directory and its canonical # (real) path. If a canonical path is encountered more than once, # recursion has occurred. if dir_real not in memo: memo[dir_real] = dir_rel else: raise RecursionError(real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel) for node in os.listdir(dir_full): node_rel = os.path.join(dir_rel, node) node_full = os.path.join(root_full, node_rel) node_stat = os.stat(node_full) if stat.S_ISDIR(node_stat.st_mode): # Child node is a directory, recurse into it and yield its # decendant files. for file_rel in _iter_tree_next(root_full, node_rel, memo): yield file_rel elif stat.S_ISREG(node_stat.st_mode): # Child node is a file, yield it. yield node_rel # NOTE: Make sure to remove the canonical (real) path of the directory # from the ancestors memo once we are done with it. This allows the # same directory to appear multiple times. If this is not done, the # second occurance of the directory will be incorrectly interpreted as # a recursion. See . del memo[dir_real] def lookup_pattern(name): """ Lookups a registered pattern factory by name. *name* (``str``) is the name of the pattern factory. Returns the registered pattern factory (``callable``). If no pattern factory is registered, raises ``KeyError``. """ return _registered_patterns[name] def match_files(patterns, files): """ Matches the files to the patterns. *patterns* (``Iterable`` of ``pathspec.Pattern``) contains the patterns to use. *files* (``Iterable`` of ``str``) contains the normalized files to be matched against *patterns*. Returns the matched files (``set`` of ``str``). """ all_files = files if isinstance(files, collections.Container) 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_files(files, separators=None): """ Normalizes the file paths to use the POSIX path separator (i.e., `/`). *files* (``Iterable`` of ``str``) contains the file paths to be normalized. *separators* (``Container`` of ``str``) optionally contains the path separators to normalize. Returns a ``dict`` mapping the normalized file path (``str``) to the original file path (``str``) """ if separators is None: separators = NORMALIZE_PATH_SEPS file_map = {} for path in files: norm = path for sep in separators: norm = norm.replace(sep, posixpath.sep) file_map[norm] = path return file_map def register_pattern(name, pattern_factory, override=None): """ Registers the specified pattern factory. *name* (``str``) is the name to register the pattern factory under. *pattern_factory* (``callable``) is used to compile patterns. It must accept an uncompiled pattern (``str``) and return the compiled pattern (``pathspec.Pattern``). *override* (``bool``) optionally is whether to allow overriding an already registered pattern under the same name (``True``), instead of raising an ``AlreadyRegisteredError`` (``False``). Default is ``None`` for ``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 ``AlreadyRegisteredError`` exception is raised when a pattern factory is registered under a name already in use. """ def __init__(self, name, pattern_factory): """ Initializes the ``AlreadyRegisteredError`` instance. *name* (``str``) is the name of the registered pattern. *pattern_factory* (``callable``) is the registered pattern factory. """ super(AlreadyRegisteredError, self).__init__(name, pattern_factory) @property def message(self): """ *message* (``str``) is the error message. """ return "{name!r} is already registered for pattern factory:{!r}.".format( name=self.name, pattern_factory=self.pattern_factory, ) @property def name(self): """ *name* (``str``) is the name of the registered pattern. """ return self.args[0] @property def pattern_factory(self): """ *pattern_factory* (``callable``) is the registered pattern factory. """ return self.args[1] class RecursionError(Exception): """ The ``RecursionError`` exception is raised when recursion is detected. """ def __init__(self, real_path, first_path, second_path): """ Initializes the ``RecursionError`` instance. *real_path* (``str``) is the real path that recursion was encountered on. *first_path* (``str``) is the first path encountered for *real_path*. *second_path* (``str``) is the second path encountered for *real_path*. """ super(RecursionError, self).__init__(real_path, first_path, second_path) @property def first_path(self): """ *first_path* (``str``) is the first path encountered for *real_path*. """ return self.args[1] @property def message(self): """ *message* (``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): """ *real_path* (``str``) is the real path that recursion was encountered on. """ return self.args[0] @property def second_path(self): """ *second_path* (``str``) is the second path encountered for *real_path*. """ return self.args[2] pathspec-0.3.4/pathspec/compat.py0000644000175000017500000000065212566116100017130 0ustar calebcaleb00000000000000# 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. string_types = (basestring,) def viewkeys(mapping): return mapping.viewkeys() else: # Python 3. string_types = (str,) def viewkeys(mapping): return mapping.keys() pathspec-0.3.4/pathspec/tests/0000755000175000017500000000000012566612333016443 5ustar calebcaleb00000000000000pathspec-0.3.4/pathspec/tests/test_util.py0000644000175000017500000001250412566501663021036 0ustar calebcaleb00000000000000# encoding: utf-8 """ This script tests utility functions. """ import os import os.path import shutil import sys import tempfile import unittest from pathspec.util import iter_tree, RecursionError class IterTreeTest(unittest.TestCase): """ The ``IterTreeTest`` class tests `pathspec.util.iter_tree()`. """ 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(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. """ 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. """ 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'), ]) results = set(iter_tree(self.temp_dir)) self.assertEqual(results, set(map(self.ospath, { 'a', 'ax', 'b', 'bx', 'Dir/c', 'Dir/cx', 'Dir/d', 'Dir/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(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(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(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]) pathspec-0.3.4/pathspec/tests/test_gitignore.py0000644000175000017500000001171212566176235022053 0ustar calebcaleb00000000000000# encoding: utf-8 """ This script tests ``GitIgnorePattern``. """ import unittest import pathspec.util from pathspec import GitIgnorePattern class GitIgnoreTest(unittest.TestCase): """ The ``GitIgnoreTest`` class tests the ``GitIgnorePattern`` implementation. """ def test_00_empty(self): """ Tests an empty pattern. """ spec = GitIgnorePattern('') self.assertIsNone(spec.include) self.assertIsNone(spec.regex) 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)). """ spec = GitIgnorePattern('/') self.assertIsNone(spec.include) self.assertIsNone(spec.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 """ spec = GitIgnorePattern('/an/absolute/file/path') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^an/absolute/file/path(?:/.*)?$') def test_01_relative(self): """ Tests a relative path pattern. This should match: spam spam/ foo/spam spam/foo foo/spam/bar """ spec = GitIgnorePattern('spam') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^(?:.+/)?spam(?:/.*)?$') 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 """ spec = GitIgnorePattern('foo/spam') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^foo/spam(?:/.*)?$') def test_02_comment(self): """ Tests a comment pattern. """ spec = GitIgnorePattern('# Cork soakers.') self.assertIsNone(spec.include) self.assertIsNone(spec.regex) def test_02_ignore(self): """ Tests an exclude pattern. This should NOT match (according to git check-ignore (v2.4.1)): temp/foo """ spec = GitIgnorePattern('!temp') self.assertIsNotNone(spec.include) self.assertFalse(spec.include) self.assertEqual(spec.regex.pattern, '^(?:.+/)?temp$') 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 """ spec = GitIgnorePattern('spam/**') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^spam/.*$') def test_03_inner_double_asterisk(self): """ Tests a path with an inner double-asterisk directory. This should match: 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 """ spec = GitIgnorePattern('left/**/right') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^left(?:/.+)?/right(?:/.*)?$') def test_03_only_double_asterisk(self): """ Tests a double-asterisk pattern which matches everything. """ spec = GitIgnorePattern('**') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^.+$') def test_03_parent_double_asterisk(self): """ Tests a file name with a double-asterisk parent directory. This should match: foo/spam foo/spam/bar """ spec = GitIgnorePattern('**/spam') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^(?:.+/)?spam(?:/.*)?$') 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 """ spec = GitIgnorePattern('foo-*-bar') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^(?:.+/)?foo\\-[^/]*\\-bar(?:/.*)?$') 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 """ spec = GitIgnorePattern('~temp-*') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^(?:.+/)?\\~temp\\-[^/]*(?:/.*)?$') 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 """ spec = GitIgnorePattern('*.py') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^(?:.+/)?[^/]*\\.py(?:/.*)?$') def test_05_directory(self): """ Tests a directory pattern. This should match: dir/ foo/dir/ foo/dir/bar This should **not** match: dir """ spec = GitIgnorePattern('dir/') self.assertTrue(spec.include) self.assertEqual(spec.regex.pattern, '^(?:.+/)?dir/.*$') def test_05_registered(self): """ Tests that the pattern is registered. """ self.assertIs(pathspec.util.lookup_pattern('gitignore'), GitIgnorePattern) if __name__ == '__main__': suite = unittest.TestLoader().loadTestsFromTestCase(GitIgnoreTest) unittest.TextTestRunner(verbosity=2).run(suite) pathspec-0.3.4/pathspec/tests/__init__.py0000644000175000017500000000000012566173361020545 0ustar calebcaleb00000000000000pathspec-0.3.4/pathspec/tests/test_pathspec.py0000644000175000017500000000132012566175766021675 0ustar calebcaleb00000000000000# 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_windows_paths(self): """ Tests that Windows paths will be properly normalized and matched. """ spec = pathspec.PathSpec.from_lines('gitignore', [ '*.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', }) pathspec-0.3.4/pathspec/pathspec.py0000644000175000017500000000555012427141764017470 0ustar calebcaleb00000000000000# encoding: utf-8 """ This module provides an object oriented interface for pattern matching of files. """ import collections from . import util from .compat import string_types, viewkeys class PathSpec(object): """ The ``PathSpec`` instance is a wrapper around a list of compiled ``pathspec.Pattern`` instances. """ def __init__(self, patterns): """ Initializes the ``PathSpec`` instance. *patterns* (``Container`` or ``Iterable``) yields each compiled pattern (``pathspec.Pattern``). """ self.patterns = None """ *patterns* (``Container``) contains the compiled patterns, """ self.patterns = patterns if isinstance(patterns, collections.Container) else list(patterns) def __len__(self): """ Returns the number of compiled patterns this path-spec contains (``int``). """ return len(self.patterns) @classmethod def from_lines(cls, pattern_factory, lines): """ Compiles the pattern lines. *pattern_factory* can be either the name of a registered pattern factory (``str``), or a ``callable`` used to compile patterns. It must accept an uncompiled pattern (``str``) and return the compiled pattern (``pathspec.Pattern``). *lines* (``Iterable``) yields each uncompiled pattern (``str``). This simply has to yield each line so it can be a ``file`` (e.g., ``open(file)`` or ``io.StringIO(text)``) or the result from ``str.splitlines()``. Returns the ``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)) lines = [pattern_factory(line) for line in lines if line] return cls(lines) def match_files(self, files, separators=None): """ Matches the files to this path-spec. *files* (``Iterable`` of ``str``) contains the files to be matched against *patterns*. *separators* (``Container`` of ``str``) optionally contains the path separators to normalize. This does not need to include the POSIX path separator (`/`), but including it will not affect the results. Default is ``None`` to determine the separators based upon the current operating system by examining `os.sep` and `os.altsep`. To prevent normalization, pass an empty container (e.g., an empty tuple `()`). Returns the matched files (``Iterable`` of ``str``). """ file_map = util.normalize_files(files, separators=separators) matched_files = util.match_files(self.patterns, viewkeys(file_map)) for path in matched_files: yield file_map[path] def match_tree(self, root): """ Walks the specified root path for all files and matches them to this path-spec. *root* (``str``) is the root directory to search for files. Returns the matched files (``Iterable`` of ``str``). """ files = util.iter_tree(root) return self.match_files(files) pathspec-0.3.4/pathspec/__init__.py0000644000175000017500000000175012566612123017412 0ustar calebcaleb00000000000000# encoding: utf-8 """ The *pathspec* package provides pattern matching for file paths. So far this only includes gitignore style pattern matching. See "README.rst" or for more information. Or you can always scour the source code. """ from __future__ import unicode_literals __author__ = "Caleb P. Burns" __copyright__ = "Copyright © 2013-2015 Caleb P. Burns" __created__ = "2013-10-12" __credits__ = [ "dahlia ", "highb ", "029xue ", "mikexstudios ", "nhumrich ", ] __email__ = "cpburnz@gmail.com" __license__ = "MPL 2.0" __project__ = "pathspec" __status__ = "Development" __updated__ = "2015-08-24" __version__ = "0.3.4" from .gitignore import GitIgnorePattern from .pathspec import PathSpec from .pattern import Pattern, RegexPattern from .util import iter_tree, match_files, RecursionError pathspec-0.3.4/pathspec/pattern.py0000644000175000017500000000443512427141770017334 0ustar calebcaleb00000000000000# encoding: utf-8 """ This module provides the base definition for patterns. """ import re from .compat import string_types class Pattern(object): """ The ``Pattern`` class is the abstract definition of a pattern. """ # Make the class dict-less. __slots__ = ('include',) def __init__(self, include): """ Initializes the ``Pattern`` instance. *include* (``bool``) is whether the matched files should be included (``True``), excluded (``False``), or is a null-operation (``None``). """ self.include = include """ *include* (``bool``) is whether the matched files should be included (``True``), excluded (``False``), or is a null-operation (``None``). """ def match(self, files): """ Matches this pattern against the specified files. *files* (``Iterable``) contains each file (``str``) relative to the root directory (e.g., "relative/path/to/file"). Returns an ``Iterable`` yielding each matched file path (``str``). """ raise NotImplementedError("{}.{} must override match().".format(self.__class__.__module__, self.__class__.__name__)) class RegexPattern(Pattern): """ The ``RegexPattern`` class is an implementation of a pattern using regular expressions. """ # Make the class dict-less. __slots__ = ('regex',) def __init__(self, regex, *args, **kw): """ Initializes the ``RegexPattern`` instance. *regex* (``RegexObject`` or ``str``) is the regular expression for the pattern. `*args` are positional arguments to send to the ``Pattern`` constructor. `**kw` are keyword arguments to send to the ``Pattern`` constructor. """ self.regex = None """ *regex* (``RegexObject``) is the regular expression for the pattern. """ # NOTE: Make sure to allow a null regex pattern to be passed for a # null-operation. if isinstance(regex, string_types): regex = re.compile(regex) super(RegexPattern, self).__init__(*args, **kw) self.regex = regex def match(self, files): """ Matches this pattern against the specified files. *files* (``Iterable``) contains each file (``str``) relative to the root directory (e.g., "relative/path/to/file"). Returns an ``Iterable`` yielding each matched file path (``str``). """ if self.include is not None: for path in files: if self.regex.match(path) is not None: yield path