summaryrefslogtreecommitdiffstats
path: root/third_party/python/pathspec
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/pathspec')
-rw-r--r--third_party/python/pathspec/CHANGES.rst202
-rw-r--r--third_party/python/pathspec/LICENSE373
-rw-r--r--third_party/python/pathspec/MANIFEST.in2
-rw-r--r--third_party/python/pathspec/PKG-INFO380
-rw-r--r--third_party/python/pathspec/README.rst153
-rw-r--r--third_party/python/pathspec/pathspec/__init__.py68
-rw-r--r--third_party/python/pathspec/pathspec/compat.py38
-rw-r--r--third_party/python/pathspec/pathspec/pathspec.py185
-rw-r--r--third_party/python/pathspec/pathspec/pattern.py146
-rw-r--r--third_party/python/pathspec/pathspec/patterns/__init__.py8
-rw-r--r--third_party/python/pathspec/pathspec/patterns/gitwildmatch.py330
-rw-r--r--third_party/python/pathspec/pathspec/tests/__init__.py0
-rw-r--r--third_party/python/pathspec/pathspec/tests/test_gitwildmatch.py474
-rw-r--r--third_party/python/pathspec/pathspec/tests/test_pathspec.py129
-rw-r--r--third_party/python/pathspec/pathspec/tests/test_util.py380
-rw-r--r--third_party/python/pathspec/pathspec/util.py600
-rw-r--r--third_party/python/pathspec/setup.cfg7
-rw-r--r--third_party/python/pathspec/setup.py44
18 files changed, 3519 insertions, 0 deletions
diff --git a/third_party/python/pathspec/CHANGES.rst b/third_party/python/pathspec/CHANGES.rst
new file mode 100644
index 0000000000..c92a56f537
--- /dev/null
+++ b/third_party/python/pathspec/CHANGES.rst
@@ -0,0 +1,202 @@
+
+Change History
+==============
+
+
+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.
+- 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
+
+
+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.
diff --git a/third_party/python/pathspec/LICENSE b/third_party/python/pathspec/LICENSE
new file mode 100644
index 0000000000..14e2f777f6
--- /dev/null
+++ b/third_party/python/pathspec/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/third_party/python/pathspec/MANIFEST.in b/third_party/python/pathspec/MANIFEST.in
new file mode 100644
index 0000000000..9173153f57
--- /dev/null
+++ b/third_party/python/pathspec/MANIFEST.in
@@ -0,0 +1,2 @@
+include *.rst
+include LICENSE
diff --git a/third_party/python/pathspec/PKG-INFO b/third_party/python/pathspec/PKG-INFO
new file mode 100644
index 0000000000..6070abfe03
--- /dev/null
+++ b/third_party/python/pathspec/PKG-INFO
@@ -0,0 +1,380 @@
+Metadata-Version: 1.2
+Name: pathspec
+Version: 0.8.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.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.
+ - 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
+
+
+ 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 :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Utilities
+Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
diff --git a/third_party/python/pathspec/README.rst b/third_party/python/pathspec/README.rst
new file mode 100644
index 0000000000..e8ac70a14a
--- /dev/null
+++ b/third_party/python/pathspec/README.rst
@@ -0,0 +1,153 @@
+
+*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
diff --git a/third_party/python/pathspec/pathspec/__init__.py b/third_party/python/pathspec/pathspec/__init__.py
new file mode 100644
index 0000000000..2400402197
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/__init__.py
@@ -0,0 +1,68 @@
+# 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
+
+__author__ = "Caleb P. Burns"
+__copyright__ = "Copyright © 2013-2020 Caleb P. Burns"
+__created__ = "2013-10-12"
+__credits__ = [
+ "dahlia <https://github.com/dahlia>",
+ "highb <https://github.com/highb>",
+ "029xue <https://github.com/029xue>",
+ "mikexstudios <https://github.com/mikexstudios>",
+ "nhumrich <https://github.com/nhumrich>",
+ "davidfraser <https://github.com/davidfraser>",
+ "demurgos <https://github.com/demurgos>",
+ "ghickman <https://github.com/ghickman>",
+ "nvie <https://github.com/nvie>",
+ "adrienverge <https://github.com/adrienverge>",
+ "AndersBlomdell <https://github.com/AndersBlomdell>",
+ "highb <https://github.com/highb>",
+ "thmxv <https://github.com/thmxv>",
+ "wimglenn <https://github.com/wimglenn>",
+ "hugovk <https://github.com/hugovk>",
+ "dcecile <https://github.com/dcecile>",
+ "mroutis <https://github.com/mroutis>",
+ "jdufresne <https://github.com/jdufresne>",
+ "groodt <https://github.com/groodt>",
+ "ftrofin <https://github.com/ftrofin>",
+ "pykong <https://github.com/pykong>",
+]
+__email__ = "cpburnz@gmail.com"
+__license__ = "MPL 2.0"
+__project__ = "pathspec"
+__status__ = "Development"
+__updated__ = "2020-04-09"
+__version__ = "0.8.0"
+
+from .pathspec import PathSpec
+from .pattern import Pattern, RegexPattern
+from .util import iter_tree, lookup_pattern, match_files, RecursionError
+
+# Load pattern implementations.
+from . import patterns
+
+# Expose `GitIgnorePattern` class in the root module for backward
+# compatibility with v0.4.
+from .patterns.gitwildmatch import GitIgnorePattern
diff --git a/third_party/python/pathspec/pathspec/compat.py b/third_party/python/pathspec/pathspec/compat.py
new file mode 100644
index 0000000000..37c6480510
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/compat.py
@@ -0,0 +1,38 @@
+# 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
diff --git a/third_party/python/pathspec/pathspec/pathspec.py b/third_party/python/pathspec/pathspec/pathspec.py
new file mode 100644
index 0000000000..be058ffc87
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/pathspec.py
@@ -0,0 +1,185 @@
+# encoding: utf-8
+"""
+This module provides an object oriented interface for pattern matching
+of files.
+"""
+
+from . import util
+from .compat import Collection, iterkeys, izip_longest, string_types, unicode
+
+
+class PathSpec(object):
+ """
+ The :class:`PathSpec` class is a wrapper around a list of compiled
+ :class:`.Pattern` instances.
+ """
+
+ def __init__(self, patterns):
+ """
+ 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, Collection) else list(patterns)
+ """
+ *patterns* (:class:`~collections.abc.Collection` of :class:`.Pattern`)
+ contains the compiled patterns.
+ """
+
+ def __eq__(self, other):
+ """
+ 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)
+
+ @classmethod
+ def from_lines(cls, pattern_factory, lines):
+ """
+ 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))
+
+ lines = [pattern_factory(line) for line in lines if line]
+ return cls(lines)
+
+ def match_file(self, file, separators=None):
+ """
+ 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 <PathSpec.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):
+ """
+ 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 <PathSpec.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.Iterable` 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):
+ """
+ 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 <PathSpec.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.Iterable` of
+ :class:`str`).
+ """
+ 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 path in matched_files:
+ yield file_map[path]
+
+ def match_tree_entries(self, root, on_error=None, follow_links=None):
+ """
+ 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.Iterable` of
+ :class:`str`).
+ """
+ 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):
+ """
+ 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
diff --git a/third_party/python/pathspec/pathspec/pattern.py b/third_party/python/pathspec/pathspec/pattern.py
new file mode 100644
index 0000000000..4ba4edf790
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/pattern.py
@@ -0,0 +1,146 @@
+# encoding: utf-8
+"""
+This module provides the base definition for patterns.
+"""
+
+import re
+
+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):
+ """
+ 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):
+ """
+ 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):
+ """
+ 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):
+ """
+ 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):
+ """
+ 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):
+ """
+ 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
diff --git a/third_party/python/pathspec/pathspec/patterns/__init__.py b/third_party/python/pathspec/pathspec/patterns/__init__.py
new file mode 100644
index 0000000000..1a0d55ec74
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/patterns/__init__.py
@@ -0,0 +1,8 @@
+# encoding: utf-8
+"""
+The *pathspec.patterns* package contains the pattern matching
+implementations.
+"""
+
+# Load pattern implementations.
+from .gitwildmatch import GitWildMatchPattern
diff --git a/third_party/python/pathspec/pathspec/patterns/gitwildmatch.py b/third_party/python/pathspec/pathspec/patterns/gitwildmatch.py
new file mode 100644
index 0000000000..07fd03880a
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/patterns/gitwildmatch.py
@@ -0,0 +1,330 @@
+# 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
+
+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 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):
+ """
+ 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))
+
+ 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.
+ 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:
+ # 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):
+ """
+ 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 == '[':
+ # 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
+
+ @staticmethod
+ def escape(s):
+ """
+ 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`, :class:`bytes`)
+ """
+ # Reference: https://git-scm.com/docs/gitignore#_pattern_format
+ meta_characters = r"[]!*#?"
+
+ return "".join("\\" + x if x in meta_characters else x for x in s)
+
+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()
+ return 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)
diff --git a/third_party/python/pathspec/pathspec/tests/__init__.py b/third_party/python/pathspec/pathspec/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/tests/__init__.py
diff --git a/third_party/python/pathspec/pathspec/tests/test_gitwildmatch.py b/third_party/python/pathspec/pathspec/tests/test_gitwildmatch.py
new file mode 100644
index 0000000000..e552d5ef53
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/tests/test_gitwildmatch.py
@@ -0,0 +1,474 @@
+# 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
+
+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/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/bar/right',
+ 'left/foo/bar/right',
+ 'left/bar/right/foo',
+ 'foo/left/bar/right',
+ ]))
+ self.assertEqual(results, {
+ '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:
+
+ 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([
+ 'foo/spam',
+ 'foo/spam/bar',
+ ]))
+ self.assertEqual(results, {
+ 'foo/spam',
+ 'foo/spam/bar',
+ })
+
+ 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)
diff --git a/third_party/python/pathspec/pathspec/tests/test_pathspec.py b/third_party/python/pathspec/pathspec/tests/test_pathspec.py
new file mode 100644
index 0000000000..1f5bb8ba11
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/tests/test_pathspec.py
@@ -0,0 +1,129 @@
+# 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_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)
diff --git a/third_party/python/pathspec/pathspec/tests/test_util.py b/third_party/python/pathspec/pathspec/tests/test_util.py
new file mode 100644
index 0000000000..943bde259c
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/tests/test_util.py
@@ -0,0 +1,380 @@
+# 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 <https://bugs.python.org/issue9949>
+ # 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)
diff --git a/third_party/python/pathspec/pathspec/util.py b/third_party/python/pathspec/pathspec/util.py
new file mode 100644
index 0000000000..bcba8783b6
--- /dev/null
+++ b/third_party/python/pathspec/pathspec/util.py
@@ -0,0 +1,600 @@
+# encoding: utf-8
+"""
+This module provides utility methods for dealing with path-specs.
+"""
+
+import os
+import os.path
+import posixpath
+import stat
+
+from .compat import Collection, Iterable, string_types, unicode
+
+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):
+ """
+ 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, Collection) 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):
+ """
+ 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, Iterable) and not isinstance(value, (unicode, bytes))
+
+
+def iter_tree_entries(root, on_error=None, follow_links=None):
+ """
+ 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.Iterable` 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):
+ """
+ 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.Iterable` 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):
+ """
+ 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 <https://github.com/cpburnz/python-path-specification/pull/7>.
+ del memo[dir_real]
+
+
+def lookup_pattern(name):
+ """
+ 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):
+ """
+ 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):
+ """
+ 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, Collection) 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):
+ """
+ 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):
+ """
+ Normalizes the file path to use the POSIX path separator (i.e., ``'/'``).
+
+ *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)
+
+ # Remove current directory prefix.
+ if norm_file.startswith('./'):
+ norm_file = norm_file[2:]
+
+ return norm_file
+
+
+def normalize_files(files, separators=None):
+ """
+ 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 path (:class:`str`)
+ """
+ norm_files = {}
+ for path in files:
+ norm_files[normalize_file(path, separators=separators)] = path
+ return norm_files
+
+
+def register_pattern(name, pattern_factory, override=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):
+ """
+ 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):
+ """
+ *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):
+ """
+ *name* (:class:`str`) is the name of the registered pattern.
+ """
+ return self.args[0]
+
+ @property
+ def pattern_factory(self):
+ """
+ *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):
+ """
+ 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):
+ """
+ *first_path* (:class:`str`) is the first path encountered for
+ :attr:`self.real_path <RecursionError.real_path>`.
+ """
+ return self.args[1]
+
+ @property
+ def message(self):
+ """
+ *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):
+ """
+ *real_path* (:class:`str`) is the real path that recursion was
+ encountered on.
+ """
+ return self.args[0]
+
+ @property
+ def second_path(self):
+ """
+ *second_path* (:class:`str`) is the second path encountered for
+ :attr:`self.real_path <RecursionError.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):
+ """
+ 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):
+ """
+ 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):
+ """
+ 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):
+ """
+ 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):
+ """
+ Returns whether the entry is a symbolic link (:class:`bool`).
+ """
+ return stat.S_ISLNK(self._lstat.st_mode)
+
+ def stat(self, follow_links=None):
+ """
+ Get the cached stat result for the entry.
+
+ *follow_links* (:class:`bool` or :data:`None`) is whether to follow
+ symbolic links. If this is :data:`True`, the stat result of the
+ linked file will be returned. Default is :data:`None` for :data:`True`.
+
+ Returns that stat result (:class:`~os.stat_result`).
+ """
+ if follow_links is None:
+ follow_links = True
+
+ return self._stat if follow_links else self._lstat
diff --git a/third_party/python/pathspec/setup.cfg b/third_party/python/pathspec/setup.cfg
new file mode 100644
index 0000000000..adf5ed72aa
--- /dev/null
+++ b/third_party/python/pathspec/setup.cfg
@@ -0,0 +1,7 @@
+[bdist_wheel]
+universal = 1
+
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/third_party/python/pathspec/setup.py b/third_party/python/pathspec/setup.py
new file mode 100644
index 0000000000..130d416166
--- /dev/null
+++ b/third_party/python/pathspec/setup.py
@@ -0,0 +1,44 @@
+# 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,
+ python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
+ 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 :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: Utilities",
+ ],
+ license=__license__,
+ packages=find_packages(),
+ test_suite='pathspec.tests',
+)