pax_global_header00006660000000000000000000000064146753241350014524gustar00rootroot0000000000000052 comment=0be4cfb81792c6f8295bcb7d393dc0e786967563 wcmatch-10.0/000077500000000000000000000000001467532413500130725ustar00rootroot00000000000000wcmatch-10.0/.codecov.yml000066400000000000000000000000651467532413500153160ustar00rootroot00000000000000comment: false coverage: status: patch: false wcmatch-10.0/.coveragerc000066400000000000000000000001731467532413500152140ustar00rootroot00000000000000[run] omit= wcmatch/pep562.py [report] omit= wcmatch/pep562.py exclude_lines = pragma: no cover @overload wcmatch-10.0/.github/000077500000000000000000000000001467532413500144325ustar00rootroot00000000000000wcmatch-10.0/.github/FUNDING.yml000066400000000000000000000001061467532413500162440ustar00rootroot00000000000000github: facelessuser custom: - "https://www.paypal.me/facelessuser" wcmatch-10.0/.github/labels.yml000066400000000000000000000024201467532413500164150ustar00rootroot00000000000000template: 'facelessuser:master-labels:labels.yml:master' # Wildcard labels brace_expansion: true extended_glob: true minus_negate: false rules: - labels: ['C: infrastructure'] patterns: ['*|{tools,requirements,.github}/**|!*.md'] - labels: ['C: source'] patterns: ['wcmatch/**'] - labels: ['C: tests'] patterns: ['tests/**'] - labels: ['C: docs'] patterns: ['docs/**|*.md'] - labels: ['C: glob'] patterns: ['**/?(test_)glob*|!docs/**'] - labels: ['C: fnmatch'] patterns: ['**/?(test_)fnmatch*|!docs/**'] - labels: ['C: wcmatch'] patterns: ['**/?(test_)wcmatch*|!docs/**'] - labels: ['C: pathlib'] patterns: ['**/?(test_)pathlib*|!docs/**'] - labels: ['C: pattern-parser'] patterns: ['**/?(test_|_)wcparse*|!docs/**'] # Label management labels: - name: 'C: glob' renamed: glob color: subcategory description: Glob library. - name: 'C: fnmatch' renamed: fnmatch color: subcategory description: Fnmatch library. - name: 'C: wcmatch' renamed: wcmatch color: subcategory description: Wcmatch library. - name: 'C: pathlib' renamed: pathlib color: subcategory description: Pathlib library. - name: 'C: pattern-parser' renamed: pattern-parser color: subcategory description: Related to pattern parsing. wcmatch-10.0/.github/workflows/000077500000000000000000000000001467532413500164675ustar00rootroot00000000000000wcmatch-10.0/.github/workflows/build.yml000066400000000000000000000051751467532413500203210ustar00rootroot00000000000000name: build on: push: branches: - 'main' tags: - '**' pull_request: branches: - '**' jobs: tests: strategy: fail-fast: false max-parallel: 4 matrix: platform: [ubuntu-latest, windows-latest] python-version: [3.8, 3.9, '3.10', '3.11', '3.12', '3.13'] include: - python-version: 3.8 tox-env: py38 - python-version: 3.9 tox-env: py39 - python-version: '3.10' tox-env: py310 - python-version: '3.11' tox-env: py310 - python-version: '3.12' tox-env: py312 - python-version: '3.13' tox-env: py313 exclude: - platform: windows-latest python-version: '3.13' env: TOXENV: ${{ matrix.tox-env }} runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Install dependencies run: | python -m pip install --upgrade pip setuptools tox coverage - name: Test run: | python -m tox - name: Upload Results if: success() uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: unittests name: ${{ matrix.platform }}-${{ matrix.tox-env }} token: ${{ secrets.CODECOV_TOKEN }} # required fail_ci_if_error: false lint: strategy: max-parallel: 4 matrix: python-version: [3.11] env: TOXENV: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip setuptools tox - name: Lint run: | python -m tox documents: strategy: max-parallel: 4 matrix: python-version: [3.11] env: TOXENV: documents runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip setuptools tox - name: Install Aspell run: | sudo apt-get install aspell aspell-en - name: Build documents run: | python -m tox wcmatch-10.0/.github/workflows/deploy.yml000066400000000000000000000026111467532413500205060ustar00rootroot00000000000000name: deploy on: push: tags: - '*' jobs: documents: strategy: max-parallel: 4 matrix: python-version: [3.11] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip setuptools python -m pip install -r requirements/docs.txt - name: Deploy documents run: | git config user.name facelessuser git config user.email "${{ secrets.GH_EMAIL }}" git remote add gh-token "https://${{ secrets.GH_TOKEN }}@github.com/facelessuser/wcmatch.git" git fetch gh-token && git fetch gh-token gh-pages:gh-pages python -m mkdocs gh-deploy -v --clean --remote-name gh-token git push gh-token gh-pages pypi: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.11 - name: Package run: | pip install --upgrade wheel build python -m build -s -w - name: Publish uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_PWD }} wcmatch-10.0/.gitignore000066400000000000000000000023201467532413500150570ustar00rootroot00000000000000.DS_STORE # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # pytest .pytest_cache/ # patch *.patch docs/src/dictionary/hunspell wcmatch-10.0/.pyspelling.yml000066400000000000000000000042221467532413500160610ustar00rootroot00000000000000matrix: - name: mkdocs sources: - site/**/*.html hunspell: d: docs/src/dictionary/hunspell/en_US aspell: lang: en dictionary: wordlists: - docs/src/dictionary/en-custom.txt output: build/dictionary/mkdocs.dic pipeline: - pyspelling.filters.html: comments: false attributes: - title - alt ignores: - 'code, pre, a.magiclink, span.keys' - '.MathJax_Preview, .md-nav__link, .md-footer-custom-text, .md-source__repository, .headerlink, .md-icon' - '.md-social__link' - pyspelling.filters.url: - name: markdown sources: - README.md hunspell: d: docs/src/dictionary/hunspell/en_US aspell: lang: en dictionary: wordlists: - docs/src/dictionary/en-custom.txt output: build/dictionary/mkdocs.dic pipeline: - pyspelling.filters.markdown: - pyspelling.filters.html: comments: false attributes: - title - alt ignores: - code - pre - pyspelling.filters.url: - name: python sources: - setup.py - "{wcmatch,tests,tools}/**/*.py" hunspell: d: docs/src/dictionary/hunspell/en_US aspell: lang: en dictionary: wordlists: - docs/src/dictionary/en-custom.txt output: build/dictionary/python.dic pipeline: - pyspelling.filters.python: group_comments: True - pyspelling.flow_control.wildcard: allow: - py-comment - pyspelling.filters.context: context_visible_first: true delimiters: # Ignore lint (noqa) and coverage (pragma) as well as shebang (#!) - open: '^(?: *(?:noqa\b|pragma: no cover|type: .*?)|!)' close: '$' # Ignore Python encoding string -*- encoding stuff -*- - open: '^ *-\*-' close: '-\*-$' - pyspelling.filters.context: context_visible_first: true escapes: '\\[\\`]' delimiters: # Ignore multiline content between fences (fences can have 3 or more back ticks) # ``` # content # ``` - open: '(?s)^(?P *`{3,})$' close: '^(?P=open)$' # Ignore text between inline back ticks - open: '(?P`+)' close: '(?P=open)' - pyspelling.filters.url: wcmatch-10.0/LICENSE.md000066400000000000000000000020621467532413500144760ustar00rootroot00000000000000MIT License Copyright (c) 2018 - 2024 Isaac Muse Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wcmatch-10.0/README.md000066400000000000000000000075201467532413500143550ustar00rootroot00000000000000[![Donate via PayPal][donate-image]][donate-link] [![Build][github-ci-image]][github-ci-link] [![Coverage Status][codecov-image]][codecov-link] [![PyPI Version][pypi-image]][pypi-link] [![PyPI Downloads][pypi-down]][pypi-link] [![PyPI - Python Version][python-image]][pypi-link] ![License][license-image-mit] # Wildcard Match ## Overview Wildcard Match provides an enhanced `fnmatch`, `glob`, and `pathlib` library in order to provide file matching and globbing that more closely follows the features found in Bash. In some ways these libraries are similar to Python's builtin libraries as they provide a similar interface to match, filter, and glob the file system. But they also include a number of features found in Bash's globbing such as backslash escaping, brace expansion, extended glob pattern groups, etc. They also add a number of new useful functions as well, such as `globmatch` which functions like `fnmatch`, but for paths. Wildcard Match also adds a file search utility called `wcmatch` that is built on top of `fnmatch` and `globmatch`. It was originally written for [Rummage](https://github.com/facelessuser/Rummage), but split out into this project to be used by other projects that may find its approach useful. Bash is used as a guide when making decisions on behavior for `fnmatch` and `glob`. Behavior may differ from Bash version to Bash version, but an attempt is made to keep Wildcard Match up with the latest relevant changes. With all of this said, there may be a few corner cases in which we've intentionally chosen to not *exactly* mirror Bash. If an issue is found where Wildcard Match seems to deviate in an illogical way, we'd love to hear about it in the [issue tracker](https://github.com/facelessuser/wcmatch/issues). ## Features A quick overview of Wildcard Match's Features: - Provides an interface comparable to Python's builtin in `fnmatch`, `glob`, and `pathlib`. - Allows for a much more configurable experience when matching or globbing with many more features. - Adds support for `**` in glob. - Adds support for Zsh style `***` recursive glob for symlinks. - Adds support for escaping characters with `\`. - Add support for POSIX style character classes inside sequences: `[[:alnum:]]`, etc. The `C` locale is used. - Adds support for brace expansion: `a{b,{c,d}}` --> `ab ac ad`. - Adds support for expanding `~` or `~username` to the appropriate user path. - Adds support for extended match patterns: `@(...)`, `+(...)`, `*(...)`, `?(...)`, and `!(...)`. - Adds ability to match path names via the path centric `globmatch`. - Provides a `pathlib` variant that uses Wildcard Match's `glob` library instead of Python's default. - Provides an alternative file crawler called `wcmatch`. - And more... ## Installation Installation is easy with pip: ``` pip install wcmatch ``` ## Documentation https://facelessuser.github.io/wcmatch/ ## License MIT [github-ci-image]: https://github.com/facelessuser/wcmatch/workflows/build/badge.svg?branch=main&event=push [github-ci-link]: https://github.com/facelessuser/wcmatch/actions?query=workflow%3Abuild+branch%3Amain [codecov-image]: https://img.shields.io/codecov/c/github/facelessuser/wcmatch/main.svg?logo=codecov&logoColor=aaaaaa&labelColor=333333 [codecov-link]: https://codecov.io/github/facelessuser/wcmatch [pypi-image]: https://img.shields.io/pypi/v/wcmatch.svg?logo=pypi&logoColor=aaaaaa&labelColor=333333 [pypi-down]: https://img.shields.io/pypi/dm/wcmatch.svg?logo=pypi&logoColor=aaaaaa&labelColor=333333 [pypi-link]: https://pypi.python.org/pypi/wcmatch [python-image]: https://img.shields.io/pypi/pyversions/wcmatch?logo=python&logoColor=aaaaaa&labelColor=333333 [license-image-mit]: https://img.shields.io/badge/license-MIT-blue.svg?labelColor=333333 [donate-image]: https://img.shields.io/badge/Donate-PayPal-3fabd1?logo=paypal [donate-link]: https://www.paypal.me/facelessuser wcmatch-10.0/docs/000077500000000000000000000000001467532413500140225ustar00rootroot00000000000000wcmatch-10.0/docs/src/000077500000000000000000000000001467532413500146115ustar00rootroot00000000000000wcmatch-10.0/docs/src/dictionary/000077500000000000000000000000001467532413500167565ustar00rootroot00000000000000wcmatch-10.0/docs/src/dictionary/en-custom.txt000066400000000000000000000014251467532413500214330ustar00rootroot00000000000000API Accessors Backport Changelog EXTGLOB EXTMATCH EmojiOne Fnmatch GUIDs GitHub Gitter Glob Lookahead MERCHANTABILITY MacOS MkDocs NONINFRINGEMENT POSIX Pathlib Preprocess PyPI Setuptools Symlink TODO Twemoji UNC Wcmatch Wildcard XORed Zsh accessor amongst backend basename boolean builtin centric configurability deMorgan deduping deprecations dev falsy filename filenames filepath filesystem glob globbing initializer lookahead lookaheads lookbehinds macOS matcher matchers paren pathlike posix pre prepend prepends preprocessing preprocessors prerelease prereleases recurse recursing regex sharepoint sharepoints sortable subclass subclassed subclasses subdirectories sublicense subpatterns symlink symlinked symlinks syntaxes tuple un unclosed unintuitive unordered versa wildcard zsh wcmatch-10.0/docs/src/markdown/000077500000000000000000000000001467532413500164335ustar00rootroot00000000000000wcmatch-10.0/docs/src/markdown/.snippets/000077500000000000000000000000001467532413500203565ustar00rootroot00000000000000wcmatch-10.0/docs/src/markdown/.snippets/abbr.md000066400000000000000000000000001467532413500215740ustar00rootroot00000000000000wcmatch-10.0/docs/src/markdown/.snippets/links.md000066400000000000000000000004721467532413500220230ustar00rootroot00000000000000[issues]: https://github.com/facelessuser/wcmatch/issues [pathlib]: https://docs.python.org/3/library/pathlib.html [glob]: https://docs.python.org/3/library/glob.html [fnmatch]: https://docs.python.org/3/library/fnmatch.html [unicode-properties]: https://facelessuser.github.io/backrefs/#special-syntax-exceptions wcmatch-10.0/docs/src/markdown/.snippets/posix.md000066400000000000000000000013301467532413500220370ustar00rootroot00000000000000## POSIX Character Classes A number of POSIX style character classes are available in the form `[:alnum:]`. They must be used inside sequences: `[[:digit:]]`. The `C` locale is used, and the values for each character class are found in the table below. Property | Pattern ---------- | ------------------------------------------------- `alnum` | `[a-zA-Z0-9]` `alpha` | `[a-zA-Z]` `ascii` | `[\x00-\x7F]` `blank` | `[ \t]` `cntrl` | `[\x00-\x1F\x7F]` `digit` | `[0-9]` `graph` | `[\x21-\x7E]` `lower` | `[a-z]` `print` | `[\x20-\x7E]` `punct` | ``[!\"\#$%&'()*+,\-./:;<=>?@\[\\\]^_`{}~]`` `space` | `[ \t\r\n\v\f]` `upper` | `[A-Z]` `word` | `[a-zA-Z0-9_]` `xdigit` | `[A-Fa-f0-9]` wcmatch-10.0/docs/src/markdown/.snippets/refs.md000066400000000000000000000000371467532413500216370ustar00rootroot00000000000000--8<-- links.md abbr.md --8<-- wcmatch-10.0/docs/src/markdown/about/000077500000000000000000000000001467532413500175455ustar00rootroot00000000000000wcmatch-10.0/docs/src/markdown/about/changelog.md000066400000000000000000000543171467532413500220300ustar00rootroot00000000000000# Changelog ## 10.0 - **NEW**: Added `GLOBSTARLONG` which adds support for the Zsh style `***` which acts like `**` with `GLOBSTAR` but but traverses symlinks. - **NEW**: `pathlib.match` will respect symlink rules (when the `REALPATH` flag is given). Hidden file rules will be respected at all times. Enable `DOTALL` to match hidden files. - **NEW**: Symlinks should not be traversed when `GLOBSTAR` is enabled unless `FOLLOW` is also enabled, but they should still be matched. Prior to this change, symlinks were not traversed _and_ they were ignored from matching which contradicts how Bash works and could be confusing to users. - **FIX**: Fix some inconsistencies with `globmatch` and symlink handling when `REALPATH` is enabled. ## 9.0 - **NEW**: Remove deprecated function `glob.raw_escape`. - **NEW**: Officially support Python 3.13. ## 8.5.2 - **FIX**: Fix `pathlib` issue with inheritance on Python versions greater than 3.12. - **FIX**: Fix `EXTMATCH` case with `!(...)` patterns. ## 8.5.1 - **FIX**: Fix issue with type check failure in `wcmatch.glob`. ## 8.5 - **NEW**: Formally support Python 3.11 (no change). - **NEW**: Add support for Python 3.12 (`pathlib` changes). - **NEW**: Drop Python 3.7 support. - **FIX**: Fix handling of current directory when magic and non-magic patterns are mixed in `glob` pattern list. ## 8.4.1 - **FIX**: Windows drive path separators should normalize like other path separators. - **FIX**: Fix a Windows pattern parsing issue that caused absolute paths with ambiguous drives to not parse correctly. ## 8.4 - **NEW**: Drop support for Python 3.6. - **NEW**: Switch to Hatch backend instead of Setuptools. - **NEW**: Add new `exclude` option to `fnmatch`, `pathlib`, and `glob` methods that allows exclusion patterns to be specified directly without needing to enable `NEGATE` and prepend patterns with `!`. `exclude` accepts a separate pattern or pattern list. `exclude` should not be used in conjunction with `NEGATE`. One or the other should be used. ## 8.3 - **NEW**: Officially support Python 3.10. - **NEW**: Provide type hints for API. - **FIX**: Gracefully handle calls with an empty pattern list. ## 8.2 - **NEW**: Add support for `dir_fd` in glob patterns. - **FIX**: Small fix for Python 3.10 Beta 1 and `pathlib`. ## 8.1.2 - **FIX**: `fnmatch.translate` no longer requires user to normalize their Windows paths for comparison. Previously, portions of the `translate` regex handled both `/` and `\\`, while other portions did not. This inconsistent handling forced users to normalize paths for reliable matching. Now all of the generated regex should handle both `/` and `\\`. - **FIX**: On Linux/Unix systems, a backslash should not be assumed literal if it is followed by a forward slash. Backslash is magic on all systems, and an escaped forward slash is still counted as a forward slash, not a backslash and forward slash. - **FIX**: A trailing backslash that is not escaped via another backslash should not be assumed as a backslash. Since it is escaping nothing, it will be ignored. Literal backslashes on any system must be escaped. ## 8.1.1 - **FIX**: When tracking unique glob paths, the unique cache had inverted logic for case sensitive vs case insensitive comparison. (#164) ## 8.1 - **NEW**: Add `is_magic` function to the `glob` and `fnmatch` library. - **NEW**: `fnmatch` now has `escape` available via its API. The `fnmatch` variant uses filename logic instead of path logic. - **NEW**: Deprecate `raw_escape` in `glob` as it is very niche and the same can be accomplished simply by using `#!py3 codecs.decode(string, 'unicode_escape')` and then using `escape`. - **FIX**: Use `os.fspath` to convert path-like objects to string/bytes, whatever the return from `__fspath__` is what Wildcard Match will accept. Don't try to convert paths via `__str__` or `__bytes__` as not all path-like objects may implement both. - **FIX**: Better checking of types to ensure consistent failure if the path, pattern, or root directory of are not all of type `str` or `bytes`. - **FIX**: Some internal fixes and refactoring. - **FIX**: Refactor code to take advantage of `bracex`'s ability to abort parsing on extremely large pattern expansions. Patterns like `{1..10000000}` will now abort dramatically quicker. Require `bracex` 2.1.1 which aborts much quicker. - **FIX**: Fix some corner cases where limit would not abort correctly. ## 8.0.1 - **FIX**: Small bug in `[:alpha:]` range. ## 8.0 - **NEW**: `WcMatch`'s `on_init` hook now only accepts `kwargs` and not `args`. - **NEW**: Cosmetic change of referring to the first `__init__` parameter as `root_dir` instead of `base`. This is to make it more clear when we are talking about the overall root directory that all paths are relative to vs the base path of a file which is relative to the root directory and the actual file name. - **NEW**: Internal attribute of `WcMatch` changed from `base` to `_root_dir`. This attribute is not really meant to be referenced by users and as been marked as private. - **NEW**: Drop requirement for `backrefs` and update documentation to note that POSIX properties never actually enabled the use of Unicode properties. While the documentation stated this and it was probably intended, it was never actually enabled. Currently, Wildcard match has chosen to keep with the ASCII definition for now as it has been since the feature was introduced. This may change in the future if there is demand for it. - **NEW**: Add `[:word:]` POSIX character class. ## 7.2 - **NEW**: Drop Python 3.5 support. - **NEW**: Formally support Python 3.9 support. - **FIX**: Small fix for regular expression output to ensure `NODIR` pattern looks at both `/` and `\\` on Windows. ## 7.1 - **NEW**: Translate functions will now use capturing groups for `EXTGLOB`/`EXTMATCH` groups in the returned regular expression patterns. ## 7.0.1 - **FIX**: Ensure that when using `REALPATH` that all symlinks are evaluated. - **FIX**: Fix issue where an extended pattern pattern can't follow right behind an inverse extended pattern. - **FIX**: Fix issues related to nested inverse glob patterns. ## 7.0 Check out [Release Notes](./release.md#upgrade-to-7.0) to learn more about upgrading to 7.0. - **NEW**: Recognize extended UNC paths. - **NEW**: Allow escaping any character in Windows drives for better compatibility with `SPLIT` and `BRACE` which requires a user to escape `{`, `}` and `|` to avoid expanding a pattern. - **NEW**: `raw_escape` now accepts the `raw_chars` parameter so that translation of Python character back references can be disabled. - **NEW**: Search functions that use `scandir` will not return `.` and `..` for wildcard patterns that require iterating over a directory to match the files against a pattern. This matches Python's glob and is most likely what most users expect. Pattern matching logic is unaffected. - **NEW**: Add `SCANDOTDIR` flag to enable previous behavior of injecting `.` and `..` in `scandir` results. `SCANDOTDIR` has no affect on match functions such as `globmatch` which don't use directory scanning. - **NEW**: Flag `NODOTDIR` has been added to disable patterns such as `.*` from matching `.` and `..`. When enabled, matching logic is changed to require a literal pattern of `.` and `..` to match the special directories `.` and `..`. This is more Zsh like. - **FIX**: Negative extended glob patterns (`!(...)`) incorrectly allowed for hidden files to be returned when one of the subpatterns started with `.`, even when `DOTMATCH`/`DOTGLOB` was not enabled. - **FIX**: When `NOUNIQUE` is enabled and `pathlib` is being used, you could still get non-unique results across patterns expanded with `BRACE` or `SPLIT` (or even by simply providing a list of patterns). Ensure that unique results are only returned when `NOUNIQUE` is not enabled. - **FIX**: Fix corner cases with `escape` and `raw_escape` with back slashes. - **FIX**: Ensure that `globmatch` does not match `test//` with pattern `test/*`. - **FIX**: `pathlib.match` should not evaluate symlinks that are on the left hand side of what was matched. ## 6.1 - **NEW**: `EXTMATCH`/`EXTGLOB` can now be used with `NEGATE` without needing `MINUSNEGATE`. If a pattern starts with `!(`, and `NEGATE` and `EXTMATCH`/`EXTGLOB` are both enabled, the pattern will not be treated as a `NEGATE` pattern (even if `!(` doesn't yield a valid `EXTGLOB` pattern). To negate a pattern that starts with a literal `(`, you must escape the bracket: `!\(`. - **FIX**: Support Python 3.9. - **FIX**: Adjust pattern limit logic of `glob` to be consistent with other functions. ## 6.0.3 - **FIX**: Fix issue where when `FOLLOW` and `GLOBSTAR` were used, a pattern like `**/*` would not properly match a directory which was a symlink. While Bash does not return a symlinked folder with `**`, `*` (and other patterns), should properly find the symlinked directory. - **FIX**: `pathlib` clearly states that the `match` method, if the pattern is relative, matches from the right. Wildcard Match used the same implementation that `rglob` used, which prepends `**/` to a relative pattern. This is essentially like `MATCHBASE`, but allows for multiple directory levels. This means that dot files (and special folders such as `.` and `..`) on the left side could prevent the path from matching depending on flags that were set. `match` will now be evaluated in such a way as to give the same right to left matching feel that Python's `pathlib` uses. ## 6.0.2 - **FIX**: Fix logic related to dot files and `GLOBSTAR`. Recursive directory search should return all dot files, which should then be filtered by the patterns. They should not be excluded before being filtered by the pattern. ## 6.0.1 - **FIX**: If we only have one pattern (exclusion patterns not included) we can disable unique path filtering on returns as you cannot have a duplicate path with only one inclusion pattern. ## 6.0 - **NEW**: Tilde user expansion support via the new `GLOBTILDE` flag. - **NEW**: `glob` by default now returns only unique results, regardless of whether multiple patterns that match the same file were provided, or even when `BRACE` or `SPLIT` expansion produces new patterns that match the same file. - **NEW**: A new flag called `NOUNIQUE` has been added that makes `glob` act like Bash, which will return the same file multiple times if multiple patterns match it, whether provided directly or due to the result of `BRACE` or `SPLIT` expansion. - **NEW**: Limit number of patterns that can be processed (expanded and otherwise) to 1000. Allow user to change this value via an optional `limit` parameter in related API functions. - **FIX**: Matching functions that receive multiple patterns, or that receive a single pattern that expands to multiple, will filter out duplicate patterns in order avoid redundant matching. While the `WcMatch` class crawls the file system, it utilizes the aforementioned matching functions in it's operation, and indirectly takes advantage of this. `glob` (and related functions: `rglob`, `iglob`, etc.) will also filter redundant patterns except when `NOUNIQUE` is enabled, this is so they can better act like Bash when `NOUNIQUE` is enabled. - **FIX**: `BRACE` is now processed before `SPLIT` in order to fix a number of edge cases. - **FIX**: `RAWCHARS` was inconsistently applied at different times depending on what was calling it. It is now applied first followed by `BRACE`, `SPLIT`, and finally `GLOBTILDE`. ## 5.1.0 - **NEW**: Add new parameter to `glob` related functions (except in `pathlib`) called `root_dir` that allows a user to specify a different working directory with either a string or path-like object. Path-like inputs are only supported on Python 3.6+. - **NEW**: Support path-like objects for `globmatch` and `globfilter` path inputs. Path-like inputs are only supported on Python 3.6+. - **FIX**: Filter functions should not alter the slashes of files it filters. Filtered strings and paths should be returned unaltered. ## 5.0.3 - **FIX**: Rework `glob` relative path handling so internally it is indistinguishable from when it is given no relative path and uses the current working directory. This fixes an issue where `pathlib` couldn't handle negate patterns properly (`!negate`). ## 5.0.2 - **FIX**: Fix case where a `GLOBSTAR` pattern, followed by a slash, was not disabling `MATCHBASE`. - **FIX**: Fix `pathlib` relative path resolution in glob implementations. ## 5.0.1 - **FIX**: In `glob`, avoid using up too many file descriptors by acquiring all file/folder names under a directory in one batch before recursing into other folders. ## 5.0 - **NEW**: Add `wcmatch.pathlib` which contains `pathlib` variants that uses `wcmatch.glob` instead of the default Python glob. - **NEW**: `escape` and `raw_escape` can manually be forced to use Windows or Linux/Unix logic via the keyword only argument by setting to `False` or `True` respectively. The default is `None` which will auto detect the system. - **NEW**: The deprecated flag `FORCECASE` has now been removed. - **NEW**: The deprecated functions `globsplit` and `fnsplit` have been removed. - **NEW**: The deprecated variables `version` and `version_info` have been removed. ## 4.3.1 - **FIX**: Regression for root level literal matches in `glob`. - **FIX**: Bug where `glob` would mistakenly abort if a pattern started with a literal file or directory and could not match a file or directory. This caused subsequent patterns in the chain to not get evaluated. ## 4.3.0 - **NEW**: Add `CASE` flag which allows for case sensitive paths on Linux, macOS, and Windows. Windows drive letters and UNC `//host-name/share-name/` portion are still treated insensitively, but all directories will be treated with case sensitivity. - **NEW**: With the recent addition of `CASE` and `FORCEUNIX`, `FORCECASE` is no longer needed. Deprecate `FORCECASE` which will be removed at some future point. ## 4.2.0 - **NEW**: Drop Python 3.4 support. - **NEW**: Add flags `FORCEWIN` and `FORCEUNIX` to force Windows or Linux/Unix path logic on commands that do not access the file system: `translate`, `fnmatch`, `filter`, `globmatch`, `globfilter`, etc. These flags will not work with `glob`, `iglob` or with the `WcMatch` class. It also will not work when using the `REALPATH` flag with things like `fnmatch`, `filter`, `globmatch`, `globfilter`. - **FIX**: `glob` corner case where the first folder, if defined as a literal name (not a magic pattern), would not be treated properly if `IGNORECASE` was enabled in Linux. ## 4.1.0 - **NEW**: Add `WcMatch.is_aborted`. - **FIX**: Remove deprecation of `kill` and `reset` in `WcMatch`. There are legitimate reasons to not deprecate killing via `kill` instead of simply breaking. - **FIX**: If for any reason, a file exists, but fails "is directory" check, consider it as a file. ## 4.0.1 - **FIX**: Fix regression with exclusion patterns that use braces in `glob`. - **FIX**: Translate functions should have `NODIR` patterns exclude if matched not exclude if not matched. ## 4.0 - **NEW**: Deprecated `WcMatch` class methods `kill` and `reset`. `WcMatch` should be broken with a simple `break` statement instead. - **NEW**: Add a new flag `MARK` to force `glob` to return directories with a trailing slash. - **NEW**: Add `MATCHBASE` that causes glob glob related functions and `WcMatch`, when the pattern has no slashes in it, to seek for any file anywhere in the tree with a matching basename. - **NEW**: Add `NODIR` that causes `glob` matchers and crawlers to only match and return files. - **NEW**: Exclusion patterns (enabled with `NEGATE`) now always enable `DOTALL` in the exclusion patterns. They also will match symlinks in `**` patterns. Only non `NEGATE` patterns that are paired with a `NEGATE` pattern are subject to symlinks and dot rules. Exclusion patterns themselves allow dots and symlinks to make filtering easier. - **NEW**: Exclusion patterns no longer provide a default inclusion pattern if one is not specified. Exclusion patterns are meant to filter the results of inclusion patterns. You can either use the `SPLIT` flag and provide an inclusion pattern with your default ('default_pattern|!exclusion'), or feed in a list of multiple patterns instead of a single string (`['inclusion', '!exclusion']`). If you really need the old behavior, you can use the `NEGATEALL` flag which will provide a default inclusion pattern that matches all files. - **NEW**: Translate now outputs exclusion patterns so that if they match, the file is excluded. This is opposite logic to how it used to be, but is more efficient. - **FIX**: An empty pattern in `glob` should not match slashes. ## 3.0.2 - **FIX**: Fix an offset issue when processing an absolute path pattern in `glob` on Linux or macOS. - **FIX**: Fix an issue where the `glob` command would use `GLOBSTAR` logic on `**` even when `GLOBSTAR` was disabled. ## 3.0.1 - **FIX**: In the `WcMatch` class, defer hidden file check until after the file or directory is compared against patterns to potentially avoid calling hidden if the pattern doesn't match. The reduced `lstat` calls improve performance. ## 3.0 - **NEW**: `globsplit` and `fnsplit` have been deprecated. Users are encouraged to use the new `SPLIT` flag to allow functions to use multiple wildcard paths delimited by `|`. - **NEW**: `globmatch` and `globfilter` will now parse provided paths as real paths if the new `REALPATH` flag is set. This has the advantage of allowing the commands to be aware of symlinks and properly apply related logic (whether to follow the links or not). It also helps to clarify ambiguous cases where it isn't clear if a file path references a directory because the trailing slash was omitted. It also allows the command to be aware of Windows drives evaluate the path in proper context compared to the current working directory. - **NEW**: `WcMatch` class no longer accepts the `recursive` or `show_hidden` parameter, instead the `RECURSIVE` or `HIDDEN` flag must be used. - **NEW**: `WcMatch` class now can search symlink directories with the new `SYMLINK` flag. - **NEW**: `glob` and `iglob` functions now behave like Bash 5.0 in regards to symlinks in `GLOBSTAR` (`**`). `GLOBSTAR` will ignore symlink directories. This affects other functions such as `globmatch` and `globfilter` when the `REALPATH` flag is enabled. - **NEW**: New flag called `FOLLOW` was added to force related `glob` commands to recognize and follow symlink directories. - **FIX**: Fix `glob` regression where inverse patterns such as `!**/test/**` would allow a directory `base/test` to match when it should have excluded it. - **FIX**: `glob` should handle root paths (`/`) properly, and on Windows, it should assume the drive of the current working directory. ## 2.2.1 - **FIX**: `EXTMATCH`/`EXTGLOB` should allow literal dots and should not treat dots like sequences do. - **FIX**: Fix `!(...)` extended match patterns in `glob` and `globmatch` so that they properly match `.` and `..` if their pattern starts with `.`. - **FIX**: Fix `!(...)` extended match patterns so that they handle path separators correctly. - **FIX**: Patterns such as `?` or `[.]` should not trigger matching directories `.` and `..` in `glob` and `globmatch`. ## 2.2.0 - **NEW**: Officially support Python 3.8. ## 2.1.0 - **NEW**: Deprecate `version` and `version_info` in favor of the more standard `__version__` and `__version_info__`. - **FIX**: Fix issue where exclusion patterns would trigger before end of path. - **FIX**: Fix `GLOBSTAR` regular expression pattern issues. ## 2.0.3 - **FIX**: In `glob`, properly handle files in the current working directory when give a literal pattern that matches it. ## 2.0.2 - **FIX**: `wcmatch` override events (`on_error` and `on_skip`) should verify the return is **not None** and not **not falsy**. ## 2.0.1 - **FIX**: Can't install due to requirements being assigned to setup opposed to install. ## 2.0 /// danger | Breaking Changes Version 2.0 introduces breaking changes in regards to flags. This is meant to bring about consistency amongst the provided libraries. Flag names have been changed in some cases, and logic has been inverted in some cases. /// - **NEW**: Glob's `NOBRACE`, `NOGLOBSTAR`, and `NOEXTGLOB` flags are now `BRACE`, `GLOBSTAR`, and `EXTGLOB` and now enable the features instead of disabling the features. This logic matches the provided `fnmatch` and `wcmatch`. - **NEW**: Glob's `DOTGLOB` and `EXTGLOB` also have the respective aliases `DOTMATCH` and `EXTMATCH` to provide consistent flags across provided libraries, but the `GLOB` variants that match Bash's feature names can still be used. - **NEW**: `fnmatch`'s `PERIOD` flag has been replaced with `DOTMATCH` with inverted logic from what was originally provided. - **NEW**: Documentation exposes the shorthand form of flags: `FORCECASE` --> `F`, etc. - **FIX**: Wcmatch always documented that it had the flag named `EXTMATCH`, but internally it was actually `EXTGLOB`, this was a bug though. `EXTMATCH` is now the documented and the actual flag to use. ## 1.0.2 - **FIX**: Officially support Python 3.7. ## 1.0.1 - **FIX**: Ensure that all patterns in `glob` that have a directory preceding `**` but also end with `**` returns the preceding directory. - **FIX**: Fix byte conversion in path normalization. - **FIX**: Ensure POSIX character classes, when at the start of a sequence, properly have hyphens escaped following it. `[[:ascii:]-z]` should convert to `[\x00-\x7f\\-b]` not `[\x00-\x7f-b]`. - **FIX**: Fix an issue where we would fail because we couldn't covert raw characters even though raw character parsing was disabled. - **FIX**: Better default for file patterns. Before if no pattern was provided for files, `'*'` was assumed, now it is `''`, and if `''` is used, all files will be matched. This works better for when full path is enabled as you get the same file matching logic. ## 1.0 - **NEW**: Initial release wcmatch-10.0/docs/src/markdown/about/contributing.md000066400000000000000000000045351467532413500226050ustar00rootroot00000000000000# Contributing & Support ## Become a Sponsor :octicons-heart-fill-16:{: .heart-throb} Open source projects take time and money. Help support the project by becoming a sponsor. You can add your support at any tier you feel comfortable with. No amount is too little. We also accept one time contributions via PayPal. [:octicons-mark-github-16: GitHub Sponsors](https://github.com/sponsors/facelessuser){: .md-button .md-button--primary } [:fontawesome-brands-paypal: PayPal](https://www.paypal.me/facelessuser){ .md-button} ## Bug Reports 1. Please **read the documentation** and **search the issue tracker** to try and find the answer to your question **before** posting an issue. 2. When creating an issue on the repository, please provide as much info as possible: - Version being used. - Operating system. - Version of Python. - Errors in console. - Detailed description of the problem. - Examples for reproducing the error. You can post pictures, but if specific text or code is required to reproduce the issue, please provide the text in a plain text format for easy copy/paste. The more info provided, the greater the chance someone will take the time to answer, implement, or fix the issue. 3. Be prepared to answer questions and provide additional information if required. Issues in which the creator refuses to respond to follow up questions will be marked as stale and closed. ## Reviewing Code Take part in reviewing pull requests and/or reviewing direct commits. Make suggestions to improve the code and discuss solutions to overcome weakness in the algorithm. ## Answer Questions in Issues Take time and answer questions and offer suggestions to people who've created issues in the issue tracker. Often people will have questions that you might have an answer for. Or maybe you know how to help them accomplish a specific task they are asking about. Feel free to share your experience to help others out. ## Pull Requests Pull requests are welcome, and a great way to help fix bugs and add new features. ## Documentation Improvements A ton of time has been spent not only creating and supporting this tool and related extensions, but also spent making this documentation. If you feel it is still lacking, show your appreciation for the tool and/or extensions by helping to improve the documentation. wcmatch-10.0/docs/src/markdown/about/license.md000066400000000000000000000000621467532413500215070ustar00rootroot00000000000000# License ## Wildcard Match --8<-- "LICENSE.md" wcmatch-10.0/docs/src/markdown/about/release.md000066400000000000000000000272471467532413500215230ustar00rootroot00000000000000# Release Notes ## Upgrade to 8.0 {: #upgrade-to-\8.0} Notable changes are minor and will affect very few. This should clarify breaking changes and how to migrate if applicable. ### `WcMatch` class Initialization Hook The [`WcMatch`](../wcmatch.md#wcmatch) class `on_init` hook was cleaned up. Prior to 8.0, it accepted both `*args` and `**kwargs` which is quite difficult to maintain and honestly for users to use. Moving forward, the `WcMatch` class will restrict all parameters to `**kwargs`. If you are using the `on_init` hook, you will simply need to change your override to accept arguments as `**kwargs`: ```py3 # Excplicitly named def on_init(self, key1=value, key2=value): # Or just use `**kwargs` def on_init(self, **kwargs): ``` Lastly, only pass your custom variables in as keyword arguments: ```py3 CustomWcmatch('.', '*.md|*.txt', flags=wcmatch.RECURSIVE, custom_key=value) ``` ## Upgrade to 7.0 {: #upgrade-to-\7.0} Notable changes will be highlighted here to help with migration to 7.0. ### Globbing Special Directories File globbing with [`glob.glob`](../glob.md#glob), [`glob.iglob`](../glob.md#iglob), [`pathlib.path.glob`](../pathlib.md#glob), and [`pathlib.Path.rglob`](../pathlib.md#rglob) no longer inject `.` and `..` into results when scanning directories. This *only* affects the results of a scanned directory and does not fundamentally change how glob patterns evaluate a path. Python's default glob does not return `.` or `..` for any "magic" (non-literal) patterns in `glob`. This is because magic patterns trigger glob to iterate over a directory in an attempt to find a file that can match the given "magic" pattern. Since `.` and `..` are not returned by Python's implementation of `scandir`, `.` and `..` never get evaluated. Literal patterns can side step the directory iteration with a simple check to see if the file exists. What this means is that a "magic" pattern of `.*` will not match `.` or `..`, because it is not returned in the scan, but a literal pattern of `.` or `..` will as the literal patterns are simply checked to see if they exist. This is common behavior for a number of libraries, Python, [node-glob], etc., but not all. Moving forward, we have chosen to adopt the Python's behavior as our default behavior, with the option of forcing Bash's behavior of returning `.` and `..` in a directory scan if desired. These examples will illustrate the behavior. In the first example, Python's `pathlib` is used to glob a directory. We can note that not a single entry in the results is `.` or `..`. ```pycon3 >>> import pathlib >>> list(pathlib.Path('.').glob('.*')) [PosixPath('.DS_Store'), PosixPath('.codecov.yml'), PosixPath('.tox'), PosixPath('.coverage'), PosixPath('.coveragerc'), PosixPath('.gitignore'), PosixPath('.github'), PosixPath('.pyspelling.yml'), PosixPath('.git')] ``` We can also show that if we search for the literal pattern of `..` that glob will then return `..` in the results. ```pycon3 >>> import pathlib >>> list(pathlib.Path('.').glob('..')) [PosixPath('..')] ``` When using the `match` function, we see that the pattern can match `..` just fine. This illustrates that it is not the pattern logic that restricts this, but a result of the behavior exhibited by `scandir`. ```pycon3 >>> import pathlib >>> pathlib.Path('..').match('.*') True ``` While our algorithm is different due to some of the features we support, and it may oversimplify things to say we now turn off injecting `.` and `..` into `scandir` results, but for all intents and purposes, all of our file system globbing functions exhibit the same behavior as Python's default glob now. ```pycon3 >>> from wcmatch import glob >>> glob.glob('.*') ['.DS_Store', '.codecov.yml', '.tox', '.coverage', '.coveragerc', '.gitignore', '.github', '.pyspelling.yml', '.git'] >>> glob.glob('..') ['..'] >>> glob.globmatch('..', '.*') True ``` Because this change only affects how files are returned when iterating the directories, we can notice that exclude patterns, which are used to filter the results, can match `.` or `..` with `.*`: ```pycon3 >>> from wcmatch import glob >>> glob.glob('..') ['..'] >>> glob.glob(['..', '!.*'], flags=glob.NEGATE) [] ``` If we want to modify the pattern matcher, and not just the the directory scanner, we can use the flag [`NODITDIR`](../glob.md#nodotdir). ```pycon3 >>> from wcmatch import glob >>> glob.glob(['..', '!.*'], flags=glob.NEGATE | glob.NODOTDIR) ['..'] >>> glob.glob(['..', '!..'], flags=glob.NEGATE | glob.NODOTDIR) [] ``` These changes were done for a couple of reasons: 1. Generally, it is rare to specifically want `.` and `..`, so often when people glob with something like `**/.*`, they are just trying to get hidden files. While we generally model our behavior off Bash, there are many alternative shells (such as Zsh) that do not return or match `.` and `..` with magic patterns by design, regardless of what directory scanner returns. 2. Many people who come to use our library are probably coming from having experience with Python's glob. By mirroring this behavior out of the box, it may help people adapt to the library easier. 3. Python's `pathlib`, which Wildcard Match's `pathlib` is derived from, normalizes paths by stripping out `.` directories and trimming off trailing slashes. This means patterns such as `**/.*`, which would normally match both `.hidden` and `.hidden/.`, would normalize those results to return two `.hidden` results. Mirroring this behavior helps provide more sane results and prevent confusing duplicates when using `pathlib`. 4. This is not unique behavior to Python's glob and our implementation. For example, let's take a look at [`node-glob`](https://github.com/isaacs/node-glob) and its underlying match library called [`minimatch`](https://github.com/isaacs/minimatch). ```js > glob('.*', {}, function (er, files) { ... console.log(files) ... }) > [ '.codecov.yml', '.coverage', '.coveragerc', '.DS_Store', '.git', '.github', '.gitignore', '.pyspelling.yml', '.tox' ] ``` We also see that the file matching library has no issues matching `.` or `..` with `.*`. ```js > minimatch("..", ".*") true ``` We can also see that ignore patterns, just like our ignore patterns, are applied to the results, and are unaffected by the underlying behavior of the directory scanner: ```js > glob('..', {}, function (er, files) { ... console.log(files) ... }) > [ '..' ] > glob('..', {ignore: ['.*']}, function (er, files) { ... console.log(files) ... }) > [] ``` For the majority of people, this is most likely an improvement rather than a hindrance, but if the old behavior is desired, you can use the new option [`SCANDOTDIR`](../glob.md#scandotdir) which restores the logic that emulates the feel of `scandir` returning `.` and `..` when iterating a directory. Due to the way [`pathlib`](../pathlib.md) normalizes paths, [`SCANDOTDIR`](../glob.md#scandotdir) is not recommended to be used with [`pathlib`](../pathlib.md). ### Windows Drive Handling It is not practical to scan a system for all mounted drives and available network paths. Just like with Python's default globbing, we do not scan all available drives, and so wildcard patterns do not apply to these drives. Unfortunately, our implementation used to only handle very basic UNC cases, and if patterns with extended UNC paths were attempted, failure was likely. 7.0 brings improvements related to Windows drives and UNC paths. Glob patterns will now properly respect extended UNC paths such as `//?/UNC/LOCALHOST/c$` and others. This means you can use these patterns without issues. And just like simple cases (`//server/mount`), extended cases do not require escaping meta characters, except when using pattern expansion syntax that is available via [`BRACE`](../glob.md#brace) and [`SPLIT`](../glob.md#split). ### Glob Escaping Because it can be problematic trying to mix Windows drives that use characters such as `{` and `}` with the [`BRACE`](../glob.md#brace) flag, you can now escape these meta characters in drives if required. Prior to 7.0, such escaping was disallowed, but now you can safely escape `{` and `}` to ensure optimal brace handling. While you can safely escape other meta characters in drives as well, it is never actually needed. Additionally, [`glob.escape`](../glob.md#escape) and [`glob.raw_escape`](../glob.md#raw_escape) will automatically escape `{`, `}` and `|` to avoid complications with [`BRACE`](../glob.md#brace) and [`SPLIT`](../glob.md#split). In general, a lot of corner cases with [`glob.escape`](../glob.md#escape) and [`glob.raw_escape`](../glob.md#raw_escape) were cleaned up. [`glob.escape`](../glob.md#escape) is meant to handle the escaping of normal paths so that they can be used in patterns. ```pycon3 >>> glob.escape(r'my\file-[work].txt', unix=False) 'my\\\\file\\-\\[work\\].txt' ``` If you are accepting an input from a source that is giving you a representation of a Python string (where `\` is represented by two `\`), then [`glob.raw_escape`](../glob.md#raw_escape) is what you want: ```pycon3 >>> glob.raw_escape(r'my\\file-[work].txt', unix=False) 'my\\\\file\\-\\[work\\].txt' ``` By default, [`glob.raw_escape`](../glob.md#raw_escape) always translates Python character back references into actual characters, but if this is not needed, a new option called `raw_chars` (`True` by default) has been added to disable this behavior: ```pycon3 >>> glob.raw_escape(r'my\\file-\x31.txt', unix=False) 'my\\\\file\\-1.txt' >>> glob.raw_escape(r'my\\file-\x31.txt', unix=False, raw_chars=False) 'my\\\\file\\-\\\\x31.txt' ``` ### Reduction of `pathlib` Duplicate Results In general, glob should return only unique results for a single inclusive pattern (exclusion patterns are not considered). If given multiple patterns, or if given a pattern that is expanded into multiple via [`BRACE`](../glob.md#brace) or [`SPLIT`](../glob.md#split), then duplicate results are actually possible. In 6.0, logic to strip redundant patterns and to filter out duplicate results was added. This deduping is performed by default if more than a single inclusive pattern is provided, even if they are indirectly provided via pattern expansion. The [`NOUNIQUE`](../glob.md#nounique) flag disables this behavior if desired. In general, this works well, but due to `pathlib`'s path normalization quirks, there were cases where duplicate results would still be returned for multiple patterns, and even a case where duplicates were returned for a single pattern. Due to `pathlib` file path normalization, `.` directories are stripped out, and trailing slashes are stripped off paths. With the changes noted in [Globbing](#globbing-special-directories) single pattern cases no longer return duplicate paths, but results across multiple patterns still could. For instance, it is possible that three different patterns, provided at the same time (or through pattern expansion) could match the following paths: `file/./path`, `file/path/.`, and `file/path`. Each of these results are unique as far as glob is concerned, but due to the `pathlib` normalization of `.` and trailing slashes, `pathlib` glob will return all three of these results as `file/path`, giving three identical results. In 7.0, logic was added to detect `pathlib` normalization cases and ensure that redundant results are not returned. ```pycon3 >>> glob.glob(['docs/./src', 'docs/src/.', 'docs/src']) ['docs/./src', 'docs/src/.', 'docs/src'] >>> list(pathlib.Path('.').glob(['docs/./src', 'docs/src/.', 'docs/src'])) [PosixPath('docs/src')] >>> list(pathlib.Path('.').glob(['docs/./src', 'docs/src/.', 'docs/src'], flags=pathlib.NOUNIQUE)) [PosixPath('docs/src'), PosixPath('docs/src'), PosixPath('docs/src')] ``` wcmatch-10.0/docs/src/markdown/fnmatch.md000066400000000000000000000416661467532413500204120ustar00rootroot00000000000000# `wcmatch.fnmatch` ```py3 from wcmatch import fnmatch ``` ## Syntax The `fnmatch` library is similar to the builtin [`fnmatch`][fnmatch], but with some enhancements and some differences. It is mainly used for matching filenames with glob patterns. For path names, Wildcard Match's [`globmatch`](./glob.md#globmatch) is a more appropriate choice. Not all of the features listed below are enabled by default. See [flags](#flags) for more information. /// tip | Backslashes When using backslashes, it is helpful to use raw strings. In a raw string, a single backslash is used to escape a character `#!py3 r'\?'`. If you want to represent a literal backslash, you must use two: `#!py3 r'some\\path'`. /// Pattern | Meaning ----------------- | ------- `*` | Matches everything. `?` | Matches any single character. `[seq]` | Matches any character in seq. `[!seq]` | Matches any character not in seq. Will also accept character exclusions in the form of `[^seq]`. `[[:alnum:]]` | POSIX style character classes inside sequences. See [POSIX Character Classes](#posix-character-classes) for more info. `\` | Escapes characters. If applied to a meta character or non-meta characters, the character will be treated as a literal character. If applied to another escape, the backslash will be a literal backslash. `!` | When used at the start of a pattern, the pattern will be an exclusion pattern. Requires the [`NEGATE`](#negate) flag. If also using the [`MINUSNEGATE`](#minusnegate) flag, `-` will be used instead of `!`. `?(pattern_list)` | The pattern matches if zero or one occurrences of any of the patterns in the `pattern_list` match the input string. Requires the [`EXTMATCH`](#extmatch) flag. `*(pattern_list)` | The pattern matches if zero or more occurrences of any of the patterns in the `pattern_list` match the input string. Requires the [`EXTMATCH`](#extmatch) flag. `+(pattern_list)` | The pattern matches if one or more occurrences of any of the patterns in the `pattern_list` match the input string. Requires the [`EXTMATCH`](#extmatch) flag. `@(pattern_list)` | The pattern matches if exactly one occurrence of any of the patterns in the `pattern_list` match the input string. Requires the [`EXTMATCH`](#extmatch) flag. `!(pattern_list)` | The pattern matches if the input string cannot be matched with any of the patterns in the `pattern_list`. Requires the [`EXTMATCH`](#extmatch) flag. `{}` | Bash style brace expansions. This is applied to patterns before anything else. Requires the [`BRACE`](#brace) flag. - Slashes are generally treated as normal characters, but on windows they are normalized. On Windows, `/` will match both `/` and `\\`. There is no need to explicitly use `\\` in patterns on Windows, but if you do, they must be escaped to specify a literal `\\`. If a backslash is escaped, it will match all valid windows separators, just like `/` does. - By default, `.` is *not* matched by `*`, `?`, and `[]`. See the [`DOTMATCH`](#dotmatch) flag to match `.` at the start of a filename without a literal `.`. --8<-- "posix.md" ## Multi-Pattern Limits Many of the API functions allow passing in multiple patterns or using either [`BRACE`](#brace) or [`SPLIT`](#split) to expand a pattern in to more patterns. The number of allowed patterns is limited `1000`, but you can raise or lower this limit via the keyword option `limit`. If you set `limit` to `0`, there will be no limit. /// new | New 6.0 The imposed pattern limit and corresponding `limit` option was introduced in 6.0. /// ## API #### `fnmatch.fnmatch` {: #fnmatch} ```py3 def fnmatch(filename, patterns, *, flags=0, limit=1000, exclude=None) ``` `fnmatch` takes a file name, a pattern (or list of patterns), and flags. It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or a list of patterns. It will return a boolean indicating whether the file name was matched by the pattern(s). ```pycon3 >>> from wcmatch import fnmatch >>> fnmatch.fnmatch('test.txt', '@(*.txt|*.py)', flags=fnmatch.EXTMATCH) True ``` When applying multiple patterns, a file matches if it matches any of the patterns: ```pycon3 >>> from wcmatch import fnmatch >>> fnmatch.fnmatch('test.txt', ['*.txt', '*.py'], flags=fnmatch.EXTMATCH) True ``` Exclusions can be used by taking advantage of the `exclude` parameter. It takes a single exclude pattern or a list of patterns. Files that match the exclude pattern will not be matched. ```pycon3 >>> from wcmatch import fnmatch >>> fnmatch.fnmatch('test.py', '*', exclude='*.py') False >>> fnmatch.fnmatch('test.txt', '*', exclude='*.py') True ``` Inline exclusion patterns are allowed as well. When exclusion patterns are used in conjunction with inclusion patterns, a file will be considered matched if one of the inclusion patterns match **and** none of the exclusion patterns match. If an exclusion pattern is given without any inclusion patterns, the pattern will match nothing. Exclusion patterns are meant to filter other patterns, not match anything by themselves. ```pycon3 >>> from wcmatch import fnmatch >>> fnmatch.fnmatch('test.py', '*|!*.py', flags=fnmatch.NEGATE | fnmatch.SPLIT) False >>> fnmatch.fnmatch('test.txt', '*|!*.py', flags=fnmatch.NEGATE | fnmatch.SPLIT) True >>> fnmatch.fnmatch('test.txt', ['*.txt', '!avoid.txt'], flags=fnmatch.NEGATE) True >>> fnmatch.fnmatch('avoid.txt', ['*.txt', '!avoid.txt'], flags=fnmatch.NEGATE) False ``` As mentioned, exclusion patterns need to be applied to a inclusion pattern to work, but if it is desired, you can force exclusion patterns to assume all files should be filtered with the exclusion pattern(s) with the [`NEGATEALL`](#negateall) flag. Essentially, it means if you use a pattern such as `!*.md`, it will assume two pattern were given: `*` and `!*.md`. ```pycon3 >>> from wcmatch import fnmatch >>> fnmatch.fnmatch('test.py', '!*.py', flags=fnmatch.NEGATE | fnmatch.NEGATEALL) False >>> fnmatch.fnmatch('test.txt', '!*.py', flags=fnmatch.NEGATE | fnmatch.NEGATEALL) True ``` /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 8.4 `exclude` parameter was added. /// #### `fnmatch.filter` {: #filter} ```py3 def filter(filenames, patterns, *, flags=0, limit=1000, exclude=None): ``` `filter` takes a list of filenames, a pattern (or list of patterns), and flags. It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or a list of patterns.It returns a list of all files that matched the pattern(s). The same logic used for [`fnmatch`](#fnmatch) is used for `filter`, albeit more efficient for processing multiple files. ```pycon3 >>> from wcmatch import fnmatch >>> fnmatch.filter(['a.txt', 'b.txt', 'c.py'], '*.txt') ['a.txt', 'b.txt'] ``` /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 8.4 `exclude` parameter was added. /// #### `fnmatch.translate` {: #translate} ```py3 def translate(patterns, *, flags=0, limit=1000, exclude=None): ``` `translate` takes a file pattern (or list of patterns) and flags. It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or a list of patterns. It returns two lists: one for inclusion patterns and one for exclusion patterns. The lists contain the regular expressions used for matching the given patterns. It should be noted that a file is considered matched if it matches at least one inclusion pattern and matches **none** of the exclusion patterns. ```pycon3 >>> from wcmatch import fnmatch >>> fnmatch.translate('*.{a,{b,c}}', flags=fnmatch.BRACE) (['^(?s:(?=.)(?![.]).*?\\.a)$', '^(?s:(?=.)(?![.]).*?\\.b)$', '^(?s:(?=.)(?![.]).*?\\.c)$'], []) >>> fnmatch.translate('**|!*.{a,{b,c}}', flags=fnmatch.BRACE | fnmatch.NEGATE | fnmatch.SPLIT) (['^(?s:(?=.)(?![.]).*?)$'], ['^(?s:(?=.).*?\\.a)$', '^(?s:(?=.).*?\\.b)$', '^(?s:(?=.).*?\\.c)$']) ``` When using [`EXTMATCH`](#extmatch) patterns, patterns will be returned with capturing groups around the groups: While in regex patterns like `#!py3 r'(a)+'` would capture only the last character, even though multiple where matched, we wrap the entire group to be captured: `#!py3 '+(a)'` --> `#!py3 r'((a)+)'`. ```pycon3 >>> from wcmatch import fnmatch >>> import re >>> gpat = fnmatch.translate("@(file)+([[:digit:]])@(.*)", flags=fnmatch.EXTMATCH) >>> pat = re.compile(gpat[0][0]) >>> pat.match('file33.test.txt').groups() ('file', '33', '.test.txt') ``` /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 7.1 Translate patterns now provide capturing groups for [`EXTMATCH`](#extmatch) groups. /// /// new | New 8.4 `exclude` parameter was added. /// #### `fnmatch.escape` {: #escape} ```py3 def escape(pattern): ``` The `escape` function will conservatively escape `-`, `!`, `*`, `?`, `(`, `)`, `[`, `]`, `|`, `{`, `}`, and `\` with backslashes, regardless of what feature is or is not enabled. It is meant to escape filenames. ```pycon3 >>> from wcmatch import fnmatch >>> fnmatch.escape('**file**{}.txt') '\\*\\*file\\*\\*\\{\\}.txt' >>> fnmatch.fnmatch('**file**{}.txt', fnmatch.escape('**file**{}.txt')) True ``` /// new | New 8.1 An `escape` variant for `fnmatch` was made available in 8.1. /// ### `fnmatch.is_magic` {: #is_magic} ```py3 def is_magic(pattern, *, flags=0): """Check if the pattern is likely to be magic.""" ``` This checks a given filename or `pattern` to see if it is "magic" or not. The check is based on the enabled features via `flags`. Filenames or patterns are expected to be/target full names. This variant of `is_magic` is meant to be run on filenames or patterns for file names only. If you need to check patterns with full paths, particularly Windows paths that include drive names or UNC sharepoints (which require special logic), it is recommended to use the [`glob.escape`](./glob.md#escape) function. ```pycon3 >>> fnmatch.is_magic('test') False >>> fnmatch.is_magic('[test]ing?') True ``` The table below illustrates which symbols are searched for based on the given feature. Each feature adds to the "default". In the case of [`NEGATE`](#negate), if [`MINUSNEGATE`](#minusnegate) is also enabled, [`MINUSNEGATE`](#minusnegate)'s symbols will be searched instead of [`NEGATE`](#negate)'s symbols. Features | Symbols ----------------------------- | ------- Default | `?*[]\` [`EXTMATCH`](#extmatch) | `()` [`BRACE`](#brace) | `{}` [`NEGATE`](#negate) | `!` [`MINUSNEGATE`](#minusnegate) | `-` [`SPLIT`](#split) | `|` /// new | New 8.1 Added `is_magic` in 8.1. /// ## Flags #### `fnmatch.CASE, fnmatch.C` {: #case} `CASE` forces case sensitivity. `CASE` has higher priority than [`IGNORECASE`](#ignorecase). #### `fnmatch.IGNORECASE, fnmatch.I` {: #ignorecase} `IGNORECASE` forces case insensitivity. [`CASE`](#case) has higher priority than `IGNORECASE`. #### `fnmatch.RAWCHARS, fnmatch.R` {: #rawchars} `RAWCHARS` causes string character syntax to be parsed in raw strings: `#!py3 r'\u0040'` --> `#!py3 r'@'`. This will handle standard string escapes and Unicode including `#!py3 r'\N{CHAR NAME}'`. #### `fnmatch.NEGATE, fnmatch.N` {: #negate} `NEGATE` causes patterns that start with `!` to be treated as exclusion patterns. A pattern of `!*.py` would match any file but Python files. Exclusion patterns cannot be used by themselves though, and must be paired with a normal, inclusion pattern, either by utilizing the [`SPLIT`](#split) flag, or providing multiple patterns in a list. Assuming the `SPLIT` flag, this means using it in a pattern such as `inclusion|!exclusion`. If it is desired, you can force exclusion patterns, when no inclusion pattern is provided, to assume all files match unless the file matches the excluded pattern. This is done with the [`NEGATEALL`](#negateall) flag. `NEGATE` enables [`DOTMATCH`](#dotglob) in all exclude patterns, this cannot be disabled. This will not affect the inclusion patterns. If `NEGATE` is set and exclusion patterns are passed via a matching function's `exclude` parameter, `NEGATE` will be ignored and the `exclude` patterns will be used instead. Either `exclude` or `NEGATE` should be used, not both. #### `fnmatch.NEGATEALL, fnmatch.A` {: #negateall} `NEGATEALL` can force exclusion patterns, when no inclusion pattern is provided, to assume all files match unless the file matches the excluded pattern. Essentially, it means if you use a pattern such as `!*.md`, it will assume two patterns were given: `*` and `!*.md`, where `!*.md` is applied to the results of `*`. Dot files will not be returned unless [`DOTMATCH`](#dotmatch). #### `fnmatch.MINUSNEGATE, fnmatch.M` {: #minusnegate} When `MINUSNEGATE` is used with [`NEGATE`](#negate), exclusion patterns are recognized by a pattern starting with `-` instead of `!`. This plays nice with the [`EXTMATCH`](#extmatch) option. #### `fnmatch.DOTMATCH, fnmatch.D` {: #dotmatch} By default, [`fnmatch`](#fnmatch) and related functions will not match file or directory names that start with dot `.` unless matched with a literal dot. `DOTMATCH` allows the meta characters (such as `*`) to match dots like any other character. Dots will not be matched in `[]`, `*`, or `?`. #### `fnmatch.EXTMATCH, fnmatch.E` {: #extmatch} `EXTMATCH` enables extended pattern matching. This includes special pattern lists such as `+(...)`, `*(...)`, `?(...)`, etc. See the [syntax overview](#syntax) for more information. /// tip | EXTMATCH and NEGATE When using `EXTMATCH` and [`NEGATE`](#negate) together, if a pattern starts with `!(`, the pattern will not be treated as a [`NEGATE`](#negate) pattern (even if `!(` doesn't yield a valid `EXTMATCH` pattern). To negate a pattern that starts with a literal `(`, you must escape the bracket: `!\(`. /// #### `fnmatch.BRACE, fnmatch.B` {: #brace} `BRACE` enables Bash style brace expansion: `a{b,{c,d}}` --> `ab ac ad`. Brace expansion is applied before anything else. When applied, a pattern will be expanded into multiple patterns. Each pattern will then be parsed separately. Redundant, identical patterns are discarded[^1] by default. For simple patterns, it may make more sense to use [`EXTMATCH`](#extmatch) which will only generate a single pattern which will perform much better: `@(ab|ac|ad)`. /// warning | Massive Expansion Risk 1. It is important to note that each pattern is matched separately, so patterns such as `{1..100}` would generate **one hundred** patterns. Sometimes patterns like this are needed, so construct patterns thoughtfully and carefully. 2. `BRACE` and [`SPLIT`](#split) both expand patterns into multiple patterns. Using these two syntaxes simultaneously can exponential increase in duplicate patterns: ```pycon3 >>> expand('test@(this{|that,|other})|*.py', BRACE | SPLIT | EXTMATCH) ['test@(this|that)', 'test@(this|other)', '*.py', '*.py'] ``` This effect is reduced as redundant, identical patterns are optimized away[^1]. But it is useful to know if trying to construct efficient patterns. /// [^1]: Identical patterns are only reduced by comparing case sensitively as POSIX character classes are case sensitive: `[[:alnum:]]` =/= `[[:ALNUM:]]`. #### `fnmatch.SPLIT, fnmatch.S` {: #split} `SPLIT` is used to take a string of multiple patterns that are delimited by `|` and split them into separate patterns. This is provided to help with some interfaces that might need a way to define multiple patterns in one input. It pairs really well with [`EXTMATCH`](#extmatch) and takes into account sequences (`[]`) and extended patterns (`*(...)`) and will not parse `|` within them. You can also escape the delimiters if needed: `\|`. While `SPLIT` is not as powerful as [`BRACE`](#brace), it's syntax is very easy to use, and when paired with [`EXTMATCH`](#extmatch), it feels natural and comes a bit closer. It also much harder to create massive expansions of patterns with it, except when paired *with* [`BRACE`](#brace). See [`BRACE`](#brace) and it's warnings related to pairing it with `SPLIT`. ```pycon3 >>> from wcmatch import fnmatch >>> fnmatch.fnmatch('test.txt', '*.txt|*.py', flags=fnmatch.SPLIT) True >>> fnmatch.fnmatch('test.py', '*.txt|*.py', flags=fnmatch.SPLIT) True ``` #### `fnmatch.FORCEWIN, fnmatch.W` {: #forcewin} `FORCEWIN` will force Windows name and case logic to be used on Linux/Unix systems. It will also cause slashes to be normalized. This is great if you need to match Windows specific names on a Linux/Unix system. If `FORCEWIN` is used along side [`FORCEUNIX`](#forceunix), both will be ignored. #### `fnmatch.FORCEUNIX, fnmatch.U` {: #forceunix} `FORCEUNIX` will force Linux/Unix name and case logic to be used on Windows systems. This is great if you need to match Linux/Unix specific names on a Windows system. When using `FORCEUNIX`, the names are assumed to be case sensitive, but you can use [`IGNORECASE`](#ignorecase) to use case insensitivity. If `FORCEUNIX` is used along side [`FORCEWIN`](#forcewin), both will be ignored. wcmatch-10.0/docs/src/markdown/glob.md000066400000000000000000001434041467532413500177060ustar00rootroot00000000000000# `wcmatch.glob` ```py3 from wcmatch import glob ``` ## Syntax The `glob` library provides methods for traversing the file system and returning files that matched a defined set of glob patterns. The library also provides a function called [`globmatch`](#globmatch) for matching file paths which is similar to [`fnmatch`](./fnmatch.md#fnmatch), but for paths. In short, [`globmatch`](#globmatch) matches what [`glob`](#glob) globs :slight_smile:. /// tip When using backslashes, it is helpful to use raw strings. In a raw string, a single backslash is used to escape a character `#!py3 r'\?'`. If you want to represent a literal backslash, you must use two: `#!py3 r'some\\path'`. /// Pattern | Meaning ----------------- | ------- `*` | Matches everything except slashes. On Windows it will avoid matching backslashes as well as slashes. `**` | Matches zero or more directories, but will never match the directories ` . ` and `..`. Requires the [`GLOBSTAR`](#globstar) flag. `***` | Like `**` but will also recurse symlinks. Requires the [`GLOBSTARLONG`](#globstarlong) flag. `?` | Matches any single character. `[seq]` | Matches any character in seq. `[!seq]` | Matches any character not in seq. Will also accept character exclusions in the form of `[^seq]`. `[[:alnum:]]` | POSIX style character classes inside sequences. See [POSIX Character Classes](#posix-character-classes) for more info. `\` | Escapes characters. If applied to a meta character or non-meta characters, the character will be treated as a literal character. If applied to another escape, the backslash will be a literal backslash. `!` | When used at the start of a pattern, the pattern will be an exclusion pattern. Requires the [`NEGATE`](#negate) flag. If also using the [`MINUSNEGATE`](#minusnegate) flag, `-` will be used instead of `!`. `?(pattern_list)` | The pattern matches if zero or one occurrences of any of the patterns in the `pattern_list` match the input string. Requires the [`EXTGLOB`](#extglob) flag. `*(pattern_list)` | The pattern matches if zero or more occurrences of any of the patterns in the `pattern_list` match the input string. Requires the [`EXTGLOB`](#extglob) flag. `+(pattern_list)` | The pattern matches if one or more occurrences of any of the patterns in the `pattern_list` match the input string. Requires the [`EXTGLOB`](#extglob) flag. `@(pattern_list)` | The pattern matches if exactly one occurrence of any of the patterns in the `pattern_list` match the input string. Requires the [`EXTGLOB`](#extglob) flag. `!(pattern_list)` | The pattern matches if the input string cannot be matched with any of the patterns in the `pattern_list`. Requires the [`EXTGLOB`](#extglob) flag. `{}` | Bash style brace expansions. This is applied to patterns before anything else. Requires the [`BRACE`](#brace) flag. `~/pattern` | User path expansion via `~/pattern` or `~user/pattern`. Requires the [`GLOBTILDE`](#globtilde) flag. - Slashes are generally treated special in glob related methods. Slashes are not matched in `[]`, `*`, `?`, or extended patterns like `*(...)`. Slashes can be matched by `**` if [`GLOBSTAR`](#globstar) is set. - Slashes on Windows are normalized. `/` will match both `/` and `\\`. There is no need to explicitly use `\\` in patterns on Windows, but if you do, they must be escaped to specify a literal `\\`. If a backslash is escaped, it will match all valid windows separators, just like `/` does. - On Windows, drives are treated special and must come at the beginning of the pattern and cannot be matched with `*`, `[]`, `?`, or even extended match patterns like `+(...)`. - Windows drives are recognized as either `C:/` and `//Server/mount/`. If a path uses an ambiguous root (`/some/path`), the system will assume the drive of the current working directory. - Meta characters have no effect when inside a UNC path: `//Server?/mount*/`. The one exception is pattern expansion characters like `{}` which are used by [brace expansion](#brace) and `|` used by [pattern splitting](#split). Pattern expansion characters are the only characters that can be escaped in a Windows drive/mount. - If [`FORCEUNIX`](#forceunix) is applied on a Windows system, match and filter commands that do not touch the file system will **not** have slashes normalized. In addition, drive letters will also not be handled. Essentially, paths will be treated as if on a Linux/Unix system. Commands that do touch the file system ([`glob`](#glob) and [`iglob`](#iglob)) will ignore [`FORCEUNIX`](#forceunix) and [`FORCEWIN`](#forcewin). [`globmatch`](#globmatch) and [`globfilter`](#globfilter), will also ignore [`FORCEUNIX`](#forceunix) and [`FORCEWIN`](#forcewin) if the [`REALPATH`](#realpath) flag is enabled. [`FORCEWIN`](#forcewin) will do the opposite on a Linux/Unix system, and will force Windows logic on a Linux/Unix system. Like with [`FORCEUNIX`](#forceunix), it only applies to commands that don't touch the file system. - By default, file and directory names starting with `.` are only matched with literal `.`. The patterns `*`, `**`, `?`, and `[]` will not match a leading `.`. To alter this behavior, you can use the [`DOTGLOB`](#dotglob) flag. - [`NEGATE`](#negate) will always enable [`DOTGLOB`](#dotglob) in exclude patterns. - Even with [`DOTGLOB`](#dotglob) enabled, special tokens will not match a special directory (`.` or `..`). But when a literal `.` is used at the start of the pattern (`.*`, `.`, `..`, etc.), `.` and `..` can potentially be matched. - In general, Wildcard Match's behavior is modeled off of Bash's, and prior to version 7.0, unlike Python's default [`glob`][glob], Wildcard Match's [`glob`](#glob) would match and return `.` and `..` for magic patterns like `.*`. This is because our directory scanning logic inserts `.` and `..` into results to be faithful to Bash. While this emulates Bash's behavior, it can be surprising to the user, especially if they are used to Python's default glob. In 7.0 we now avoid returning `.` and `..` in our directory scanner. This does not affect how patterns are matched, just what is returned via our directory scan logic. You can once again enable the old Bash-like behavior with the flag [`SCANDOTDIR`](#scandotdir) if this old behavior is desired. Python's default: ```pycon3 >>> import glob >>> glob.glob('docs/.*') [] ``` Wildcard Match: ```pycon3 >>> from wcmatch import glob >>> glob.glob('docs/.*') [] ``` Bash: ```shell-session $ echo docs/.* docs/. docs/.. ``` Bash-like behavior restored in Wildcard Match [`SCANDOTDIR`](#scandotdir): ```pycon3 >>> from wcmatch import glob >>> glob.glob('docs/.*', flags=glob.SCANDOTDIR) ['docs/.', 'docs/..'] ``` It is important to stress that this logic only relates to directory scanning and does not fundamentally alter glob patterns. We can still match a path of `..` with `.*` when strictly doing a match: ```pycon3 >>> from wcmatch import glob >>> glob.globmatch('..', '.*') True ``` Nor does it affect exclude results as they are used to filter the results after directory scanning: ```pycon3 >>> from wcmatch import glob >>> glob.glob('..') ['..'] >>> glob.glob(['..', '!.*'], flags=glob.NEGATE) [] ``` If we wish to fundamentally alter the pattern matching behavior, we can use [`NODOTDIR`](#nodotdir). This would provide a more Zsh feel. ```pycon3 >>> from wcmatch import glob >>> glob.glob(['..', '!.*'], flags=glob.NEGATE | glob.NODOTDIR) ['..'] >>> glob.glob(['..', '!..'], flags=glob.NEGATE | glob.NODOTDIR) [] >>> glob.globmatch('..', '.*', flags=glob.NODOTDIR) False ``` /// new | Changes 7.0 Prior to 7.0 `.` and `..` would get returned by our directory scanner. This is no longer the default. /// /// new | New 7.0 Legacy behavior of directory scanning, in relation to `.` and `..`, can be restored via [`SCANDOTDIR`](#scandotdir). [`NODOTDIR`](#nodotdir) was added in 7.0. /// --8<-- "posix.md" ## Windows Separators On Windows, it is not required to use backslashes for path separators as `/` will match path separators for all systems. The following will work on Windows and Linux/Unix systems. ```python glob.glob('docs/.*') ``` With that said, you can match Windows separators with backslashes as well. Keep in mind that Wildcard Match allows escaped characters in patterns, so to match a literal backslash separator, you must escape the backslash. It is advised to use raw strings when using backslashes to make the patterns more readable, but either of the below will work. ```python glob.glob(r'docs\\.*') glob.glob('docs\\\\.*') ``` ## Multi-Pattern Limits Many of the API functions allow passing in multiple patterns or using either [`BRACE`](#brace) or [`SPLIT`](#split) to expand a pattern in to more patterns. The number of allowed patterns is limited `1000`, but you can raise or lower this limit via the keyword option `limit`. If you set `limit` to `0`, there will be no limit. /// new | New 6.0 The imposed pattern limit and corresponding `limit` option was introduced in 6.0. /// ## API #### `glob.glob` {: #glob} ```py3 def glob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, exclude=None): ``` `glob` takes a pattern (or list of patterns), flags, and an optional root directory (string or path-like object) and/or directory file descriptor. It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or a list of patterns.When executed it will crawl the file system returning matching files. /// warning | Path-like Input Support Path-like object input support is only available in Python 3.6+ as the path-like protocol was added in Python 3.6. /// ```pycon3 >>> from wcmatch import glob >>> glob.glob('**/*.md') ['docs/src/markdown/_snippets/abbr.md', 'docs/src/markdown/_snippets/links.md', 'docs/src/markdown/_snippets/refs.md', 'docs/src/markdown/changelog.md', 'docs/src/markdown/fnmatch.md', 'docs/src/markdown/glob.md', 'docs/src/markdown/index.md', 'docs/src/markdown/installation.md', 'docs/src/markdown/license.md', 'README.md'] ``` Using a list, we can add exclusion patterns and also exclude directories and/or files: ```pycon3 >>> from wcmatch import glob >>> glob.glob(['**/*.md', '!README.md', '!**/_snippets'], flags=glob.NEGATE) ['docs/src/markdown/changelog.md', 'docs/src/markdown/fnmatch.md', 'docs/src/markdown/glob.md', 'docs/src/markdown/index.md', 'docs/src/markdown/installation.md', 'docs/src/markdown/license.md'] ``` When a glob pattern ends with a slash, it will only return directories: ```pycon3 >>> from wcmatch import glob >>> glob.glob('**/') ['__pycache__/', 'docs/', 'docs/src/', 'docs/src/markdown/', 'docs/src/markdown/_snippets/', 'docs/theme/', 'requirements/', 'stuff/', 'tests/', 'tests/__pycache__/', 'wcmatch/', 'wcmatch/__pycache__/'] ``` When providing a list, all patterns are run in the same context, but will not be run in the same pass. Each pattern is run in a separate pass, except for exclusion patterns (see the [`NEGATE`](#negate) flag) which are applied as filters to the inclusion patterns. Since each pattern is run in its own pass, it is possible for many directories to be researched multiple times. In Bash, duplicate files can be returned: ```console $ echo *.md README.md LICENSE.md README.md README.md ``` And we see that Wildcard Match's `glob` behaves the same, except it only returns unique results. ```pycon3 >>> from wcmatch import glob >>> glob.glob(['*.md', 'README.md']) ['LICENSE.md', 'README.md'] ``` If we wanted to completely match Bash's results, we would turn off unique results with the [`NOUNIQUE`](#nounique) flag. ```pycon3 >>> from wcmatch import glob >>> glob.glob(['*.md', 'README.md'], flags=glob.NOUNIQUE) ['LICENSE.md', 'README.md', 'README.md'] ``` And if we apply an exclusion pattern, since the patterns share the same context, the exclusion applies to both: ```pycon3 >>> from wcmatch import glob >>> glob.glob(['*.md', , 'README.md', '!README.md'], flags=glob.NEGATE | glob.NOUNIQUE) ['LICENSE.md'] ``` Features like [`BRACE`](#brace) and [`SPLIT`](#split) actually take a single string and breaks them up into multiple patterns. These features, when enabled and used, will also exhibit this behavior: ```pycon3 >>> from wcmatch import glob >>> glob.glob('{*,README}.md', flags=glob.BRACE | glob.NOUNIQUE) ['LICENSE.md', 'README.md', 'README.md'] ``` This also aligns with Bash's behavior: ```console $ echo {*,README}.md LICENSE.md README.md README.md ``` You can resolve user paths with `~` if the [`GLOBTILDE`](#globtilde) flag is enabled. You can also target specific users with `~user`. ```pycon3 >>> from wcmatch import glob >>> glob.glob('~', flags=glob.GLOBTILDE) ['/home/facelessuser'] >>> glob.glob('~root', flags=glob.GLOBTILDE) ['/root'] ``` By default, `glob` uses the current working directory to evaluate relative patterns. Normally you'd have to use `#!py3 os.chdir('/new/path')` to evaluate patterns relative to a different path. By setting `root_dir` parameter you can change the root path without using `os.chdir`. ```pycon3 >>> from wcmatch import glob >>> glob.glob('*') ['appveyor.yml', 'docs', 'LICENSE.md', 'MANIFEST.in', 'mkdocs.yml', 'README.md', 'requirements', 'setup.cfg', 'setup.py', 'tests', 'tox.ini', 'wcmatch'] >>> glob.glob('*', root_dir='docs/src') ['dictionary', 'markdown'] ``` Additionally, you can use `dir_fd` and specify a root directory with a directory file descriptor. ```pycon3 >>> import os >>> from wcmatch import glob >>> dir_fd = os.open('docs/src', os.O_RDONLY | os.O_DIRECTORY) >>> glob.glob('*', dir_fd=dir_fd) ['markdown', 'dictionary'] ``` /// warning | Support for Directory Descriptors Directory descriptors may not be supported on all systems. You can check whether or not `dir_fd` is supported for a your platform referencing the attribute `#!py3 glob.SUPPORT_DIR_FD` which will be `#!py3 True` if it is supported. Additionally, the `#!py3 os.O_DIRECTORY` may not be defined on some systems. You can likely just use `#!py3 os.O_RDONLY`. /// /// new | New 5.1 `root_dir` was added in 5.1.0. /// /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 8.2 `dir_fd` parameter was added in 8.2. /// /// new | New 8.4 `exclude` parameter was added. /// #### `glob.iglob` {: #iglob} ```py3 def iglob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, exclude=None): ``` `iglob` is just like [`glob`](#glob) except it returns an iterator. ```pycon3 >>> from wcmatch import glob >>> list(glob.iglob('**/*.md')) ['docs/src/markdown/_snippets/abbr.md', 'docs/src/markdown/_snippets/links.md', 'docs/src/markdown/_snippets/refs.md', 'docs/src/markdown/changelog.md', 'docs/src/markdown/fnmatch.md', 'docs/src/markdown/glob.md', 'docs/src/markdown/index.md', 'docs/src/markdown/installation.md', 'docs/src/markdown/license.md', 'README.md'] ``` /// new | New 5.1 `root_dir` was added in 5.1.0. /// /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 8.2 `dir_fd` parameter was added in 8.2. /// /// new | New 8.4 `exclude` parameter was added. /// #### `glob.globmatch` {: #globmatch} ```py3 def globmatch(filename, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, exclude=None): ``` `globmatch` takes a file name (string or path-like object), a pattern (or list of patterns), flags, and an optional root directory and/or file descriptor. It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or a list of patterns. It will return a boolean indicating whether the file path was matched by the pattern(s). ```pycon3 >>> from wcmatch import glob >>> glob.globmatch('some/path/test.txt', '**/*/@(*.txt|*.py)', flags=glob.EXTGLOB) True ``` When applying multiple patterns, a file path matches if it matches any of the patterns: ```pycon3 >>> from wcmatch import glob >>> glob.globmatch('some/path/test.txt', ['**/*/*.txt', '**/*/*.py']) True ``` Exclusion patterns are allowed as well. When exclusion patterns are used in conjunction with other patterns, a path will be considered matched if one of the positive patterns match **and** none of the exclusion patterns match. If an exclusion pattern is given without any inclusion patterns, the pattern will match nothing. Exclusion patterns are meant to filter other patterns, not match anything by themselves. ```pycon3 >>> from wcmatch import glob >>> glob.globmatch('some/path/test.py', '**|!**/*.txt', flags=glob.NEGATE | glob.GLOBSTAR | glob.SPLIT) True >>> glob.globmatch('some/path/test.txt', '**|!**/*.txt', flags=glob.NEGATE | glob.GLOBSTAR | glob.SPLIT) False >>> glob.globmatch('some/path/test.txt', ['*/*/*.txt', '!*/*/avoid.txt'], flags=glob.NEGATE) True >>> glob.globmatch('some/path/avoid.txt', ['*/*/*.txt', '!*/*/avoid.txt'], flags=glob.NEGATE) False ``` As mentioned, exclusion patterns need to be applied to a inclusion pattern to work, but if it is desired, you can force exclusion patterns to assume all files should be filtered with the exclusion pattern(s) with the [`NEGATEALL`](#negateall) flag. Essentially, it means if you use a pattern such as `!*.md`, it means if you use a pattern such as `!*.md`, it will assume two pattern were given: `*` and `!*.md` (where `**` is specifically treated as if [`GLOBSTAR`](#globstar) was enabled). ```pycon3 >>> from wcmatch import glob >>> glob.globmatch('some/path/test.py', '!**/*.txt', flags=glob.NEGATE | glob.GLOBSTAR | glob.NEGATEALL) True >>> glob.globmatch('some/path/test.txt', '!**/*.txt', flags=glob.NEGATE | glob.GLOBSTAR | glob.NEGATEALL) False ``` By default, `globmatch` and [`globfilter`](#globfilter) do not operate on the file system. This is to allow you to process paths from any source, even paths that are not on your current system. So if you are trying to explicitly match a directory with a pattern such as `*/`, your path must end with a slash (`my_directory/`) to be recognized as a directory. It also won't be able to evaluate whether a directory is a symlink or not as it will have no way of checking. Here we see that `globmatch` fails to match the filepath as the pattern is explicitly looking for a directory and our filepath does not end with `/`. ```pycon3 >>> from wcmatch import glob >>> glob.globmatch('docs', '*/') False ``` If you would like for `globmatch` (or [`globfilter`](#globfilter)) to operate on your current filesystem directly, simply pass in the [`REALPATH`](#realpath) flag. When enabled, the path under consideration will be analyzed and will use that context to determine if the file exists, if it is a directory, does it's context make sense compared to what the pattern is looking vs the current working directory, or if it has symlinks that should not be traversed by [`GLOBSTAR`](#globstar). Here we use [`REALPATH`](#realpath) and can see that `globmatch` now knows that `doc` is a directory. ```pycon3 >>> from wcmatch import glob >>> glob.globmatch('docs', '*/', flags=glob.REALPATH) True ``` It also can tell if a file doesn't exist or is out of scope compared to what is being asked. For instance, the below example fails because the pattern is looking for any folder that is relative to the current path, which `/usr` is not. When we disable [`REALPATH`](#realpath), it will match just fine. Both cases can be useful depending on how you plan to use `globmatch`. ```pycon3 >>> from wcmatch import glob >>> glob.globmatch('/usr', '**/', flags=glob.G | glob.REALPATH) False >>> glob.globmatch('/usr', '**/', flags=glob.G) True ``` If you are using [`REALPATH`](#realpath) and want to evaluate the paths relative to a different directory, you can set the `root_dir` parameter. ```pycon3 >>> from wcmatch import glob >>> glob.globmatch('markdown', 'markdown', flags=glob.REALPATH) False >>> glob.globmatch('markdown', 'markdown', flags=glob.REALPATH, root_dir='docs/src') True ``` Additionally, you could also provide a root directory using a file descriptor. ```pycon3 >>> import os >>> from wcmatch import glob >>> dir_fd = os.open('docs/src', os.O_RDONLY | os.O_DIRECTORY) >>> glob.globmatch('markdown', 'markdown', flags=glob.REALPATH) False >>> glob.globmatch('markdown', 'markdown', flags=glob.REALPATH, dir_fd=dir_fd) True ``` /// warning | Support for Directory Descriptors Directory descriptors may not be supported on all systems. You can check whether or not `dir_fd` is supported for a your platform referencing the attribute `#!py3 glob.SUPPORT_DIR_FD` which will be `#!py3 True` if it is supported. Additionally, the `#!py3 os.O_DIRECTORY` may not be defined on some systems. You can likely just use `#!py3 os.O_RDONLY`. /// /// new | New 5.1 - `root_dir` was added in 5.1.0. - path-like object support for file path inputs was added in 5.1.0 /// /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 8.2 `dir_fd` parameter was added in 8.2. /// /// new | New 8.4 `exclude` parameter was added. /// #### `glob.globfilter` {: #globfilter} ```py3 def globfilter(filenames, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, method=None): ``` `globfilter` takes a list of file paths (strings or path-like objects), a pattern (or list of patterns), flags, and an optional root directory and/or directory file descriptor. It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or a list of patterns.It returns a list of all files paths that matched the pattern(s). The same logic used for [`globmatch`](#globmatch) is used for `globfilter`, albeit more efficient for processing multiple files. /// warning | Path-like Input Support Path-like object input support is only available in Python 3.6+ as the path-like protocol was added in Python 3.6. /// ```pycon3 >>> from wcmatch import glob >>> glob.globfilter(['some/path/a.txt', 'b.txt', 'another/path/c.py'], '**/*.txt') ['some/path/a.txt', 'b.txt'] ``` Like [`globmatch`](#globmatch), `globfilter` does not operate directly on the file system, with all the caveats associated. But you can enable the [`REALPATH`](#realpath) flag and `globfilter` will use the filesystem to gain context such as: whether the file exists, whether it is a directory or not, or whether it has symlinks that should not be traversed by `GLOBSTAR`. See [`globmatch`](#globmatch) for examples. /// new | New 5.1 - `root_dir` was added in 5.1.0. - path-like object support for file path inputs was added in 5.1.0 /// /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 8.2 `dir_fd` parameter was added in 8.2. /// /// new | New 8.4 `exclude` parameter was added. /// #### `glob.translate` {: #translate} ```py3 def translate(patterns, *, flags=0, limit=1000, exclude=None): ``` `translate` takes a file pattern (or list of patterns) and flags. It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or a list of patterns. It returns two lists: one for inclusion patterns and one for exclusion patterns. The lists contain the regular expressions used for matching the given patterns. It should be noted that a file is considered matched if it matches at least one inclusion pattern and matches **none** of the exclusion patterns. ```pycon3 >>> from wcmatch import glob >>> glob.translate('**/*.{py,txt}') (['^(?s:(?=[^/])(?!(?:\\.{1,2})(?:$|[/]))(?:(?!\\.)[^/]*?)?[/]+(?=[^/])(?!(?:\\.{1,2})(?:$|[/]))(?:(?!\\.)[^/]*?)?\\.\\{py,txt\\}[/]*?)$'], []) >>> glob.translate('**|!**/*.{py,txt}', flags=glob.NEGATE | glob.SPLIT) (['^(?s:(?=[^/])(?!(?:\\.{1,2})(?:$|[/]))(?:(?!\\.)[^/]*?)?[/]*?)$'], ['^(?s:(?=[^/])(?!(?:\\.{1,2})(?:$|[/]))[^/]*?[/]+(?=[^/])(?!(?:\\.{1,2})(?:$|[/]))[^/]*?\\.\\{py,txt\\}[/]*?)$']) ``` When using [`EXTGLOB`](#extglob) patterns, patterns will be returned with capturing groups around the groups: While in regex patterns like `#!py3 r'(a)+'` would capture only the last character, even though multiple where matched, we wrap the entire group to be captured: `#!py3 '+(a)'` --> `#!py3 r'((a)+)'`. ```pycon3 >>> from wcmatch import glob >>> import re >>> gpat = glob.translate("@(file)+([[:digit:]])@(.*)", flags=glob.EXTGLOB) >>> pat = re.compile(gpat[0][0]) >>> pat.match('file33.test.txt').groups() ('file', '33', '.test.txt') ``` /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 7.1 Translate patterns now provide capturing groups for [`EXTGLOB`](#extglob) groups. /// /// new | New 8.4 `exclude` parameter was added. /// #### `glob.escape` {: #escape} ```py3 def escape(pattern, unix=None): ``` The `escape` function will conservatively escape `-`, `!`, `*`, `?`, `(`, `)`, `[`, `]`, `|`, `{`, `}`, and `\` with backslashes, regardless of what feature is or is not enabled. It is meant to escape path parts (filenames, Windows drives, UNC sharepoints) or full paths. ```pycon3 >>> from wcmatch import glob >>> glob.escape('some/path?/**file**{}.txt') 'some/path\\?/\\*\\*file\\*\\*\\{}.txt' >>> glob.globmatch('some/path?/**file**{}.txt', glob.escape('some/path?/**file**{}.txt')) True ``` `escape` can also handle Windows style paths with `/` or `\` path separators. It is usually recommended to use `/` as Windows backslashes are only supported via a special escape, but `\` will be expanded to an escaped backslash (represented in a raw string as `#!py3 r'\\'` or a normal string as `#!py3 '\\\\'`). ```pycon3 >>> from wmcatch import glob >>> glob.escape('some\\path?\\**file**{}.txt', unix=False) 'some\\\\path\\?\\\\\\*\\*file\\*\\*\\{\\}.txt' >>> glob.globmatch('some\\path?\\**file**{}.txt', glob.escape('some\\path?\\**file**{}.txt'), flags=glob.FORCEWIN) True >>> glob.escape('some/path?/**file**{}.txt', unix=False) 'some/path\\?/\\*\\*file\\*\\*\\{\\}.txt' >>> glob.globmatch('some\\path?\\**file**{}.txt', glob.escape('some/path?/**file**{}.txt'), flags=glob.FORCEWIN) True ``` On a Windows system, meta characters are not processed in drives or UNC sharepoints except for pattern expansion meta characters. `{` and `}` (when using [`BRACE`](#brace)) and `|` (when using [`SPLIT`](#split)) are the only meta characters that can affect drives and UNC sharepoints; therefore, they are the only characters that need to be escaped. `escape`, when it detects or is informed that it is processing a Windows path, `escape` will properly find and handle drives and UNC sharepoints. ```pycon3 >>> from wmcatch import glob >>> glob.escape('//./Volume{b75e2c83-0000-0000-0000-602f00000000}\Test\Foo.txt', unix=False) '//./Volume\\{b75e2c83-0000-0000-0000-602f00000000\\}\\\\Test\\\\Foo.txt' ``` `escape` will detect the system it is running on and pick Windows escape logic or Linux/Unix logic. Since [`globmatch`](#globmatch) allows you to match Unix style paths on a Windows system and vice versa, you can force Unix style escaping or Windows style escaping via the `unix` parameter. When `unix` is `None`, the escape style will be detected, when `unix` is `True` Linux/Unix style escaping will be used, and when `unix` is `False` Windows style escaping will be used. ```pycon3 >>> glob.escape('some/path?/**file**{}.txt', unix=True) ``` /// new | New 5.0 The `unix` parameter is now `None` by default. Set to `True` to force Linux/Unix style escaping or set to `False` to force Windows style escaping. /// /// new | New 7.0 `{`, `}`, and `|` will be escaped in Windows drives. Additionally, users can escape these characters in Windows drives manually in their match patterns as well. /// ### `glob.is_magic` {: #is_magic} ```py3 def is_magic(pattern, *, flags=0): """Check if the pattern is likely to be magic.""" ``` This checks a given path or `pattern` or to see if "magic" symbols are present or not. The check is based on the enabled features via `flags`. Paths and patterns are expected to be/target full paths, full filenames, full drive names, or full UNC sharepoints. If `is_magic` is run on a Windows path it will always flag it as "magic" unless you convert the directory separators to `/` as `\` is a "magic" symbol. ```pycon3 >>> glob.is_magic('test') False >>> glob.is_magic('[test]ing?') True ``` When `is_magic` is called, the system it is called on is detected automatically and/or inferred from flags such as [`FORCEUNIX`](#forceunix) or [`FORCEWIN`](#forcewin). If the pattern is checked against a Windows system, UNC sharepoints will be detected and treated differently. Wildcard Match cannot detect and glob all possible connected sharepoints, so they are treated differently and cannot contain magic except in three cases: 1. The drive or sharepoint is using backslashes as backslashes are treated as magic. 2. [`BRACE`](#brace) is enabled and either `{` or `}` are found in the drive name or UNC sharepoint. 3. [`SPLIT`](#split) is enabled and `|` is found in the drive name or UNC sharepoint. ```pycon3 >>> glob.is_magic('//?/UNC/server/mount{}/', flags=glob.FORCEWIN) False >>> glob.is_magic('//?/UNC/server/mount{}/', flags=glob.FORCEWIN | glob.BRACE) True ``` The table below illustrates which symbols are searched for based on the given feature. Each feature adds to the "default". In the case of [`NEGATE`](#negate), if [`MINUSNEGATE`](#minusnegate) is also enabled, [`MINUSNEGATE`](#minusnegate)'s symbols will be searched instead of [`NEGATE`](#negate)'s symbols. Features | Symbols ----------------------------- | ------- Default | `?*[]\` [`EXTMATCH`](#extmatch) | `()` [`BRACE`](#brace) | `{}` [`NEGATE`](#negate) | `!` [`MINUSNEGATE`](#minusnegate) | `-` [`SPLIT`](#split) | `|` [`GLOBTILDE`](#globtilde) | `~` /// new | New 8.1 Added `is_magic` in 8.1. /// ## Flags #### `glob.CASE, glob.C` {: #case} `CASE` forces case sensitivity. `CASE` has higher priority than [`IGNORECASE`](#ignorecase). On Windows, drive letters (`C:`) and UNC sharepoints (`//host/share`) portions of a path will still be treated case insensitively, but the rest of the path will have case sensitive logic applied. #### `glob.IGNORECASE, glob.I` {: #ignorecase} `IGNORECASE` forces case insensitivity. [`CASE`](#case) has higher priority than `IGNORECASE`. #### `glob.RAWCHARS, glob.R` {: #rawchars} `RAWCHARS` causes string character syntax to be parsed in raw strings: `#!py3 r'\u0040'` --> `#!py3 r'@'`. This will handle standard string escapes and Unicode including `#!py3 r'\N{CHAR NAME}'`. #### `glob.NEGATE, glob.N` {: #negate} `NEGATE` causes patterns that start with `!` to be treated as exclusion patterns. A pattern of `!*.py` exclude any Python files. Exclusion patterns cannot be used by themselves though, and must be paired with a normal, inclusion pattern, either by utilizing the [`SPLIT`](#split) flag, or providing multiple patterns in a list. Assuming the [`SPLIT`](#split) flag, this means using it in a pattern such as `inclusion|!exclusion`. If it is desired, you can force exclusion patterns, when no inclusion pattern is provided, to assume all files match unless the file matches the excluded pattern. This is done with the [`NEGATEALL`](#negateall) flag. `NEGATE` enables [`DOTGLOB`](#dotglob) in all exclude patterns, this cannot be disabled. This will not affect the inclusion patterns. If `NEGATE` is set and exclusion patterns are passed via a matching or glob function's `exclude` parameter, `NEGATE` will be ignored and the `exclude` patterns will be used instead. Either `exclude` or `NEGATE` should be used, not both. #### `glob.NEGATEALL, glob.A` {: #negateall} `NEGATEALL` can force exclusion patterns, when no inclusion pattern is provided, to assume all files match unless the file matches the excluded pattern. Essentially, it means if you use a pattern such as `!*.md`, it will assume two patterns were given: `**` and `!*.md`, where `!*.md` is applied to the results of `**`, and `**` is specifically treated as if [`GLOBSTAR`](#globstar) was enabled. Dot files will not be returned unless [`DOTGLOB`](#dotglob) is enabled. Symlinks will also not be traversed unless [`FOLLOW`](#follow) is enabled. #### `glob.MINUSNEGATE, glob.M` {: #minusnegate} When `MINUSNEGATE` is used with [`NEGATE`](#negate), exclusion patterns are recognized by a pattern starting with `-` instead of `!`. This plays nice with the extended glob feature which already uses `!` in patterns such as `!(...)`. #### `glob.GLOBSTAR, glob.G` {: #globstar} `GLOBSTAR` enables the feature where `**` matches zero or more directories. #### `glob.GLOBSTARLONG, glob.GL` {: #globstarlong} /// new | New 10.0 /// When `GLOBSTARLONG` is enabled `***` will act like `**`, but will cause symlinks to be traversed as well. Enabling `GLOBSTARLONG` automatically enables [`GLOBSTAR`](#globstar). [`FOLLOW`](#follow) will be ignored and `***` will be required to traverse a symlink. But it should be noted that when using [`MATCHBASE`](#matchbase) and [`FOLLOW`](#follow) with `GLOBSTARLONG`, that [`FOLLOW`](#follow) will cause the implicit leading `**` that [`MATCHBASE`](#matchbase) applies to act as an implicit `***`. #### `glob.FOLLOW, glob.L` {: #follow} `FOLLOW` will cause [`GLOBSTAR`](#globstar) patterns (`**`) to traverse symlink directories. `FOLLOW` will have no affect if using [`GLOBSTARLONG`](#globstarlong) and an explicit `***` will be required to traverse a symlink. `FOLLOW` will have an affect if enabled with [`GLOBSTARLONG`](#globstarlong) and [`MATCHBASE`](#matchbase) and will cause the implicit leading `**` that `MATCHBASE` applies to act as an implicit `***`. #### `glob.REALPATH, glob.P` {: #realpath} In the past, only [`glob`](#glob) and [`iglob`](#iglob) operated on the filesystem, but with `REALPATH`, other functions will now operate on the filesystem as well: [`globmatch`](#globmatch) and [`globfilter`](#globfilter). Normally, functions such as [`globmatch`](#globmatch) would simply match a path with regular expression and return the result. The functions were not concerned with whether the path existed or not. It didn't care if it was even valid for the operating system. `REALPATH` forces [`globmatch`](#globmatch) and [`globfilter`](#globfilter) to treat the string path as a real file path for the given system it is running on. It will augment the patterns used to match files and enable additional logic so that the path must meet the following in order to match: - Path must exist. - Directories that are symlinks will not be traversed by [`GLOBSTAR`](#globstar) patterns (`**`) unless the [`FOLLOW`](#follow) flag is enabled. - If [`GLOBSTARLONG`](#globstarlong) is enabled, `***` will traverse symlinks, [`FOLLOW`](#follow) will be ignored except if [`MATCHBASE`](#matchbase) is also enabled, in that case, the implicit leading `**` added by [`MATCHBASE`](#matchbase) will act as `***`. - When presented with a pattern where the match must be a directory, but the file path being compared doesn't indicate the file is a directory with a trailing slash, the command will look at the filesystem to determine if it is a directory. - Paths must match in relation to the current working directory unless the pattern is constructed in a way to indicates an absolute path. Since `REALPATH` causes the file system to be referenced when matching a path, flags such as [`FORCEUNIX`](#forceunix) and [`FORCEWIN`](#forcewin) are not allowed with this flag and will be ignored. #### `glob.DOTGLOB, glob.D` {: #dotglob} By default, [`glob`](#glob) and [`globmatch`](#globmatch) will not match file or directory names that start with dot `.` unless matched with a literal dot. `DOTGLOB` allows the meta characters (such as `*`) to glob dots like any other character. Dots will not be matched in `[]`, `*`, or `?`. Alternatively `DOTMATCH` will also be accepted for consistency with the other provided libraries. Both flags are exactly the same and are provided as a convenience in case the user finds one more intuitive than the other since `DOTGLOB` is often the name used in Bash. #### `glob.NODOTDIR, glob.Z` {: #globnodotdir} `NOTDOTDIR` fundamentally changes how glob patterns deal with `.` and `..`. This is great if you'd prefer a more Zsh feel when it comes to special directory matching. When `NODOTDIR` is enabled, "magic" patterns, such as `.*`, will not match the special directories of `.` and `..`. In order to match these special directories, you will have to use literal glob patterns of `.` and `..`. This can be used in all glob API functions that accept flags, and will affect inclusion patterns as well as exclusion patterns. ```pycon3 >>> from wcmatch import glob >>> glob.globfilter(['.', '..'], '.*') ['.', '..'] >>> glob.globfilter(['.', '..'], '.*', flags=glob.NODOTDIR) [] >>> glob.globfilter(['.', '..'], '.', flags=glob.NODOTDIR) ['.'] >>> glob.globfilter(['.', '..'], '..', flags=glob.NODOTDIR) ['..'] ``` Also affects exclusion patterns: ```pycon3 >>> from wcmatch import glob >>> glob.glob(['..', '!.*'], flags=glob.NEGATE) [] >>> glob.glob(['..', '!.*'], flags=glob.NEGATE | glob.NODOTDIR) ['..'] >>> glob.glob(['..', '!..'], flags=glob.NEGATE | glob.NODOTDIR) [] ``` /// new | New 7.0 `NODOTDIR` was added in 7.0. /// #### `glob.SCANDOTDIR, glob.SD` {: #scandotdir} `SCANDOTDIR` controls the directory scanning behavior of [`glob`](#glob) and [`iglob`](#iglob). The directory scanner of these functions do not return `.` and `..` in their results. This means that unless you use an explicit `.` or `..` in your glob pattern, `.` and `..` will not be returned. When `SCANDOTDIR` is enabled, `.` and `..` will be returned when a directory is scanned causing "magic" patterns, such as `.*`, to match `.` and `..`. This only controls the directory scanning behavior and not how glob patterns behave. Exclude patterns, which filter the returned results via [`NEGATE`](#negate), can still match `.` and `..` with "magic" patterns such as `.*` regardless of whether `SCANDOTDIR` is enabled or not. It will also have no affect on [`globmatch`](#globmatch). To fundamentally change how glob patterns behave, you can use [`NODOTDIR`](#nodotdir). ```pycon3 >>> from wcmatch import glob >>> glob.glob('.*') ['.codecov.yml', '.tox', '.coverage', '.coveragerc', '.gitignore', '.github', '.pyspelling.yml', '.git'] >>> glob.glob('.*', flags=glob.SCANDOTDIR) ['.', '..', '.codecov.yml', '.tox', '.coverage', '.coveragerc', '.gitignore', '.github', '.pyspelling.yml', '.git'] ``` /// new | New 7.0 `SCANDOTDIR` was added in 7.0. /// #### `glob.EXTGLOB, glob.E` {: #extglob} `EXTGLOB` enables extended pattern matching which includes special pattern lists such as `+(...)`, `*(...)`, `?(...)`, etc. Pattern lists allow for multiple patterns within them separated by `|`. See the globbing [syntax overview](#syntax) for more information. Alternatively `EXTMATCH` will also be accepted for consistency with the other provided libraries. Both flags are exactly the same and are provided as a convenience in case the user finds one more intuitive than the other since `EXTGLOB` is often the name used in Bash. /// tip | EXTGLOB and NEGATE When using `EXTGLOB` and [`NEGATE`](#negate) together, if a pattern starts with `!(`, the pattern will not be treated as a [`NEGATE`](#negate) pattern (even if `!(` doesn't yield a valid `EXTGLOB` pattern). To negate a pattern that starts with a literal `(`, you must escape the bracket: `!\(`. /// #### `glob.BRACE, glob.B` {: #brace} `BRACE` enables Bash style brace expansion: `a{b,{c,d}}` --> `ab ac ad`. Brace expansion is applied before anything else. When applied, a pattern will be expanded into multiple patterns. Each pattern will then be parsed separately. Duplicate patterns will be discarded[^1] by default, and `glob` and `iglob` will return only unique results. If you need [`glob`](#glob) or [`iglob`](#iglob) to behave more like Bash and return all results, you can set [`NOUNIQUE`](#nounique). [`NOUNIQUE`](#nounique) has no effect on matching functions such as [`globmatch`](#globmatch) and [`globfilter`](#globfilter). For simple patterns, it may make more sense to use [`EXTGLOB`](#extglob) which will only generate a single pattern which will perform much better: `@(ab|ac|ad)`. /// warning | Massive Expansion Risk 1. It is important to note that each pattern is crawled separately, so patterns such as `{1..100}` would generate **one hundred** patterns. In a match function ([`globmatch`](#globmatch)), that would cause a hundred compares, and in a file crawling function ([`glob`](#glob)), it would cause the file system to be crawled one hundred times. Sometimes patterns like this are needed, so construct patterns thoughtfully and carefully. 2. `BRACE` and [`SPLIT`](#split) both expand patterns into multiple patterns. Using these two syntaxes simultaneously can exponential increase duplicate patterns: ```pycon3 >>> expand('test@(this{|that,|other})|*.py', BRACE | SPLIT | EXTMATCH) ['test@(this|that)', 'test@(this|other)', '*.py', '*.py'] ``` This effect is reduced as redundant, identical patterns are optimized away[^1], but when using crawling functions (like [`glob`](#glob)) *and* [`NOUNIQUE`](#nounique) that optimization is removed, and all of those patterns will be crawled. For this reason, especially when using functions like [`glob`](#glob), it is recommended to use one syntax or the other. /// [^1]: Identical patterns are only reduced by comparing case sensitively as POSIX character classes are case sensitive: `[[:alnum:]]` =/= `[[:ALNUM:]]`. #### `glob.SPLIT, glob.S` {: #split} `SPLIT` is used to take a string of multiple patterns that are delimited by `|` and split them into separate patterns. This is provided to help with some interfaces that might need a way to define multiple patterns in one input. It pairs really well with [`EXTGLOB`](#extglob) and takes into account sequences (`[]`) and extended patterns (`*(...)`) and will not parse `|` within them. You can also escape the delimiters if needed: `\|`. Duplicate patterns will be discarded[^1] by default, and `glob` and `iglob` will return only unique results. If you need [`glob`](#glob) or [`iglob`](#iglob) to behave more like Bash and return all results, you can set [`NOUNIQUE`](#nounique). [`NOUNIQUE`](#nounique) has no effect on matching functions such as [`globmatch`](#globmatch) and [`globfilter`](#globfilter). While `SPLIT` is not as powerful as [`BRACE`](#brace), it's syntax is very easy to use, and when paired with [`EXTGLOB`](#extglob), it feels natural and comes a bit closer. It is also much harder to create massive expansions of patterns with it, except when paired *with* [`BRACE`](#brace). See [`BRACE`](#brace) and its warnings related to pairing it with `SPLIT`. ```pycon3 >>> from wcmatch import glob >>> glob.globmatch('test.txt', '*.txt|*.py', flags=fnmatch.SPLIT) True >>> glob.globmatch('test.py', '*.txt|*.py', flags=fnmatch.SPLIT) True ``` #### `glob.NOUNIQUE, glob.Q` {: #nounique} `NOUNIQUE` is used to disable Wildcard Match's unique results return. This mimics Bash's output behavior if that is desired. ```pycon3 >>> from wcmatch import glob >>> glob.glob('{*,README}.md', flags=glob.BRACE | glob.NOUNIQUE) ['LICENSE.md', 'README.md', 'README.md'] >>> glob.glob('{*,README}.md', flags=glob.BRACE ) ['LICENSE.md', 'README.md'] ``` By default, only unique paths are returned in [`glob`](#glob) and [`iglob`](#iglob). Normally this is what a programmer would want from such a library, so input patterns are reduced to unique patterns[^1] to reduce excessive matching with redundant patterns and excessive crawls through the file system. Also, as two different patterns that have been fed into [`glob`](#glob) may match the same file, the results are also filtered as to not return the duplicates. Unique results is are accomplished by filtering out duplicate patterns and by retaining an internal set of returned files to determine duplicates. The internal set of files is not retained if only a single, inclusive pattern is provided. Exclusive patterns via [`NEGATE`](#negate) will not trigger the logic. Singular inclusive patterns that use pattern expansions due to [`BRACE`](#brace) or [`SPLIT`](#split) will act as if multiple patterns were provided, and will trigger the duplicate filtering logic. This is mentioned as functions such as [`iglob`](#iglob), which normally are expected to not retain results in memory, will be forced to retain a set to ensure unique results if multiple inclusive patterns are provided. `NOUNIQUE` disables all of the aforementioned "unique" optimizations, but only for [`glob`](#glob) and [`iglob`](#iglob). Functions like [`globmatch`](#globmatch) and [`globfilter`](#globfilter) would get no benefit from disabling "unique" optimizations as they only match what they are given. /// new | New in 6.0 "Unique" optimizations were added in 6.0, along with `NOUNIQUE`. /// #### `glob.GLOBTILDE, glob.T` {: #globtilde} `GLOBTILDE` allows for user path expansion via `~`. You can get the current user path by using `~` at the start of a path. `~` can be used as the entire pattern, or it must be followed by a directory slash: `~/more-pattern`. To specify a specific user, you can explicitly specify a user name via `~user`. If additional pattern is needed, the user name must be followed by a directory slash: `~user/more-pattern`. ```pycon3 >>> from wcmatch import glob >>> glob.glob('~', flags=glob.GLOBTILDE) ['/home/facelessuser'] >>> glob.glob('~root', flags=glob.GLOBTILDE) ['/root'] ``` `GLOBTILDE` can also be used in things like [`globfilter`](#globfilter) or [`globmatch`](#globmatch), but you must be using [`REALPATH`](#realpath) or the user path will not be expanded. ```pycon3 from wcmatch import glob >>> glob.globmatch('/home/facelessuser/', '~', flags=glob.GLOBTILDE | glob.REALPATH) True ``` /// new | New 6.0 Tilde expansion with `GLOBTILDE` was added in version 6.0. /// #### `glob.MARK, glob.K` {: #mark} `MARK` ensures that [`glob`](#glob) and [`iglob`](#iglob) to return all directories with a trailing slash. This makes it very clear which paths are directories and allows you to save calling `os.path.isdir` as you can simply check for a path separator at the end of the path. This flag only applies to calls to `glob` or `iglob`. If you are passing the returned files from `glob` to [`globfilter`](#globfilter) or [`globmatch`](#globmatch), it is important to ensure directory paths have trailing slashes as these functions have no way of telling the path is a directory otherwise (except when [`REALPATH`](#realpath) is enabled). If you have [`REALPATH`](#realpath) enabled, ensuring the files have trailing slashes can still save you a call to `os.path.isdir` as [`REALPATH`](#realpath) resorts to calling it if there is no trailing slash. ```pycon3 >>> from wcmatch import glob >>> glob.glob('*', flags=glob.MARK) ['appveyor.yml', 'base.patch', 'basematch.diff', 'docs/', 'LICENSE.md', 'MANIFEST.in', 'mkdocs.yml', 'README.md', 'requirements/', 'setup.cfg', 'setup.py', 'tests/', 'tools/', 'tox.ini', 'wcmatch/'] >>> glob.glob('*') ['appveyor.yml', 'base.patch', 'basematch.diff', 'docs', 'LICENSE.md', 'MANIFEST.in', 'mkdocs.yml', 'README.md', 'requirements', 'setup.cfg', 'setup.py', 'tests', 'tools', 'tox.ini', 'wcmatch'] ``` #### `glob.MATCHBASE, glob.X` {: #matchbase} `MATCHBASE`, when a pattern has no slashes in it, will cause [`glob`](#glob) and [`iglob`](#iglob) to seek for any file anywhere in the tree with a matching basename. When enabled for [`globfilter`](#globfilter) and [`globmatch`](#globmatch), any path whose basename matches. `MATCHBASE` is sensitive to files and directories that start with `.` and will not match such files and directories if [`DOTGLOB`](#dotglob) is not enabled. ```pycon3 >>> from wcmatch import glob >>> glob.glob('*.txt', flags=glob.MATCHBASE) ['docs/src/dictionary/en-custom.txt', 'docs/src/markdown/_snippets/abbr.txt', 'docs/src/markdown/_snippets/links.txt', 'docs/src/markdown/_snippets/posix.txt', 'docs/src/markdown/_snippets/refs.txt', 'requirements/docs.txt', 'requirements/lint.txt', 'requirements/setup.txt', 'requirements/test.txt', 'requirements/tools.txt'] ``` #### `glob.NODIR, glob.O` {: #nodir} `NODIR` will cause [`glob`](#glob), [`iglob`](#iglob), [`globmatch`](#globmatch), and [`globfilter`](#globfilter) to return only matched files. ```pycon3 >>> from wcmatch import glob >>> glob.glob('*', flags=glob.NODIR) ['appveyor.yml', 'LICENSE.md', 'MANIFEST.in', 'mkdocs.yml', 'README.md', 'setup.cfg', 'setup.py', 'spell.log', 'tox.ini'] >>> glob.glob('*') ['appveyor.yml', 'docs', 'LICENSE.md', 'MANIFEST.in', 'mkdocs.yml', 'README.md', 'requirements', 'setup.cfg', 'setup.py', 'spell.log', 'tests', 'tools', 'tox.ini', 'wcmatch'] ``` #### `glob.FORCEWIN, glob.W` {: #forcewin} `FORCEWIN` will force Windows path and case logic to be used on Linux/Unix systems. It will also cause slashes to be normalized and Windows drive syntax to be handled special. This is great if you need to match Windows specific paths on a Linux/Unix system. This will only work on commands that do not access the file system: [`translate`](#translate), [`globmatch`](#globmatch), [`globfilter`](#globfilter), etc. These flags will not work with [`glob`](#glob) or [`iglob`](#iglob). It also will not work when using the [`REALPATH`](#realpath) flag with things like [`globmatch`](#globmatch) and [`globfilter`](#globfilter). If `FORCEWIN` is used along side [`FORCEUNIX`](#forceunix), both will be ignored. #### `glob.FORCEUNIX, glob.U` {: #forceunix} `FORCEUNIX` will force Linux/Unix path and case logic to be used on Windows systems. This is great if you need to match Linux/Unix specific paths on a Windows system. This will only work on commands that do not access the file system: [`translate`](#translate), [`globmatch`](#globmatch), [`globfilter`](#globfilter), etc. These flags will not work with [`glob`](#glob) or [`iglob`](#iglob). It also will not work when using the [`REALPATH`](#realpath) flag with things like [`globmatch`](#globmatch) and [`globfilter`](#globfilter). When using `FORCEUNIX`, the paths are assumed to be case sensitive, but you can use [`IGNORECASE`](#ignorecase) to use case insensitivity. If `FORCEUNIX` is used along side [`FORCEWIN`](#forcewin), both will be ignored. wcmatch-10.0/docs/src/markdown/index.md000066400000000000000000000060511467532413500200660ustar00rootroot00000000000000# Wildcard Match ## Overview Wildcard Match provides an enhanced [`fnmatch`](./fnmatch.md), [`glob`](./glob.md), and [`pathlib`](./pathlib.md) library in order to provide file matching and globbing that more closely follows the features found in Bash. In some ways these libraries are similar to Python's builtin libraries as they provide a similar interface to match, filter, and glob the file system. But they also include a number of features found in Bash's globbing such as backslash escaping, brace expansion, extended glob pattern groups, etc. They also add a number of new useful functions as well, such as [`globmatch`](./glob.md#globmatch) which functions like [`fnmatch`](./fnmatch.md#fnmatch), but for paths. Wildcard Match also adds a file search utility called [`wcmatch`](./wcmatch.md) that is built on top of [`fnmatch`](./fnmatch.md#fnmatch) and [`globmatch`](./glob.md#globmatch). It was originally written for [Rummage](https://github.com/facelessuser/Rummage), but split out into this project to be used by other projects that may find its approach useful. Bash is used as a guide when making decisions on behavior for [`fnmatch`](./fnmatch.md) and [`glob`](./glob.md). Behavior may differ from Bash version to Bash version, but an attempt is made to keep Wildcard Match up with the latest relevant changes. With all of this said, there may be a few corner cases in which we've intentionally chosen to not *exactly* mirror Bash. If an issue is found where Wildcard Match seems to deviate in an illogical way, we'd love to hear about it in the [issue tracker][issues]. ## Features A quick overview of Wildcard Match's Features: - Provides an interface comparable to Python's builtin in [`fnmatch`][fnmatch], [`glob`][glob], and [`pathlib`][pathlib]. - Allows for a much more configurable experience when matching or globbing with many more features. - Adds support for `**` in glob. - Adds support for Zsh style `***` recursive glob for symlinks. - Adds support for escaping characters with `\`. - Add support for POSIX style character classes inside sequences: `[[:alnum:]]`, etc. The `C` locale is used. - Adds support for brace expansion: `a{b,{c,d}}` --> `ab ac ad`. - Adds support for expanding `~` or `~username` to the appropriate user path. - Adds support for extended match patterns: `@(...)`, `+(...)`, `*(...)`, `?(...)`, and `!(...)`. - Adds ability to match path names via the path centric `globmatch`. - Provides a [`pathlib`][pathlib] variant that uses Wildcard Match's `glob` library instead of Python's default. - Provides an alternative file crawler called `wcmatch`. - And more... ## Installation Installation is easy with pip: ```console $ pip install wcmatch ``` ## Libraries - [`fnmatch`](./fnmatch.md): A file name matching library. - [`glob`](./glob.md): A file system searching and file path matching library. - [`pathlib`](./pathlib.md): A implementation of Python's `pathlib` that uses our own `glob` implementation. - [`wcmatch`](./wcmatch.md): An alternative file search library built on `fnmatch` and `globmatch`. wcmatch-10.0/docs/src/markdown/pathlib.md000066400000000000000000001065071467532413500204110ustar00rootroot00000000000000# `wcmatch.pathlib` ```py3 from wcmatch import pathlib ``` /// new | New 5.0 `wcmatch.pathlib` was added in `wcmatch` 5.0. /// ## Overview `pathlib` is a library that contains subclasses of Python's [`pathlib`][pathlib] `Path` and `PurePath` classes, and their Posix and Windows subclasses, with the purpose of overriding the default `glob` behavior with Wildcard Match's very own [`glob`](./glob.md). This allows a user of `pathlib` to use all of the glob enhancements that Wildcard Match provides. This includes features such as extended glob patterns, brace expansions, and more. This documentation does not mean to exhaustively describe the [`pathlib`][pathlib] library, just the differences introduced by Wildcard Match's implementation. Please check out Python's [`pathlib`][pathlib] documentation to learn more about [`pathlib`][pathlib] in general. Also, to learn more about the underlying glob library being used, check out the documentation for Wildcard Match's [`glob`](./glob.md). ## Multi-Pattern Limits Many of the API functions allow passing in multiple patterns or using either [`BRACE`](#brace) or [`SPLIT`](#split) to expand a pattern in to more patterns. The number of allowed patterns is limited `1000`, but you can raise or lower this limit via the keyword option `limit`. If you set `limit` to `0`, there will be no limit. /// new | New 6.0 The imposed pattern limit and corresponding `limit` option was introduced in 6.0. /// ### Differences The API is the same as Python's default [`pathlib`][pathlib] except for the few differences related to file globbing and matching: - Each `pathlib` object's [`glob`](#glob), [`rglob`](#rglob), and [`match`](#match) methods are now driven by the [`wcmatch.glob`](./glob.md) library. As a result, some of the defaults and accepted parameters are different. Also, many new optional features can be enabled via [flags](#flags). - [`glob`](#glob), [`rglob`](#rglob), and [`match`](#match) can take a single string pattern or a list of patterns. They also accept [flags](#flags) via the `flags` keyword. This matches the interfaces found detailed in our [`glob`](./glob.md) documentation. - [`glob`](#glob), [`rglob`](#rglob), and [`match`](#match) do not enable [`GLOBSTAR`](#globstar) or [`DOTGLOB`](#dotglob) by default. These flags must be passed in to take advantage of this functionality. - A [`globmatch`](#globmatch) function has been added to `PurePath` classes (and `Path` classes which are derived from `PurePath`) which is like [`match`](#match) except performs a "full" match. Python 3.13 added a similar function called [`full_match`](#full_match) which came long after our [`globmatch`](#globmatch) support was added. In recent versions we've also added [`full_match`](#full_match) as an alias to our [`globmatch`](#globmatch) function. See [`match`](#match), [`globmatch`](#globmatch), and [`full_match`](#full_match) for more information. - If file searching methods ([`glob`](#glob) and [`rglob`](#rglob)) are given multiple patterns, they will ensure duplicate results are filtered out. This only occurs when more than one inclusive pattern is given, or a pattern is expanded into multiple, inclusive patterns via [`BRACE`](#brace) or [`SPLIT`](#split). When this occurs, an internal set is kept to track the results returned so that duplicates can be filtered. This will not occur if only a single, inclusive pattern is given or the [`NOUNIQUE`](#nounique) flag is specified. - Python's [`pathlib`][pathlib] has logic to ignore `.` when used as a directory in both the file path and glob pattern. We do not alter how [`pathlib`][pathlib] stores paths, but our implementation allows explicit use of `.` as a literal directory and will match accordingly. With that said, since [`pathlib`][pathlib] normalizes paths by removing `.` directories, in most cases, you won't notice the difference, except when it comes to a path that is literally just `.`. Python's default glob: ```pycon3 >>> import pathlib >>> list(pathlib.Path('.').glob('docs/./src')) [PosixPath('docs/src')] ``` Ours: ```pycon3 >>> form wcmatch import pathlib >>> list(pathlib.Path('.').glob('docs/./src')) [PosixPath('docs/src')] ``` Python's default glob: ```pycon3 >>> import pathlib >>> pathlib.Path('.').match('.') Traceback (most recent call last): File "", line 1, in File "/usr/local/Cellar/python@3.8/3.8.3/Frameworks/Python.framework/Versions/3.8/lib/python3.8/pathlib.py", line 976, in match raise ValueError("empty pattern") ValueError: empty pattern ``` Ours: ```pycon3 >>> from wcmatch import pathlib >>> pathlib.Path('.').match('.') True ``` ### Similarities - [`glob`](#glob), [`rglob`](#rglob), and [`match`](#match) should mimic the basic behavior of Python's original [`pathlib`][pathlib] library, just with the enhancements and configurability that Wildcard Match's [`glob`](./glob.md) provides. - [`glob`](#glob) and [`rglob`](#rglob) will yield an iterator of the results. - [`rglob`](#rglob) will exhibit the same *recursive* behavior. - [`match`](#match) will match using the same *recursive* behavior as [`rglob`](#rglob). ## Classes #### `pathlib.PurePath` {: #purepath} `PurePath` is Wildcard Match's version of Python's `PurePath` class. Depending on the system, it will create either a [`PureWindowsPath`](#purewindowspath) or a [`PurePosixPath`](#pureposixpath) object. Both objects will utilize [`wcmatch.glob`](./glob.md) for all glob related actions. `PurePath` objects do **not** touch the filesystem. They include the methods [`match`](#match) and [`globmatch`](#globmatch) (amongst others). You can force the path to access the filesystem if you give either function the [`REALPATH`](#realpath) flag. We do not restrict this, but we do not enable it by default. [`REALPATH`](#realpath) simply forces the match to check the filesystem to see if the file exists and is a directory or not. ```pycon3 >>> from wcmatch import pathlib >>> pathlib.PurePath('docs/src') PurePosixPath('docs/src') ``` `PurePath` classes implement the [`match`](#match) and [`globmatch`](#globmatch) methods: ```pycon3 >>> from wcmatch import pathlib >>> p = pathlib.PurePath('docs/src') >>> p.match('src') True >>> p.globmatch('**/src', flags=pathlib.GLOBSTAR) True ``` #### `pathlib.PureWindowsPath` {: #purewindowspath} `PureWindowsPath` is Wildcard Match's version of Python's `PureWindowsPath`. The `PureWindowsPath` class is useful if you'd like to have the ease that `pathlib` offers when working with a path, but don't want it to access the filesystem. This is also useful if you'd like to manipulate Windows path strings on a Posix system. This class will utilize Wildcard Match's [`glob`](./glob.md) for all glob related actions. The class is subclassed from [`PurePath`](#purepath). ```pycon3 >>> from wcmatch import pathlib >>> os.name 'posix' >>> pathlib.PureWindowsPath('c:/some/path') PureWindowsPath('c:/some/path') ``` #### `pathlib.PurePosixPath` {: #pureposixpath} `PurePosixPath` is Wildcard Match's version of Python's `PurePosixPath`. The `PurePosixPath` class is useful if you'd like to have the ease that `pathlib` offers when working with a path, but don't want it to access the filesystem. This is also useful if you'd like to manipulate Posix path strings on a Windows system. This class will utilize Wildcard Match's [`glob`](./glob.md) for all glob related actions. The class is subclassed from [`PurePath`](#purepath). ```pycon3 >>> from wcmatch import pathlib >>> os.name 'nt' >>> pathlib.PureWindowsPath('/usr/local/bin') PurePosixPath('/usr/local/bin') ``` #### `pathlib.Path` {: #path} `Path` is Wildcard Match's version of Python's `Path` class. Depending on the system, it will create either a [`WindowsPath`](#windowspath) or a [`PosixPath`](#posixpath) object. Both objects will utilize [`wcmatch.glob`](./glob.md) for all glob related actions. `Path` classes are subclassed from the [`PurePath`](#purepath) objects, so you get all the features of the `Path` class in addition to the [`PurePath`](#purepath) class features. `Path` objects have access to the filesystem. They include the [`PurePath`](#purepath) methods [`match`](#match) and [`globmatch`](#globmatch) (amongst others). Since these methods are [`PurePath`](#purepath) methods, they do not touch the filesystem. But, you can force them to access the filesystem if you give either function the [`REALPATH`](#realpath) flag. We do not restrict this, but we do not enable it by default. [`REALPATH`](#realpath) simply forces the match to check the filesystem to see if the file exists and is a directory or not. ```pycon3 >>> from wcmatch import pathlib >>> pathlib.PurePath('docs/src') PosixPath('docs/src') ``` `Path` classes implement the [`glob`](#glob) and [`globmatch`](#rglob) methods: ```pycon3 >>> from wcmatch import pathlib >>> p = pathlib.Path('docs/src') >>> p.match('src') True >>> p.globmatch('**/src', flags=pathlib.GLOBSTAR) True >>> list(p.glob('**/*.txt', flags=pathlib.GLOBSTAR)) [PosixPath('docs/src/dictionary/en-custom.txt'), PosixPath('docs/src/markdown/_snippets/links.txt'), PosixPath('docs/src/markdown/_snippets/refs.txt'), PosixPath('docs/src/markdown/_snippets/abbr.txt'), PosixPath('docs/src/markdown/_snippets/posix.txt')] >>> list(p.rglob('*.txt')) [PosixPath('docs/src/dictionary/en-custom.txt'), PosixPath('docs/src/markdown/_snippets/links.txt'), PosixPath('docs/src/markdown/_snippets/refs.txt'), PosixPath('docs/src/markdown/_snippets/abbr.txt'), PosixPath('docs/src/markdown/_snippets/posix.txt')] ``` #### `pathlib.WindowsPath` {: #windowspath} `WindowsPath` is Wildcard Match's version of Python's `WindowsPath`. The `WindowsPath` class is useful if you'd like to have the ease that `pathlib` offers when working with a path and be able to manipulate or gain access to to information about that file. You cannot instantiate this class on a Posix system. This class will utilize Wildcard Match's [`glob`](./glob.md) for all glob related actions. The class is subclassed from [`Path`](#path). ```pycon3 >>> from wcmatch import pathlib >>> os.name 'posix' >>> pathlib.Path('c:/some/path') WindowsPath('c:/some/path') ``` #### `pathlib.PosixPath` {: #posixpath} `PosixPath` is Wildcard Match's version of Python's `PosixPath`. The `PosixPath` class is useful if you'd like to have the ease that `pathlib` offers when working with a path and be able to manipulate or gain access to to information about that file. You cannot instantiate this class on a Windows system. This class will utilize Wildcard Match's [`glob`](./glob.md) for all glob related actions. The class is subclassed from [`Path`](#path). ```pycon3 >>> from wcmatch import pathlib >>> os.name 'posix' >>> pathlib.Path('/usr/local/bin') PosixPath('/usr/local/bin') ``` ## Methods #### `PurePath.match` {: #match} ```py3 def match(self, patterns, *, flags=0, limit=1000, exclude=None): ``` `match` takes a pattern (or list of patterns), and flags. It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or a list of patterns. It will return a boolean indicating whether the object's file path was matched by the pattern(s). `match` mimics Python's `pathlib` version of `match`. Python's `match` uses a right to left evaluation that behaves like [`rglob`](#rglob) but as a matcher instead of a globbing function. Wildcard Match emulates this behavior as well. What this means is that when provided with a path `some/path/name`, the patterns `name`, `path/name` and `some/path/name` will all match. Essentially, it matches what [`rglob`](#rglob) returns. `match` does not access the filesystem, but you can force the path to access the filesystem if you give it the [`REALPATH`](#realpath) flag. We do not restrict this, but we do not enable it by default. [`REALPATH`](#realpath) simply forces the match to check the filesystem to see if the file exists, if it is a directory or not, and whether it is a symlink. Since [`Path`](#path) is derived from [`PurePath`](#purepath), this method is also available in [`Path`](#path) objects. ```pycon3 >>> from wcmatch import pathlib >>> p = pathlib.PurePath('docs/src') >>> p.match('src') True ``` /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 8.4 `exclude` parameter was added. /// #### `PurePath.globmatch` {: #globmatch} ```py3 def globmatch(self, patterns, *, flags=0, limit=1000, exclude=None): ``` `globmatch` takes a pattern (or list of patterns), and flags. It also allows configuring the [max pattern limit](#multi-pattern-limits). Exclusion patterns can be specified via the `exclude` parameter which takes a pattern or a list of patterns. It will return a boolean indicating whether the objects file path was matched by the pattern(s). `globmatch` is similar to [`match`](#match) except it does not use the same recursive logic that [`match`](#match) does. In all other respects, it behaves the same. `globmatch` does not access the filesystem, but you can force the path to access the filesystem if you give it the [`REALPATH`](#realpath) flag. We do not restrict this, but we do not enable it by default. [`REALPATH`](#realpath) simply forces the match to check the filesystem to see if the file exists, if it is a directory or not, and whether it is a symlink. Since [`Path`](#path) is derived from [`PurePath`](#purepath), this method is also available in [`Path`](#path) objects. ```pycon3 >>> from wcmatch import pathlib >>> p = pathlib.PurePath('docs/src') >>> p.globmatch('**/src', flags=pathlib.GLOBSTAR) True ``` /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 8.4 `exclude` parameter was added. /// #### `PurePath.full_match` {: #full_match} /// new | new 10.0 /// ```py3 def full_match(self, patterns, *, flags=0, limit=1000, exclude=None): ``` Python 3.13 added the new `full_match` method to `PurePath` objects. Essentially, this does for normal `pathlib` what our existing `PurePath.globmatch` has been doing prior to Python 3.13. We've added an alias for `PurePath.full_match` that redirects to [`PurePath.globmatch`](#globmatch) for completeness. #### `Path.glob` {: #glob} ```py3 def glob(self, patterns, *, flags=0, limit=1000, exclude=None): ``` `glob` takes a pattern (or list of patterns) and flags. It also allows configuring the [max pattern limit](#multi-pattern-limits). It will crawl the file system, relative to the current [`Path`](#path) object, returning a generator of [`Path`](#path) objects. If a file/folder matches any regular, inclusion pattern, it is considered a match. If a file matches *any* exclusion pattern (specified via `exclude` or using negation patterns when enabling the [`NEGATE`](#negate) flag), then it will not be returned. This method calls our own [`iglob`](./glob.md#iglob) implementation, and as such, should behave in the same manner in respect to features, the one exception being that instead of returning path strings in the generator, it will return [`Path`](#path) objects. The one difference between this `glob` and the [`iglob`](./glob.md#iglob) API is that this function does not accept the `root_dir` parameter. All searches are relative to the object's path, which is evaluated relative to the current working directory. ```pycon3 >>> from wcmatch import pathlib >>> p = pathlib.Path('docs/src') >>> list(p.glob('**/*.txt', flags=pathlib.GLOBSTAR)) [PosixPath('docs/src/dictionary/en-custom.txt'), PosixPath('docs/src/markdown/_snippets/links.txt'), PosixPath('docs/src/markdown/_snippets/refs.txt'), PosixPath('docs/src/markdown/_snippets/abbr.txt'), PosixPath('docs/src/markdown/_snippets/posix.txt')] ``` /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 8.4 `exclude` parameter was added. /// #### `Path.rglob` {: #rglob} ```py3 def rglob(self, patterns, *, flags=0, path_limit=1000, exclude=None): ``` `rglob` takes a pattern (or list of patterns) and flags. It also allows configuring the [max pattern limit](#multi-pattern-limits). It will crawl the file system, relative to the current [`Path`](#path) object, returning a generator of [`Path`](#path) objects. If a file/folder matches any regular patterns, it is considered a match. If a file matches *any* exclusion pattern (specified via `exclude` or using negation patterns when enabling the [`NEGATE`](#negate) flag), then it will be not be returned. `rglob` mimics Python's [`pathlib`][pathlib] version of `rglob` in that it uses a recursive logic. What this means is that when you are matching a path in the form `some/path/name`, the patterns `name`, `path/name` and `some/path/name` will all match. Essentially, the pattern behaves as if a [`GLOBSTAR`](#globstar) pattern of `**/` was added at the beginning of the pattern. `rglob` is similar to [`glob`](#glob) except for the use of recursive logic. In all other respects, it behaves the same. ```pycon3 >>> from wcmatch import pathlib >>> p = pathlib.Path('docs/src') >>> list(p.rglob('*.txt')) [PosixPath('docs/src/dictionary/en-custom.txt'), PosixPath('docs/src/markdown/_snippets/links.txt'), PosixPath('docs/src/markdown/_snippets/refs.txt'), PosixPath('docs/src/markdown/_snippets/abbr.txt'), PosixPath('docs/src/markdown/_snippets/posix.txt')] ``` /// new | New 6.0 `limit` was added in 6.0. /// /// new | New 8.4 `exclude` parameter was added. /// ## Flags #### `pathlib.CASE, pathlib.C` {: #case} `CASE` forces case sensitivity. `CASE` has higher priority than [`IGNORECASE`](#ignorecase). On Windows, drive letters (`C:`) and UNC sharepoints (`//host/share`) portions of a path will still be treated case insensitively, but the rest of the path will have case sensitive logic applied. #### `pathlib.IGNORECASE, pathlib.I` {: #ignorecase} `IGNORECASE` forces case insensitivity. [`CASE`](#case) has higher priority than `IGNORECASE`. #### `glob.RAWCHARS, glob.R` {: #rawchars} `RAWCHARS` causes string character syntax to be parsed in raw strings: `#!py3 r'\u0040'` --> `#!py3 r'@'`. This will handle standard string escapes and Unicode including `#!py3 r'\N{CHAR NAME}'`. #### `pathlib.NEGATE, pathlib.N` {: #negate} `NEGATE` causes patterns that start with `!` to be treated as exclusion patterns. A pattern of `!*.py` would exclude any Python files. Exclusion patterns cannot be used by themselves though, and must be paired with a normal, inclusion pattern, either by utilizing the [`SPLIT`](#split) flag, or providing multiple patterns in a list. Assuming the [`SPLIT`](#split) flag, this means using it in a pattern such as `inclusion|!exclusion`. If it is desired, you can force exclusion patterns, when no inclusion pattern is provided, to assume all files match unless the file matches the excluded pattern. This is done with the [`NEGATEALL`](#negateall) flag. `NEGATE` enables [`DOTGLOB`](#dotglob) in all exclude patterns, this cannot be disabled. This will not affect the inclusion patterns. If `NEGATE` is set and exclusion patterns are passed via a matching or glob function's `exclude` parameter, `NEGATE` will be ignored and the `exclude` patterns will be used instead. Either `exclude` or `NEGATE` should be used, not both. #### `pathlib.NEGATEALL, pathlib.A` {: #negateall} `NEGATEALL` can force exclusion patterns, when no inclusion pattern is provided, to assume all files match unless the file matches the excluded pattern. Essentially, it means if you use a pattern such as `!*.md`, it will assume two patterns were given: `**` and `!*.md`, where `!*.md` is applied to the results of `**`, and `**` is specifically treated as if [`GLOBSTAR`](#globstar) was enabled. Dot files will not be returned unless [`DOTGLOB`](#dotglob) is enabled. Symlinks will also be ignored in the return unless [`FOLLOW`](#follow) is enabled. #### `pathlib.MINUSNEGATE, pathlib.M` {: #minusnegate} When `MINUSNEGATE` is used with [`NEGATE`](#negate), exclusion patterns are recognized by a pattern starting with `-` instead of `!`. This plays nice with the extended glob feature which already uses `!` in patterns such as `!(...)`. #### `pathlib.GLOBSTAR, pathlib.G` {: #globstar} `GLOBSTAR` enables the feature where `**` matches zero or more directories. #### `glob.GLOBSTARLONG, glob.GL` {: #globstarlong} /// new | New 10.0 /// When `GLOBSTARLONG` is enabled `***` will act like `**`, but will cause symlinks to be traversed as well. Enabling `GLOBSTARLONG` automatically enables [`GLOBSTAR`](#globstar). [`FOLLOW`](#follow) will be ignored and `***` will be required to traverse a symlink. But it should be noted that when using [`MATCHBASE`](#matchbase) and [`FOLLOW`](#follow) with `GLOBSTARLONG`, that [`FOLLOW`](#follow) will cause the implicit leading `**` that [`MATCHBASE`](#matchbase) applies to act as an implicit `***`. #### `pathlib.FOLLOW, pathlib.L` {: #follow} `FOLLOW` will cause `GLOBSTAR` patterns (`**`) to match and traverse symlink directories. `FOLLOW` will have no affect if using [`GLOBSTARLONG`](#globstarlong) and an explicit `***` will be required to traverse a symlink. `FOLLOW` will have an affect if enabled with [`GLOBSTARLONG`](#globstarlong) and [`MATCHBASE`](#matchbase) and will cause the implicit leading `**` that `MATCHBASE` applies to act as an implicit `***`. #### `pathlib.REALPATH, pathlib.P` {: #realpath} In the past, only `glob` and `iglob` operated on the filesystem, but with `REALPATH`, other functions will now operate on the filesystem as well: [`globmatch`](#globmatch) and [`match`](#match). Normally, functions such as [`globmatch`](#globmatch) would simply match a path with regular expression and return the result. The functions were not concerned with whether the path existed or not. It didn't care if it was even valid for the operating system. `REALPATH` forces [`globmatch`](#globmatch) and [`match`](#match) to treat the path as a real file path for the given system it is running on. It will augment the patterns used to match files and enable additional logic so that the path must meet the following in order to match: - Path must exist. - Directories that are symlinks will not be matched by [`GLOBSTAR`](#globstar) patterns (`**`) unless the [`FOLLOW`](#follow) flag is enabled. - If [`GLOBSTARLONG`](#globstarlong) is enabled, `***` will traverse symlinks, [`FOLLOW`](#follow) will be ignored except if [`MATCHBASE`](#matchbase) is also enabled, in that case, the implicit leading `**` added by [`MATCHBASE`](#matchbase) will act as `***`. This also affects the implicit leading `**` adding by [`rglob`](#rglob). - When presented with a pattern where the match must be a directory, but the file path being compared doesn't indicate the file is a directory with a trailing slash, the command will look at the filesystem to determine if it is a directory. - Paths must match in relation to the current working directory unless the pattern is constructed in a way to indicates an absolute path. #### `pathlib.DOTGLOB, pathlib.D` {: #dotglob} By default, globbing and matching functions will not match file or directory names that start with dot `.` unless matched with a literal dot. `DOTGLOB` allows the meta characters (such as `*`) to glob dots like any other character. Dots will not be matched in `[]`, `*`, or `?`. Alternatively `DOTMATCH` will also be accepted for consistency with the other provided libraries. Both flags are exactly the same and are provided as a convenience in case the user finds one more intuitive than the other since `DOTGLOB` is often the name used in Bash. #### `pathlib.NODOTDIR, glob.Z` {: #nodotdir} `NOTDOTDIR` fundamentally changes how glob patterns deal with `.` and `..`. This is great if you'd prefer a more Zsh feel when it comes to special directory matching. When `NODOTDIR` is enabled, "magic" patterns, such as `.*`, will not match the special directories of `.` and `..`. In order to match these special directories, you will have to use literal glob patterns of `.` and `..`. This can be used in all glob API functions that accept flags, and will affect inclusion patterns as well as exclusion patterns. ```pycon3 >>> from wcmatch import pathlib >>> pathlib.Path('..').match('.*') True >>> pathlib.Path('..').match('.*', flags=pathlib.NODOTDIR) False >>> pathlib.Path('..').match('..', flags=pathlib.NODOTDIR) True ``` Also affects exclusion patterns: ```pycon3 >>> from wcmatch import pathlib >>> list(pathlib.Path('.').glob(['docs/..', '!*/.*'], flags=pathlib.NEGATE)) [] >>> list(pathlib.Path('.').glob(['docs/..', '!*/.*'], flags=pathlib.NEGATE | pathlib.NODOTDIR)) [PosixPath('docs/..')] >>> list(pathlib.Path('.').glob(['docs/..', '!*/..'], flags=pathlib.NEGATE | pathlib.NODOTDIR)) [] ``` /// new | New 7.0 `NODOTDIR` was added in 7.0. /// #### `pathlib.SCANDOTDIR, pathlib.SD` {: #scandotdir} /// warning | Not recommended for `pathlib` `pathlib` supports all of the same flags that the [`wcmatch.glob`](./glob.md) library does. But due to how `pathlib` normalizes the paths that get returned, enabling `SCANDOTDIR` will only give confusing duplicates if using patterns such as `.*`. This is not a bug, but is something to be aware of. /// `SCANDOTDIR` controls the directory scanning behavior of [`glob`](#glob) and [`rglob`](#rglob). The directory scanner of these functions do not return `.` and `..` in their results. This means unless you use an explicit `.` or `..` in your glob pattern, `.` and `..` will not be returned. When `SCANDOTDIR` is enabled, `.` and `..` will be returned when a directory is scanned causing "magic" patterns, such as `.*`, to match `.` and `..`. This only controls the directory scanning behavior and not how glob patterns behave. Exclude patterns, which filter, the returned results via [`NEGATE`](#negate), can still match `.` and `..` with "magic" patterns such as `.*` regardless of whether `SCANDOTDIR` is enabled or not. It will also have no affect on [`globmatch`](#globmatch). To fundamentally change how glob patterns behave, you can use [`NODOTDIR`](#nodotdir). ```pycon3 >>> from wcmatch import pathlib >>> list(pathlib.Path('temp').glob('**/.*', flags=glob.GLOBSTAR | glob.DOTGLOB)) [PosixPath('temp/.hidden'), PosixPath('temp/.DS_Store')] >>> list(pathlib.Path('temp').glob('**/.*', flags=pathlib.GLOBSTAR | pathlib.DOTGLOB | pathlib.SCANDOTDIR)) [PosixPath('temp'), PosixPath('temp/..'), PosixPath('temp/.hidden'), PosixPath('temp/.hidden/..'), PosixPath('temp/.DS_Store')] ``` Notice when we turn off unique result filtering how we get multiple `temp/.hidden` results. This is due to how `pathlib` normalizes directories. When comparing the results to a non-`pathlib` glob, the results make a bit more sense. ```pycon >>> list(pathlib.Path('temp').glob('**/.*', flags=pathlib.GLOBSTAR | pathlib.DOTGLOB | pathlib.SCANDOTDIR | pathlib.NOUNIQUE)) [PosixPath('temp'), PosixPath('temp/..'), PosixPath('temp/.hidden'), PosixPath('temp/.hidden'), PosixPath('temp/.hidden/..'), PosixPath('temp/.DS_Store')] >>> list(glob.glob('**/.*', flags=glob.GLOBSTAR | glob.DOTGLOB | glob.SCANDOTDIR, root_dir="temp")) ['.', '..', '.hidden', '.hidden/.', '.hidden/..', '.DS_Store'] ``` /// new | New 7.0 `SCANDOTDIR` was added in 7.0. /// #### `pathlib.EXTGLOB, pathlib.E` {: #extglob} `EXTGLOB` enables extended pattern matching which includes special pattern lists such as `+(...)`, `*(...)`, `?(...)`, etc. Pattern lists allow for multiple patterns within them separated by `|`. See the globbing [syntax overview](glob.md#syntax) for more information. Alternatively `EXTMATCH` will also be accepted for consistency with the other provided libraries. Both flags are exactly the same and are provided as a convenience in case the user finds one more intuitive than the other since `EXTGLOB` is often the name used in Bash. /// tip | EXTGLOB and NEGATE When using `EXTGLOB` and [`NEGATE`](#negate) together, if a pattern starts with `!(`, the pattern will not be treated as a [`NEGATE`](#negate) pattern (even if `!(` doesn't yield a valid `EXTGLOB` pattern). To negate a pattern that starts with a literal `(`, you must escape the bracket: `!\(`. /// #### `pathlib.BRACE, pathlib.B` {: #brace} `BRACE` enables Bash style brace expansion: `a{b,{c,d}}` --> `ab ac ad`. Brace expansion is applied before anything else. When applied, a pattern will be expanded into multiple patterns. Each pattern will then be parsed separately. Duplicate patterns will be discarded[^1] by default, and [`glob`](#glob) and [`rglob`](#rglob) will return only unique results. If you need [`glob`](#glob) or [`rglob`](#rglob) to behave more like Bash and return all results, you can set [`NOUNIQUE`](#nounique). [`NOUNIQUE`](#nounique) has no effect on matching functions such as [`globmatch`](#globmatch) and [`match`](#match). For simple patterns, it may make more sense to use [`EXTGLOB`](#extglob) which will only generate a single pattern which will perform much better: `@(ab|ac|ad)`. /// warning | Massive Expansion Risk 1. It is important to note that each pattern is crawled separately, so patterns such as `{1..100}` would generate **one hundred** patterns. In a match function ([`globmatch`](#globmatch)), that would cause a hundred compares, and in a file crawling function ([`glob`](#glob)), it would cause the file system to be crawled one hundred times. Sometimes patterns like this are needed, so construct patterns thoughtfully and carefully. 2. `BRACE` and [`SPLIT`](#split) both expand patterns into multiple patterns. Using these two syntaxes simultaneously can exponential increase duplicate patterns: ```pycon3 >>> expand('test@(this{|that,|other})|*.py', BRACE | SPLIT | EXTMATCH) ['test@(this|that)', 'test@(this|other)', '*.py', '*.py'] ``` This effect is reduced as redundant, identical patterns are optimized away[^1], but when using crawling functions (like in [`glob`](#glob)) *and* [`NOUNIQUE`](#nounique) that optimization is removed, and all of those patterns will be crawled. For this reason, especially when using functions like [`glob`](#glob), it is recommended to use one syntax or the other. /// [^1]: Identical patterns are only reduced by comparing case sensitively as POSIX character classes are case sensitive: `[[:alnum:]]` =/= `[[:ALNUM:]]`. #### `pathlib.SPLIT, pathlib.S` {: #split} `SPLIT` is used to take a string of multiple patterns that are delimited by `|` and split them into separate patterns. This is provided to help with some interfaces that might need a way to define multiple patterns in one input. It pairs really well with [`EXTGLOB`](#extglob) and takes into account sequences (`[]`) and extended patterns (`*(...)`) and will not parse `|` within them. You can also escape the delimiters if needed: `\|`. Duplicate patterns will be discarded[^1] by default, and [`glob`](#glob) and [`rglob`](#rglob) will return only unique results. If you need [`glob`](#glob) or [`rglob`](#rglob) to behave more like Bash and return all results, you can set [`NOUNIQUE`](#nounique). [`NOUNIQUE`](#nounique) has no effect on matching functions such as [`globmatch`](#globmatch) and [`match`](#match). While `SPLIT` is not as powerful as [`BRACE`](#brace), it's syntax is very easy to use, and when paired with [`EXTGLOB`](#extglob), it feels natural and comes a bit closer. It is also much harder to create massive expansions of patterns with it, except when paired *with* [`BRACE`](#brace). See [`BRACE`](#brace) and its warnings related to pairing it with `SPLIT`. ```pycon3 >>> from wcmatch import pathlib >>> list(pathlib.Path('.').glob('README.md|LICENSE.md', flags=pathlib.SPLIT)) [WindowsPath('README.md'), WindowsPath('LICENSE.md')] ``` #### `pathlib.NOUNIQUE, pathlib.Q` {: #nounique} `NOUNIQUE` is used to disable Wildcard Match's unique results return. This mimics Bash's output behavior if that is desired. ```pycon3 >>> from wcmatch import glob >>> glob.glob('{*,README}.md', flags=glob.BRACE | glob.NOUNIQUE) ['LICENSE.md', 'README.md', 'README.md'] >>> glob.glob('{*,README}.md', flags=glob.BRACE ) ['LICENSE.md', 'README.md'] ``` By default, only unique paths are returned in [`glob`](#glob) and [`rglob`](#rglob). Normally this is what a programmer would want from such a library, so input patterns are reduced to unique patterns[^1] to reduce excessive matching with redundant patterns and excessive crawls through the file system. Also, as two different patterns that have been fed into [`glob`](#glob) may match the same file, the results are also filtered as to not return the duplicates. Unique results are accomplished by filtering out duplicate patterns and by retaining an internal set of returned files to determine duplicates. The internal set of files is not retained if only a single, inclusive pattern is provided. Exclusive patterns via [`NEGATE`](#negate) will not trigger the logic, but singular inclusive patterns that use pattern expansions due to [`BRACE`](#brace) or [`SPLIT`](#split) will act as if multiple patterns were provided, and will trigger the duplicate filtering logic. Lastly, if [`SCANDOTDIR`](#scandotdir) is enabled, even singular inclusive patterns will trigger duplicate filtering logic to protect against cases where `pathlib` will normalize two unique results to be the same path, such as `.hidden` and `.hidden/.` which get normalized to `.hidden`. `NOUNIQUE` disables all of the aforementioned "unique" optimizations, but only for [`glob`](#glob) and [`rglob`](#rglob). Functions like [`globmatch`](#globmatch) and [`match`](#match) would get no benefit from disabling "unique" optimizations as they only match what they are given. /// new | New in 6.0 "Unique" optimizations were added in 6.0, along with `NOUNIQUE`. /// #### `pathlib.MATCHBASE, pathlib.X` {: #matchbase} `MATCHBASE`, when a pattern has no slashes in it, will cause all glob related functions to seek for any file anywhere in the tree with a matching basename, or in the case of [`match`](#match) and [`globmatch`](#globmatch), path whose basename matches. `MATCHBASE` is sensitive to files and directories that start with `.` and will not match such files and directories if [`DOTGLOB`](#dotglob) is not enabled. ```pycon3 >>> from wcmatch import pathlib >>> list(pathlib.Path('.').glob('*.txt', flags=pathlib.MATCHBASE)) [WindowsPath('docs/src/dictionary/en-custom.txt'), WindowsPath('docs/src/markdown/_snippets/abbr.txt'), WindowsPath('docs/src/markdown/_snippets/links.txt'), WindowsPath('docs/src/markdown/_snippets/posix.txt'), WindowsPath('docs/src/markdown/_snippets/refs.txt'), WindowsPath('requirements/docs.txt'), WindowsPath('requirements/lint.txt'), WindowsPath('requirements/setup.txt'), WindowsPath('requirements/test.txt'), WindowsPath('requirements/tools.txt'), WindowsPath('site/_snippets/abbr.txt'), WindowsPath('site/_snippets/links.txt'), WindowsPath('site/_snippets/posix.txt'), WindowsPath('site/_snippets/refs.txt')] ``` #### `pathlib.NODIR, pathlib.O` {: #nodir} `NODIR` will cause all glob related functions to return only matched files. In the case of [`PurePath`](#purepath) classes, this may not be possible as those classes do not access the file system, nor will they retain trailing slashes. ```pycon3 >>> from wcmatch import pathlib >>> list(pathlib.Path('.').glob('*', flags=pathlib.NODIR)) [WindowsPath('appveyor.yml'), WindowsPath('LICENSE.md'), WindowsPath('MANIFEST.in'), WindowsPath('mkdocs.yml'), WindowsPath('README.md'), WindowsPath('setup.cfg'), WindowsPath('setup.py'), WindowsPath('tox.ini')] >>> list(pathlib.Path('.').glob('*')) [WindowsPath('appveyor.yml'), WindowsPath('docs'), WindowsPath('LICENSE.md'), WindowsPath('MANIFEST.in'), WindowsPath('mkdocs.yml'), WindowsPath('README.md'), WindowsPath('requirements'), WindowsPath('setup.cfg'), WindowsPath('setup.py'), WindowsPath('site'), WindowsPath('tests'), WindowsPath('tox.ini'), WindowsPath('wcmatch')] ``` wcmatch-10.0/docs/src/markdown/wcmatch.md000066400000000000000000000443611467532413500204130ustar00rootroot00000000000000# `wcmatch.wcmatch` ```py3 from wcmatch import wcmatch ``` ## Overview `wcmatch.WcMatch` was originally written to provide a simple user interface for searching specific files in [Rummage](https://github.com/facelessuser/Rummage). A class was needed to facilitate a user interface where a user could select a root directory, define one or more file patterns they wanted to search for, and provide folders to exclude if needed. It needed to be aware of hidden files on different systems, not just ignoring files that start with `.`. It also needed to be extendable so we could further filter returned files by size, creation date, or whatever else was decided. While [`glob`](./glob.md) is a fantastic file and folder search tool, it just didn't make sense for such a user interface. ## `wcmatch.WcMatch` {: #wcmatch} ```py3 class WcMatch: """Finds files by wildcard.""" def __init__(self, root_dir=".", file_pattern=None, **kwargs): """Initialize the directory walker object.""" ``` `WcMatch` is an extendable file search class. It allows you to specify a root directory path, file patterns, and optional folder exclude patterns. You can specify whether you want to see hidden files and whether the search should be recursive. You can also derive from the class and tap into specific hooks to change what is returned or done when a file is matched, skipped, or when there is an error. There are also hooks where you can inject additional, custom filtering. Parameter | Default | Description ----------------- | ------------- | ----------- `root_dir` | | The root directory to search. `file_pattern` | `#!py3 ''` | One or more patterns separated by `|`. You can define exceptions by starting a pattern with `!` (or `-` if [`MINUSNEGATE`](#minusnegate) is set). The default is an empty string, but if an empty string is used, all files will be matched. `exclude_pattern` | `#!py3 ''` | Zero or more folder exclude patterns separated by `|`. You can define exceptions by starting a pattern with `!` (or `-` if [`MINUSNEGATE`](#minusnegate) is set). `flags` | `#!py3 0` | Flags to alter behavior of folder and file matching. See [Flags](#flags) for more info. `limit` | `#!py3 1000` | Allows configuring the [max pattern limit](#multi-pattern-limits). /// note Dots are not treated special in `wcmatch`. When the `HIDDEN` flag is not included, all hidden files (system and dot files) are excluded from the crawling processes, so there is no risk of `*` matching a dot file as it will not show up in the crawl. If the `HIDDEN` flag is included, `*`, `?`, and `[.]` will then match dot files. /// /// new | New 6.0 `limit` was added in 6.0. /// ### Multi-Pattern Limits The `WcMatch` class allow expanding a pattern into multiple patterns by using `|` and by using [`BRACE`](#brace). The number of allowed patterns is limited `1000`, but you can raise or lower this limit via the keyword option `limit`. If you set `limit` to `0`, there will be no limit. /// new | New 6.0 The imposed pattern limit and corresponding `limit` option was introduced in 6.0. /// ### Examples Searching for files: ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '*.md|*.txt').match() ['./LICENSE.md', './README.md'] ``` Recursively searching for files: ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '*.md|*.txt', flags=wcmatch.RECURSIVE).match() ['./LICENSE.md', './README.md', './docs/src/markdown/changelog.md', './docs/src/markdown/fnmatch.md', './docs/src/markdown/glob.md', './docs/src/markdown/index.md', './docs/src/markdown/installation.md', './docs/src/markdown/license.md', './docs/src/markdown/wcmatch.md', './docs/src/markdown/_snippets/abbr.md', './docs/src/markdown/_snippets/links.md', './docs/src/markdown/_snippets/refs.md', './requirements/docs.txt', './requirements/lint.txt', './requirements/setup.txt', './requirements/test.txt'] ``` Excluding directories: ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '*.md|*.txt', exclude_pattern='docs', flags=wcmatch.RECURSIVE).match() ['./LICENSE.md', './README.md', './requirements/docs.txt', './requirements/lint.txt', './requirements/setup.txt', './requirements/test.txt'] ``` Using file negation patterns: ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '*.md|*.txt|!README*', exclude_pattern='docs', flags=wcmatch.RECURSIVE).match() ['./LICENSE.md', './requirements/docs.txt', './requirements/lint.txt', './requirements/setup.txt', './requirements/test.txt'] ``` You can also use negation patterns in directory exclude. Here we avoid all folders with `*`, but add an exception for `requirements`. It should be noted that you cannot add an exception for the child of an excluded folder. ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '*.md|*.txt', exclude_pattern='*|!requirements', flags=wcmatch.RECURSIVE).match() ['./LICENSE.md', './README.md', './requirements/docs.txt', './requirements/lint.txt', './requirements/setup.txt', './requirements/test.txt'] ``` Negative patterns can be given by themselves. ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '*.md|*.txt', exclude_pattern='!requirements', flags=wcmatch.RECURSIVE).match() ['./LICENSE.md', './README.md', './requirements/docs.txt', './requirements/lint.txt', './requirements/setup.txt', './requirements/test.txt'] ``` Enabling hidden files: ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '*.yml').match() ['./appveyor.yml', './mkdocs.yml'] >>> wcmatch.WcMatch('.', '*.yml', flags=wcmatch.HIDDEN).match() ['./.codecov.yml', './.travis.yml', './appveyor.yml', './mkdocs.yml'] ``` ## Methods #### `WcMatch.match` {: #match} Perform match returning files that match the patterns. ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '*.md|*.txt').match() ['./LICENSE.md', './README.md'] ``` #### `WcMatch.imatch` {: #imatch} Perform match returning an iterator of files that match the patterns. ```pycon3 >>> from wcmatch import wcmatch >>> list(wcmatch.WcMatch('.', '*.md|*.txt').imatch()) ['./LICENSE.md', './README.md'] ``` #### `WcMatch.kill` {: #kill} If searching with [`imatch`](#imatch), this provides a way to gracefully kill the internal searching. Internally, you can call [`is_aborted`](#is_aborted) to check if a request to abort has been made. So if work on a file is being done in an [`on_match`](#on_match), you can check if there has been a request to kill the process, and tie up loose ends gracefully. ```pycon3 >>> from wcmatch import wcmatch >>> wcm = wcmatch.WcMatch('.', '*.md|*.txt') >>> for f in wcm.imatch(): ... print(f) ... wcm.kill() ... ./LICENSE.md ``` Once a "kill" has been issued, the class will remain in an aborted state. To clear the "kill" state, you must call [`reset`](#reset). This allows a process to define a `Wcmatch` class and reuse it. If a process receives an early kill and sets it before the match is started, when the match is started, it will immediately abort. This helps with race conditions depending on how you are using `WcMatch`. #### `WcMatch.reset` {: #reset} Resets the abort state after running `kill`. ```pycon3 >>> from wcmatch import wcmatch >>> wcm = wcmatch.WcMatch('.', '*.md|*.txt') >>> for f in wcm.imatch(): ... print(f) ... wcm.kill() ... ./LICENSE.md >>> wcm.reset() >>> list(wcm.imatch()) ['./LICENSE.md', './README.md'] ``` #### `WcMatch.is_aborted` {: #is_aborted} Checks if an abort has been issued. ```pycon3 >>> from wcmatch import wcmatch >>> wcm = wcmatch.WcMatch('.', '*.md|*.txt') >>> for f in wcm.imatch(): ... wcm.kill() ... >>> wcm.is_aborted() True ``` #### `WcMatch.get_skipped` {: #get_skipped} Returns the number of skipped files. Files in skipped folders are not included in the count. ```pycon3 >>> from wcmatch import wcmatch >>> wcm = wcmatch.WcMatch('.', '*.md|*.txt') >>> list(wcm.imatch()) ['./LICENSE.md', './README.md'] >>> wcm.get_skipped() 10 ``` ## Hooks #### `WcMatch.on_init` {: #on_init} ```py3 def on_init(self, **kwargs): """Handle custom init.""" ``` Any keyword arguments not processed by the main initializer are sent to `on_init`. This allows you to specify additional arguments when deriving from `WcMatch`. /// new | Changed 8.0 Starting in 8.0, `on_init` only accepts keyword arguments as now `WcMatch` requires all parameters (except `root_dir` and `file_pattern`) to be keyword parameters and must explicitly be specified in the form `key=value`. /// #### `WcMatch.on_validate_directory` {: #on_validate_directory} ```py3 def on_validate_directory(self, base, name): """Validate folder override.""" return True ``` When validating a directory, if the directory passes validation, it will be sent to `on_validate_directory` which can be overridden to provide additional validation if required. #### `WcMatch.on_validate_file` {: #on_validate_file} ```py3 def on_validate_file(self, base, name): """Validate file override.""" return True ``` When validating a file, if the file passes validation, it will be sent to `on_validate_file` which can be overridden to provide additional validation if required. #### `WcMatch.on_skip` {: #on_skip} ```py3 def on_skip(self, base, name): """On skip.""" return None ``` When a file that must be skipped is encountered (a file that doesn't pass validation), it is sent to `on_skip`. Here you could abort the search, store away information, or even create a special skip record to return. It is advised to create a special type for skip returns so that you can identify them when they are returned via [`match`](#match) or [`imatch`](#imatch). #### `WcMatch.on_error` {: #on_error} ```py3 def on_error(self, base, name): """On error.""" return None ``` When accessing or processing a file throws an error, it is sent to `on_error`. Here you could abort the search, store away information, or even create a special error record to return. It is advised to create a special type for error returns so that you can identify them when they are returned via [`match`](#match) or [`imatch`](#imatch). #### `WcMatch.on_match` {: #on_match} ```py3 def on_match(self, base, name): """On match.""" return os.path.join(base, name) ``` On match returns the path of the matched file. You can override `on_match` and change what is returned. You could return just the base, you could parse the file and return the content, or return a special match record with additional file meta data. `on_match` must return something, and all results will be returned via [`match`](#match) or [`imatch`](#imatch). #### `WcMatch.on_reset` {: #on_reset} ```py3 def on_reset(self): """On reset.""" pass ``` `on_reset` is a hook to provide a way to reset any custom logic in classes that have derived from `WcMatch`. `on_reset` is called on every new [`match`](#match) call. ## Flags #### `wcmatch.RECURSIVE, wcmatch.RV` {: #recursive} `RECURSIVE` forces a recursive search that will crawl all subdirectories. #### `wcmatch.HIDDEN, wcmatch.HD` {: #hidden} `HIDDEN` enables the crawling of hidden directories and will return hidden files if the wildcard pattern matches. This enables not just dot files, but system hidden files as well. #### `wcmatch.SYMLINKS, wcmatch.SL` {: #symlinks} `SYMLINKS` enables the crawling of symlink directories. By default, symlink directories are ignored during the file crawl. #### `wcmatch.CASE, wcmatch.C` {: #case} `CASE` forces case sensitivity. `CASE` has higher priority than [`IGNORECASE`](#ignorecase). #### `wcmatch.IGNORECASE, wcmatch.I` {: #ignorecase} `IGNORECASE` forces case insensitive searches. [`CASE`](#case) has higher priority than `IGNORECASE`. #### `wcmatch.RAWCHARS, wcmatch.R` {: #rawchars} `RAWCHARS` causes string character syntax to be parsed in raw strings: `#!py3 r'\u0040'` --> `#!py3 r'@'`. This will handle standard string escapes and Unicode (including `#!py3 r'\N{CHAR NAME}'`). #### `wcmatch.EXTMATCH, wcmatch.E` {: #extmatch} `EXTMATCH` enables extended pattern matching which includes special pattern lists such as `+(...)`, `*(...)`, `?(...)`, etc. /// tip | EXTMATCH and NEGATE When using `EXTMATCH` and [`NEGATE`](#negate) together, if a pattern starts with `!(`, the pattern will not be treated as a [`NEGATE`](#negate) pattern (even if `!(` doesn't yield a valid `EXTMATCH` pattern). To negate a pattern that starts with a literal `(`, you must escape the bracket: `!\(`. /// #### `wcmatch.BRACE, wcmatch.B` {: #brace} `BRACE` enables Bash style brace expansion: `a{b,{c,d}}` --> `ab ac ad`. Brace expansion is applied before anything else. When applied, a pattern will be expanded into multiple patterns. Each pattern will then be parsed separately. Redundant, identical patterns are discarded[^1] by default. For simple patterns, it may make more sense to use [`EXTMATCH`](#extmatch) which will only generate a single pattern which will perform much better: `@(ab|ac|ad)`. /// warning | Massive Expansion Risk 1. It is important to note that each pattern is matched separately, so patterns such as `{1..100}` would generate **one hundred** patterns. Since [`WcMatch`](#wcmatch_1) class is able to crawl the file system one pass accounting for all the patterns, the performance isn't as bad as it may be with [`glob`](./glob.md), but it can still impact performance as each file must get compared against many patterns until one is matched. Sometimes patterns like this are needed, so construct patterns thoughtfully and carefully. 2. Splitting patterns with `|` is built into [`WcMatch`](#wcmatch_1). `BRACE` and and splitting with `|` both expand patterns into multiple patterns. Using these two syntaxes simultaneously can exponential increase in duplicate patterns: ```pycon3 >>> expand('test@(this{|that,|other})|*.py', BRACE | SPLIT | EXTMATCH) ['test@(this|that)', 'test@(this|other)', '*.py', '*.py'] ``` This effect is reduced as redundant, identical patterns are optimized away[^1]. But it is useful to know if trying to construct efficient patterns. /// [^1]: Identical patterns are only reduced by comparing case sensitively as POSIX character classes are case sensitive: `[[:alnum:]]` =/= `[[:ALNUM:]]`. #### `wcmatch.MINUSNEGATE, wcmatch.M` {: #minusnegate} `MINUSNEGATE` requires negation patterns to use `-` instead of `!`. #### `wcmatch.DIRPATHNAME, wcmatch.DP` {: #dirpathname} `DIRPATHNAME` will enable path name searching for excluded folder patterns, but it will not apply to file patterns. This is mainly provided for cases where you may have multiple folders with the same name, but you want to target a specific folder to exclude. The path name compared will be the entire path relative to the root directory. So if the provided root directory folder was `.`, and the folder under evaluation is `./some/folder`, `some/folder` will be matched against the pattern. ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '*.md|*.txt', 'docs/src/markdown', flags=wcmatch.DIRPATHNAME | wcmatch.RECURSIVE).match() ['./LICENSE.md', './README.md', './requirements/docs.txt', './requirements/lint.txt', './requirements/setup.txt', './requirements/test.txt'] ``` #### `wcmatch.FILEPATHNAME, wcmatch.FP` {: #filepathname} `FILEPATHNAME` will enable path name searching for the file patterns, but it will not apply to directory exclude patterns. The path name compared will be the entire path relative to the root directory path. So if the provided root directory was `.`, and the file under evaluation is `./some/file.txt`, `some/file.txt` will be matched against the pattern. ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '**/*.md|!**/_snippets/*', flags=wcmatch.FILEPATHNAME | wcmatch.GLOBSTAR | wcmatch.RECURSIVE).match() ['./LICENSE.md', './README.md', './docs/src/markdown/changelog.md', './docs/src/markdown/fnmatch.md', './docs/src/markdown/glob.md', './docs/src/markdown/index.md', './docs/src/markdown/license.md', './docs/src/markdown/wcmatch.md'] ``` #### `wcmatch.PATHNAME, wcmatch.P` {: #pathname} `PATHNAME` enables both [`DIRPATHNAME`](#dirpathname) and [`FILEPATHNAME`](#wcmathfilepathname). It is provided for convenience. #### `wcmatch.MATCHBASE, wcmatch.X` {: #matchbase} When [`FILEPATHNAME`](#filepathname) or [`DIRPATHNAME`](#dirpathname) is enabled, `MATCHBASE` will ensure that that the respective file or directory pattern, when there are no slashes in the pattern, seeks for any file anywhere in the tree with a matching basename. This is essentially the behavior when [`FILEPATHNAME`](#filepathname) and [`DIRPATHNAME`](#dirpathname) is disabled, but with `MATCHBASE`, you can toggle the behavior by including slashes in your pattern. When we include no slashes: ```pycon3 >>> wcmatch.WcMatch('.', '*.md', flags=wcmatch.FILEPATHNAME | wcmatch.GLOBSTAR | wcmatch.MATCHBASE | wcmatch.RECURSIVE).match() ['./LICENSE.md', './README.md', './docs/src/markdown/changelog.md', './docs/src/markdown/fnmatch.md', './docs/src/markdown/glob.md', './docs/src/markdown/index.md', './docs/src/markdown/license.md', './docs/src/markdown/wcmatch.md'] ``` If we include slashes in the pattern, the path, not the basename, must match the pattern: ```pycon3 >>> wcmatch.WcMatch('.', 'docs/**/*.md', flags=wcmatch.FILEPATHNAME | wcmatch.GLOBSTAR | wcmatch.MATCHBASE | wcmatch.RECURSIVE).match() ['./docs/src/markdown/changelog.md', './docs/src/markdown/fnmatch.md', './docs/src/markdown/glob.md', './docs/src/markdown/index.md', './docs/src/markdown/license.md', './docs/src/markdown/wcmatch.md'] ``` If we have a leading slash, the pattern will not perform a match on the basename, but will instead be a normal path pattern that is anchored to the current base path, in this case `.`. ```pycon3 >>> wcmatch.WcMatch('.', '/*.md', flags=wcmatch.FILEPATHNAME | wcmatch.GLOBSTAR | wcmatch.MATCHBASE | wcmatch.RECURSIVE).match() ['./LICENSE.md', './README.md'] ``` #### `wcmatch.GLOBSTAR, wcmatch.G` {: #globstar} When the [`PATHNAME`](#pathname) flag is provided, you can also enable `GLOBSTAR` to enable the recursive directory pattern matches with `**`. ```pycon3 >>> from wcmatch import wcmatch >>> wcmatch.WcMatch('.', '*.md|*.txt', '**/markdown', flags=wcmatch.DIRPATHNAME | wcmatch.GLOBSTAR | wcmatch.RECURSIVE).match() ['./LICENSE.md', './README.md', './requirements/docs.txt', './requirements/lint.txt', './requirements/setup.txt', './requirements/test.txt'] ``` wcmatch-10.0/docs/theme/000077500000000000000000000000001467532413500151245ustar00rootroot00000000000000wcmatch-10.0/docs/theme/announce.html000066400000000000000000000007551467532413500176270ustar00rootroot00000000000000 {% set icon = "material/alert-decagram" %} {% include ".icons/" ~ icon ~ ".svg" %} 8.0 has been released! Check out the Release Notes for more information and migration tips.
Sponsorship is now available! {% set icon = "octicons/heart-fill-16" %} {% include ".icons/" ~ icon ~ ".svg" %} wcmatch-10.0/hatch_build.py000066400000000000000000000027431467532413500157200ustar00rootroot00000000000000"""Dynamically define some metadata.""" import os from hatchling.metadata.plugin.interface import MetadataHookInterface def get_version_dev_status(root): """Get version_info without importing the entire module.""" import importlib.util path = os.path.join(root, "wcmatch", "__meta__.py") spec = importlib.util.spec_from_file_location("__meta__", path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module.__version_info__._get_dev_status() class CustomMetadataHook(MetadataHookInterface): """Our metadata hook.""" def update(self, metadata): """See https://ofek.dev/hatch/latest/plugins/metadata-hook/ for more information.""" metadata["classifiers"] = [ f"Development Status :: {get_version_dev_status(self.root)}", 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Topic :: Software Development :: Libraries :: Python Modules', 'Typing :: Typed' ] wcmatch-10.0/mkdocs.yml000066400000000000000000000100561467532413500150770ustar00rootroot00000000000000site_name: Wildcard Match Documentation site_url: https://facelessuser.github.io/wcmatch repo_url: https://github.com/facelessuser/wcmatch edit_uri: tree/main/docs/src/markdown site_description: A wildcard file name matching library copyright: | Copyright © 2014 - 2024 Isaac Muse docs_dir: docs/src/markdown theme: custom_dir: docs/theme name: material icon: logo: material/book-open-page-variant palette: scheme: dracula primary: deep purple accent: deep purple font: text: Roboto code: Roboto Mono features: - navigation.tabs - navigation.top - navigation.instant - navigation.indexes - toc.follow - content.code.copy - navigation.footer - search.share - search.highlight - search.suggest pymdownx: sponsor: "https://github.com/sponsors/facelessuser" nav: - Home: - Introduction: index.md - Fnmatch: - Usage: fnmatch.md - Glob: - Usage: glob.md - Pathlib: - Usage: pathlib.md - Wcmatch: - Usage: wcmatch.md - About: - Contributing & Support: about/contributing.md - Changelog: about/changelog.md - Release Notes: about/release.md - License: about/license.md markdown_extensions: - markdown.extensions.toc: slugify: !!python/object/apply:pymdownx.slugs.slugify {kwds: {case: lower}} permalink: "" - markdown.extensions.smarty: smart_quotes: false - pymdownx.betterem: - markdown.extensions.attr_list: - markdown.extensions.tables: - markdown.extensions.abbr: - markdown.extensions.footnotes: - markdown.extensions.md_in_html: - pymdownx.superfences: preserve_tabs: true - pymdownx.highlight: extend_pygments_lang: - name: php-inline lang: php options: startinline: true - name: pycon3 lang: pycon options: python3: true - pymdownx.inlinehilite: - pymdownx.magiclink: repo_url_shortener: true repo_url_shorthand: true social_url_shorthand: true user: facelessuser repo: wcmatch - pymdownx.tilde: - pymdownx.caret: - pymdownx.smartsymbols: - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.escapeall: hardbreak: True nbsp: True - pymdownx.tasklist: custom_checkbox: true - pymdownx.progressbar: - pymdownx.striphtml: - pymdownx.snippets: base_path: - docs/src/markdown/.snippets - LICENSE.md auto_append: - refs.md - pymdownx.keys: separator: "\uff0b" - pymdownx.saneheaders: - pymdownx.blocks.admonition: types: - new - settings - note - abstract - info - tip - success - question - warning - failure - danger - bug - example - quote - pymdownx.blocks.details: types: - name: details-new class: new - name: details-settings class: settings - name: details-note class: note - name: details-abstract class: abstract - name: details-info class: info - name: details-tip class: tip - name: details-success class: success - name: details-question class: question - name: details-warning class: warning - name: details-failure class: failure - name: details-danger class: danger - name: details-bug class: bug - name: details-example class: example - name: details-quote class: quote - pymdownx.blocks.html: - pymdownx.blocks.definition: - pymdownx.blocks.tab: alternate_style: True extra: social: - icon: fontawesome/brands/github link: https://github.com/facelessuser plugins: - search - git-revision-date-localized: fallback_to_build_date: true - mkdocs_pymdownx_material_extras - minify: minify_html: true wcmatch-10.0/pyproject.toml000066400000000000000000000047631467532413500160200ustar00rootroot00000000000000[build-system] requires = [ "hatchling>=0.21.1", ] build-backend = "hatchling.build" [project] name = "wcmatch" description = "Wildcard/glob file name matcher." readme = "README.md" license = "MIT" requires-python = ">=3.8" authors = [ { name = "Isaac Muse", email = "Isaac.Muse@gmail.com" }, ] keywords = [ "glob", "fnmatch", "search", "wildcard" ] dynamic = [ "classifiers", "version", ] dependencies = [ "bracex>=2.1.1" ] [project.urls] Homepage = "https://github.com/facelessuser/wcmatch" [tool.hatch.version] source = "code" path = "wcmatch/__meta__.py" [tool.hatch.build.targets.wheel] include = [ "/wcmatch", ] [tool.hatch.build.targets.sdist] include = [ "/docs/src/markdown/**/*.md", "/docs/src/markdown/**/*.gif", "/docs/src/markdown/**/*.png", "/docs/src/markdown/dictionary/*.txt", "/docs/theme/**/*.css", "/docs/theme/**/*.js", "/docs/theme/**/*.html", "/requirements/*.txt", "/wcmatch/**/*.py", "/wcmatch/py.typed", "/tests/**/*.py", "/tools/**/*.py", "/.pyspelling.yml", "/.coveragerc", "/mkdocs.yml" ] [tool.mypy] files = [ "wcmatch" ] strict = true show_error_codes = true [tool.hatch.metadata.hooks.custom] [tool.ruff] line-length = 120 lint.select = [ "A", # flake8-builtins "B", # flake8-bugbear "D", # pydocstyle "C4", # flake8-comprehensions "N", # pep8-naming "E", # pycodestyle "F", # pyflakes "PGH", # pygrep-hooks "RUF", # ruff # "UP", # pyupgrade "W", # pycodestyle "YTT", # flake8-2020, "PERF" # Perflint ] lint.ignore = [ "E741", "D202", "D401", "D212", "D203", "D417", "N802", "N801", "N803", "N806", "N818", "RUF012", "RUF005", "PGH004", "RUF100" ] [tool.tox] legacy_tox_ini = """ [tox] isolated_build = true skipsdist=true envlist= py38,py39,py310,py311,py312,py313, lint [testenv] passenv=LANG deps= . -r requirements/test.txt commands= {envpython} -m mypy {envpython} -m pytest --cov wcmatch --cov-append tests {envpython} -m coverage html -d {envtmpdir}/coverage {envpython} -m coverage xml {envpython} -m coverage report --show-missing [testenv:lint] deps= -r requirements/lint.txt commands= "{envbindir}"/ruff check . [testenv:documents] deps= -r requirements/docs.txt commands= {envpython} -m mkdocs build --clean --verbose --strict {envbindir}/pyspelling [pytest] addopts=-p no:warnings """ wcmatch-10.0/requirements/000077500000000000000000000000001467532413500156155ustar00rootroot00000000000000wcmatch-10.0/requirements/docs.txt000066400000000000000000000001571467532413500173110ustar00rootroot00000000000000mkdocs_pymdownx_material_extras>=2.0 mkdocs-git-revision-date-localized-plugin mkdocs-minify-plugin pyspelling wcmatch-10.0/requirements/lint.txt000066400000000000000000000000051467532413500173170ustar00rootroot00000000000000ruff wcmatch-10.0/requirements/setup.txt000066400000000000000000000000161467532413500175130ustar00rootroot00000000000000bracex>=2.1.1 wcmatch-10.0/requirements/test.txt000066400000000000000000000000401467532413500173270ustar00rootroot00000000000000pytest pytest-cov coverage mypy wcmatch-10.0/tests/000077500000000000000000000000001467532413500142345ustar00rootroot00000000000000wcmatch-10.0/tests/__init__.py000066400000000000000000000000301467532413500163360ustar00rootroot00000000000000"""Unit test module.""" wcmatch-10.0/tests/test_fnmatch.py000066400000000000000000000720711467532413500172740ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Tests for `fnmatch`.""" import unittest import re import sys import os import pytest import wcmatch.fnmatch as fnmatch from unittest import mock from wcmatch import util import wcmatch._wcparse as _wcparse class TestFnMatch: """ Test `fnmatch`. Each entry in `cases` is run through the `fnmatch`. They are also run through `fnsplit` and then `fnmatch` as a separate operation to ensure `fnsplit` adds no unintended side effects. Each case entry is an array of 4 parameters. * Pattern * File name * Expected result (boolean of whether pattern matched file name) * Flags The default flags are `DOTMATCH`. Any flags passed through via entry are XORed. So if `DOTMATCH` is passed via an entry, it will actually disable the default `DOTMATCH`. """ cases = [ # Basic test of traditional features ['abc', 'abc', True, 0], ['?*?', 'abc', True, 0], ['???*', 'abc', True, 0], ['*???', 'abc', True, 0], ['???', 'abc', True, 0], ['*', 'abc', True, 0], ['ab[cd]', 'abc', True, 0], ['ab[!de]', 'abc', True, 0], ['ab[de]', 'abc', False, 0], ['??', 'a', False, 0], ['b', 'a', False, 0], # Test that '\' is handled correctly in character sets; [R'[\]', '\\', False, 0], [R'[!\]', 'a', False, 0], [R'[!\]', '\\', False, 0], [R'[\\]', '\\', True, 0], [R'[!\\]', 'a', True, 0], [R'[!\\]', '\\', False, 0], # Test that filenames with newlines in them are handled correctly. ['foo*', 'foo\nbar', True, 0], ['foo*', 'foo\nbar\n', True, 0], ['foo*', '\nfoo', False, 0], ['*', '\n', True, 0], # Case: General ['abc', 'abc', True, fnmatch.C], ['abc', 'AbC', False, fnmatch.C], ['AbC', 'abc', False, fnmatch.C], ['AbC', 'AbC', True, fnmatch.C], # Case and Force Unix: slash conventions ['usr/bin', 'usr/bin', True, fnmatch.C | fnmatch.U], ['usr/bin', 'usr\\bin', False, fnmatch.C | fnmatch.U], [R'usr\\bin', 'usr/bin', False, fnmatch.C | fnmatch.U], [R'usr\\bin', 'usr\\bin', True, fnmatch.C | fnmatch.U], # Case and Force Windows: slash conventions ['usr/bin', 'usr/bin', True, fnmatch.C | fnmatch.W], ['usr/bin', 'usr\\bin', True, fnmatch.C | fnmatch.W], [R'usr\\bin', 'usr/bin', True, fnmatch.C | fnmatch.W], [R'usr\\bin', 'usr\\bin', True, fnmatch.C | fnmatch.W], # Wildcard tests [b'te*', b'test', True, 0], [b'te*\xff', b'test\xff', True, 0], [b'foo*', b'foo\nbar', True, 0], # OS specific case behavior ['abc', 'abc', True, 0], ['abc', 'AbC', not util.is_case_sensitive(), 0], ['AbC', 'abc', not util.is_case_sensitive(), 0], ['AbC', 'AbC', True, 0], ['abc', 'AbC', True, fnmatch.W], ['abc', 'AbC', False, fnmatch.U], ['abc', 'AbC', True, fnmatch.U | fnmatch.I], ['AbC', 'abc', not util.is_case_sensitive(), fnmatch.W | fnmatch.U], # Can't force both, just detect system # OS specific slash behavior ['usr/bin', 'usr/bin', True, 0], ['usr/bin', 'usr\\bin', not util.is_case_sensitive(), 0], [R'usr\\bin', 'usr/bin', not util.is_case_sensitive(), 0], [R'usr\\bin', 'usr\\bin', True, 0], ['usr/bin', 'usr\\bin', True, fnmatch.W], [R'usr\\bin', 'usr/bin', True, fnmatch.W], ['usr/bin', 'usr\\bin', False, fnmatch.U], [R'usr\\bin', 'usr/bin', False, fnmatch.U], # Ensure that we don't fail on regular expression related symbols # such as &&, ||, ~~, --, or [. Currently re doesn't do anything with # && etc., but they are handled special in re as there are plans to utilize them. ['[[]', '[', True, 0], ['[a&&b]', '&', True, 0], ['[a||b]', '|', True, 0], ['[a~~b]', '~', True, 0], ['[a-z+--A-Z]', ',', True, 0], ['[a-z--/A-Z]', '.', True, 0], # `Dotmatch` cases ['.abc', '.abc', True, 0], [R'\.abc', '.abc', True, 0], ['?abc', '.abc', True, 0], ['*abc', '.abc', True, 0], ['[.]abc', '.abc', True, 0], ['*(.)abc', '.abc', True, fnmatch.E], ['*(?)abc', '.abc', True, fnmatch.E], ['*(?|.)abc', '.abc', True, fnmatch.E], ['*(?|*)abc', '.abc', True, fnmatch.E], ['!(test)', '.abc', True, fnmatch.E], ['!(test)', '..', True, fnmatch.E], # Turn off `dotmatch` cases ['.abc', '.abc', True, fnmatch.D], [R'\.abc', '.abc', True, fnmatch.D], ['?abc', '.abc', False, fnmatch.D], ['*abc', '.abc', False, fnmatch.D], ['[.]abc', '.abc', False, fnmatch.D], ['*(.)abc', '.abc', True, fnmatch.E | fnmatch.D], [R'*(\.)abc', '.abc', True, fnmatch.E | fnmatch.D], ['*(?)abc', '.abc', False, fnmatch.E | fnmatch.D], ['*(?|.)abc', '.abc', True, fnmatch.E | fnmatch.D], ['*(?|*)abc', '.abc', False, fnmatch.E | fnmatch.D], ['a.bc', 'a.bc', True, fnmatch.D], ['a?bc', 'a.bc', True, fnmatch.D], ['a*bc', 'a.bc', True, fnmatch.D], ['a[.]bc', 'a.bc', True, fnmatch.D], ['a*(.)bc', 'a.bc', True, fnmatch.E | fnmatch.D], [R'a*(\.)bc', 'a.bc', True, fnmatch.E | fnmatch.D], ['a*(?)bc', 'a.bc', True, fnmatch.E | fnmatch.D], ['a*(?|.)bc', 'a.bc', True, fnmatch.E | fnmatch.D], ['a*(?|*)bc', 'a.bc', True, fnmatch.E | fnmatch.D], ['!(test)', '.abc', False, fnmatch.D | fnmatch.E], ['!(test)', 'abc', True, fnmatch.D | fnmatch.E], ['!(test)', '..', False, fnmatch.D | fnmatch.E], # Negation list followed by extended list ['!(2)_@(foo|bar)', '1_foo', True, fnmatch.E], ['!(!(2|3))_@(foo|bar)', '2_foo', True, fnmatch.E], # POSIX style character classes ['[[:alnum:]]bc', 'zbc', True, 0], ['[[:alnum:]]bc', '1bc', True, 0], ['[a[:alnum:]]bc', 'zbc', True, 0], ['[[:alnum:][:blank:]]bc', ' bc', True, 0], ['*([[:word:]])', 'WoRD5_', True, fnmatch.E], [b'[[:alnum:]]bc', b'zbc', True, 0], [b'[[:alnum:]]bc', b'1bc', True, 0], [b'[a[:alnum:]]bc', b'zbc', True, 0], [b'[[:alnum:][:blank:]]bc', b' bc', True, 0], [b'*([[:word:]])', b'WoRD5_', True, fnmatch.E], # POSIX character classes are case sensitive ['[[:ALNUM:]]bc', 'zbc', False, 0], ['[[:AlNuM:]]bc', '1bc', False, 0], # We can't use a character class as a range. ['[-[:alnum:]]bc', '-bc', True, 0], ['[a-[:alnum:]]bc', '-bc', True, 0], ['[[:alnum:]-z]bc', '-bc', True, 0], # Negation ['[![:alnum:]]bc', '!bc', True, 0], ['[^[:alnum:]]bc', '!bc', True, 0], # Negation and extended glob together # `!` will be treated as an exclude pattern if it isn't followed by `(`. # `(` must be escaped to exclude a name that starts with `(`. # If `!(` doesn't start a valid extended glob pattern, # it will be treated as a literal, not an exclude pattern. [R'!\(test)', 'test', True, fnmatch.N | fnmatch.E | fnmatch.A], [R'!(test)', 'test', False, fnmatch.N | fnmatch.E | fnmatch.A], [R'!!(test)', 'test', True, fnmatch.N | fnmatch.E | fnmatch.A], [R'!(test', '!(test', True, fnmatch.N | fnmatch.E | fnmatch.A], # Backwards ranges ['[a-z]', 'a', True, 0], ['[z-a]', 'a', False, 0], ['[!z-a]', 'a', True, 0], ['[!a-z]', 'a', False, 0], ['[9--]', '9', False, 0], # Escaped slashes are just slashes as they aren't treated special beyond normalization. [R'a\/b', ('a/b' if util.is_case_sensitive() else 'a\\\\b'), True, 0], [R'a\/b', 'a/b', True, fnmatch.U], [R'a\/b', 'a\\\\b', True, fnmatch.W] ] @classmethod def setup_class(cls): """Setup the tests.""" cls.flags = fnmatch.DOTMATCH @staticmethod def assert_equal(a, b): """Assert equal.""" assert a == b, "Comparison between objects yielded False." @classmethod def evaluate(cls, case): """Evaluate matches.""" flags = case[3] flags = cls.flags ^ flags print("PATTERN: ", case[0]) print("FILE: ", case[1]) print("FLAGS: ", bin(flags)) print("TEST: ", case[2], '\n') cls.assert_equal(fnmatch.fnmatch(case[1], case[0], flags=flags), case[2]) cls.assert_equal( fnmatch.fnmatch(case[1], case[0], flags=flags | fnmatch.SPLIT), case[2] ) @pytest.mark.parametrize("case", cases) def test_cases(self, case): """Test case.""" self.evaluate(case) class TestFnMatchFilter: """ Test filter. `cases` is used in conjunction with the `filter` command which takes a list of file names and returns only those which match. * Pattern * List of filenames * Expected result (list of filenames that matched the pattern) * Flags The default flags are `DOTMATCH`. Any flags passed through via entry are XORed. So if `DOTMATCH` is passed via an entry, it will actually disable the default `DOTMATCH`. """ cases = [ ['P*', ['Python', 'Ruby', 'Perl', 'Tcl'], ['Python', 'Perl'], 0], [b'P*', [b'Python', b'Ruby', b'Perl', b'Tcl'], [b'Python', b'Perl'], 0], [ '*.p*', ['Test.py', 'Test.rb', 'Test.PL'], (['Test.py', 'Test.PL'] if not util.is_case_sensitive() else ['Test.py']), 0 ], [ '*.P*', ['Test.py', 'Test.rb', 'Test.PL'], (['Test.py', 'Test.PL'] if not util.is_case_sensitive() else ['Test.PL']), 0 ], [ 'usr/*', ['usr/bin', 'usr', 'usr\\lib'], (['usr/bin', 'usr\\lib'] if not util.is_case_sensitive() else ['usr/bin']), 0 ], [ R'usr\\*', ['usr/bin', 'usr', 'usr\\lib'], (['usr/bin', 'usr\\lib'] if not util.is_case_sensitive() else ['usr\\lib']), 0 ], [R'te\st[ma]', ['testm', 'test\\3', 'testa'], ['testm', 'testa'], fnmatch.I], [R'te\st[ma]', ['testm', 'test\\3', 'testa'], ['testm', 'testa'], fnmatch.C], # Issue #24 ['*.bar', ["goo.cfg", "foo.bar", "foo.bar.cfg", "foo.cfg.bar"], ["foo.bar", "foo.cfg.bar"], 0], [ '*|!*.bar', ["goo.cfg", "foo.bar", "foo.bar.cfg", "foo.cfg.bar"], ["goo.cfg", "foo.bar.cfg"], fnmatch.N | fnmatch.S ] ] @classmethod def setup_class(cls): """Setup the tests.""" cls.flags = fnmatch.DOTMATCH @staticmethod def assert_equal(a, b): """Assert equal.""" assert a == b, "Comparison between objects yielded False." @classmethod def evaluate(cls, case): """Evaluate matches.""" flags = case[3] flags = cls.flags ^ flags print("PATTERN: ", case[0]) print("FILES: ", case[1]) print("FLAGS: ", bin(flags)) value = fnmatch.filter(case[1], case[0], flags=flags) print("TEST: ", value, '<=>', case[2], '\n') cls.assert_equal(value, case[2]) @pytest.mark.parametrize("case", cases) def test_cases(self, case): """Test case.""" self.evaluate(case) class TestFnMatchTranslate(unittest.TestCase): """ Test translation cases. All these cases assume `DOTMATCH` is enabled. """ def setUp(self): """Setup the tests.""" self.flags = fnmatch.DOTMATCH def split_translate(self, pattern, flags): """Translate pattern to regex after splitting.""" return fnmatch.translate(pattern, flags=flags | fnmatch.SPLIT) def test_capture_groups(self): """Test capture groups.""" gpat = fnmatch.translate("test @(this) +(many) ?(meh)*(!) !(not this)@(.md)", flags=fnmatch.E) pat = re.compile(gpat[0][0]) match = pat.match('test this manymanymany meh!!!!! okay.md') self.assertEqual(('this', 'manymanymany', 'meh', '!!!!!', 'okay', '.md'), match.groups()) def test_nested_capture_groups(self): """Test nested capture groups.""" gpat = fnmatch.translate("@(file)@(+([[:digit:]]))@(.*)", flags=fnmatch.E) pat = re.compile(gpat[0][0]) match = pat.match('file33.test.txt') self.assertEqual(('file', '33', '33', '.test.txt'), match.groups()) def test_list_groups(self): """Test capture groups with lists.""" gpat = fnmatch.translate("+(f|i|l|e)+([[:digit:]])@(.*)", flags=fnmatch.E) pat = re.compile(gpat[0][0]) match = pat.match('file33.test.txt') self.assertEqual(('file', '33', '.test.txt'), match.groups()) def test_split_parsing(self): """Test wildcard parsing.""" _wcparse._compile.cache_clear() flags = self.flags | fnmatch.FORCEUNIX p1, p2 = self.split_translate('*test[a-z]?|*test2[a-z]?|!test[!a-z]|!test[!-|a-z]', flags | fnmatch.N) self.assertEqual(p1, [r'^(?s:(?=.).*?test[a-z].)$', r'^(?s:(?=.).*?test2[a-z].)$']) self.assertEqual(p2, [r'^(?s:test[^a-z])$', r'^(?s:test[^\-\|a-z])$']) p1, p2 = self.split_translate('test[]][!][][]', flags | fnmatch.U | fnmatch.C) self.assertEqual(p1, [r'^(?s:test[\]][^\][]\[\])$']) self.assertEqual(p2, []) p1, p2 = self.split_translate('test[!]', flags) self.assertEqual(p1, [r'^(?s:test\[!\])$']) self.assertEqual(p2, []) p1, p2 = self.split_translate('|test|', flags) self.assertEqual(p1, [r'^(?s:)$', r'^(?s:test)$']) self.assertEqual(p2, []) p1, p2 = self.split_translate('-|-test|-', flags=flags | fnmatch.N | fnmatch.M) self.assertEqual(p1, []) self.assertEqual(p2, [r'^(?s:)$', r'^(?s:test)$']) p1, p2 = self.split_translate('test[^chars]', flags) self.assertEqual(p1, [r'^(?s:test[^chars])$']) self.assertEqual(p2, []) p1 = self.split_translate(R'test[^\\-\\&]', flags=flags)[0] self.assertEqual(p1, [r'^(?s:test[^\\-\\\&])$']) p1 = self.split_translate(R'\\*\\?\\|\\[\\]', flags=flags)[0] self.assertEqual(p1, [r'^(?s:\\.*?\\.\\)$', r'^(?s:\\[\\])$']) p1 = self.split_translate(R'\\u0300', flags=flags | fnmatch.R)[0] self.assertEqual(p1, [r'^(?s:\\u0300)$']) def test_posix_range(self): """Test posix range.""" p = fnmatch.translate(R'[[:ascii:]-z]', flags=self.flags | fnmatch.U | fnmatch.C) self.assertEqual(p, (['^(?s:[\x00-\x7f\\-z])$'], [])) p = fnmatch.translate(R'[a-[:ascii:]-z]', flags=self.flags | fnmatch.U | fnmatch.C) self.assertEqual(p, (['^(?s:[a\\-\x00-\x7f\\-z])$'], [])) @mock.patch('wcmatch.util.is_case_sensitive') def test_special_escapes(self, mock__iscase_sensitive): """Test wildcard character notations.""" flags = self.flags | fnmatch.U _wcparse._compile.cache_clear() p1, p2 = fnmatch.translate( R'test\x70\u0070\U00000070\160\N{LATIN SMALL LETTER P}', flags=flags | fnmatch.R ) self.assertEqual(p1, [r'^(?s:testppppp)$']) self.assertEqual(p2, []) p1, p2 = fnmatch.translate( R'test[\x70][\u0070][\U00000070][\160][\N{LATIN SMALL LETTER P}]', flags=flags | fnmatch.R ) self.assertEqual(p1, [r'^(?s:test[p][p][p][p][p])$']) self.assertEqual(p2, []) p1, p2 = fnmatch.translate(R'test\t\m', flags=flags | fnmatch.R) self.assertEqual(p1, [r'^(?s:test\ m)$']) self.assertEqual(p2, []) p1, p2 = fnmatch.translate(R'test[\\]test', flags=flags | fnmatch.R) self.assertEqual(p1, [r'^(?s:test[\\]test)$']) self.assertEqual(p2, []) p1, p2 = fnmatch.translate('test[\\', flags=flags) self.assertEqual(p1, [r'^(?s:test\[)$']) self.assertEqual(p2, []) p1, p2 = fnmatch.translate(R'test\44test', flags=flags | fnmatch.R) self.assertEqual(p1, [r'^(?s:test\$test)$']) self.assertEqual(p2, []) p1, p2 = fnmatch.translate(R'test\44', flags=flags | fnmatch.R) self.assertEqual(p1, [r'^(?s:test\$)$']) self.assertEqual(p2, []) p1, p2 = fnmatch.translate(R'test\400', flags=flags | fnmatch.R) self.assertEqual(p1, [r'^(?s:testÄ€)$']) self.assertEqual(p2, []) with pytest.raises(SyntaxError): fnmatch.translate(R'test\N', flags=flags | fnmatch.R) with pytest.raises(SyntaxError): fnmatch.translate(R'test\Nx', flags=flags | fnmatch.R) with pytest.raises(SyntaxError): fnmatch.translate(R'test\N{', flags=flags | fnmatch.R) def test_default_compile(self): """Test default with exclusion.""" self.assertTrue(fnmatch.fnmatch('name', '!test', flags=fnmatch.N | fnmatch.A)) self.assertTrue(fnmatch.fnmatch(b'name', b'!test', flags=fnmatch.N | fnmatch.A)) self.assertFalse(fnmatch.fnmatch('test', '!test', flags=fnmatch.N | fnmatch.A)) self.assertFalse(fnmatch.fnmatch(b'test', b'!test', flags=fnmatch.N | fnmatch.A)) def test_default_translate(self): """Test default with exclusion in translation.""" self.assertTrue(len(fnmatch.translate('!test', flags=fnmatch.N | fnmatch.A)[0]) == 1) self.assertTrue(len(fnmatch.translate(b'!test', flags=fnmatch.N | fnmatch.A)[0]) == 1) class TestExcludes(unittest.TestCase): """Test expansion limits.""" def test_translate_exclude(self): """Test exclusion in translation.""" results = fnmatch.translate('*', exclude='test') self.assertTrue(len(results[0]) == 1 and len(results[1]) == 1) results = fnmatch.translate(b'*', exclude=b'test') self.assertTrue(len(results[0]) == 1 and len(results[1]) == 1) def test_translate_exclude_mix(self): """ Test translate exclude mix. If both are given, flags are ignored. """ results = fnmatch.translate(['*', '!test'], exclude=b'test', flags=fnmatch.N | fnmatch.A) self.assertTrue(len(results[0]) == 2 and len(results[1]) == 1) def test_exclude(self): """Test exclude parameter.""" self.assertTrue(fnmatch.fnmatch('name', '*', exclude='test')) self.assertTrue(fnmatch.fnmatch(b'name', b'*', exclude=b'test')) self.assertFalse(fnmatch.fnmatch('test', '*', exclude='test')) self.assertFalse(fnmatch.fnmatch(b'test', b'*', exclude=b'test')) def test_exclude_mix(self): """ Test exclusion flags mixed with exclusion parameter. If both are given, flags are ignored. """ self.assertTrue(fnmatch.fnmatch('name', '*', exclude='test', flags=fnmatch.N | fnmatch.A)) self.assertTrue(fnmatch.fnmatch(b'name', b'*', exclude=b'test', flags=fnmatch.N | fnmatch.A)) self.assertFalse(fnmatch.fnmatch('test', '*', exclude='test', flags=fnmatch.N | fnmatch.A)) self.assertFalse(fnmatch.fnmatch(b'test', b'*', exclude=b'test', flags=fnmatch.N | fnmatch.A)) self.assertTrue(fnmatch.fnmatch('name', ['*', '!name'], exclude='test', flags=fnmatch.N | fnmatch.A)) self.assertFalse(fnmatch.fnmatch('test', ['*', '!name'], exclude='test', flags=fnmatch.N | fnmatch.A)) self.assertTrue(fnmatch.fnmatch('!name', ['*', '!name'], exclude='test', flags=fnmatch.N | fnmatch.A)) def test_filter(self): """Test exclusion with filter.""" self.assertEqual(fnmatch.filter(['name', 'test'], '*', exclude='test'), ['name']) class TestIsMagic(unittest.TestCase): """Test "is magic" logic.""" def test_default(self): """Test default magic.""" self.assertTrue(fnmatch.is_magic("test*")) self.assertTrue(fnmatch.is_magic("test[")) self.assertTrue(fnmatch.is_magic("test]")) self.assertTrue(fnmatch.is_magic("test?")) self.assertTrue(fnmatch.is_magic("test\\")) self.assertFalse(fnmatch.is_magic("test~!()-/|{}")) def test_extmatch(self): """Test extended match magic.""" self.assertTrue(fnmatch.is_magic("test*", flags=fnmatch.EXTMATCH)) self.assertTrue(fnmatch.is_magic("test[", flags=fnmatch.EXTMATCH)) self.assertTrue(fnmatch.is_magic("test]", flags=fnmatch.EXTMATCH)) self.assertTrue(fnmatch.is_magic("test?", flags=fnmatch.EXTMATCH)) self.assertTrue(fnmatch.is_magic("test\\", flags=fnmatch.EXTMATCH)) self.assertTrue(fnmatch.is_magic("test(", flags=fnmatch.EXTMATCH)) self.assertTrue(fnmatch.is_magic("test)", flags=fnmatch.EXTMATCH)) self.assertFalse(fnmatch.is_magic("test~!-/|{}", flags=fnmatch.EXTMATCH)) def test_negate(self): """Test negate magic.""" self.assertTrue(fnmatch.is_magic("test*", flags=fnmatch.NEGATE)) self.assertTrue(fnmatch.is_magic("test[", flags=fnmatch.NEGATE)) self.assertTrue(fnmatch.is_magic("test]", flags=fnmatch.NEGATE)) self.assertTrue(fnmatch.is_magic("test?", flags=fnmatch.NEGATE)) self.assertTrue(fnmatch.is_magic("test\\", flags=fnmatch.NEGATE)) self.assertTrue(fnmatch.is_magic("test!", flags=fnmatch.NEGATE)) self.assertFalse(fnmatch.is_magic("test~()-/|{}", flags=fnmatch.NEGATE)) def test_minusnegate(self): """Test minus negate magic.""" self.assertTrue(fnmatch.is_magic("test*", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE)) self.assertTrue(fnmatch.is_magic("test[", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE)) self.assertTrue(fnmatch.is_magic("test]", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE)) self.assertTrue(fnmatch.is_magic("test?", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE)) self.assertTrue(fnmatch.is_magic("test\\", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE)) self.assertTrue(fnmatch.is_magic("test-", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE)) self.assertFalse(fnmatch.is_magic("test~()!/|{}", flags=fnmatch.NEGATE | fnmatch.MINUSNEGATE)) def test_brace(self): """Test brace magic.""" self.assertTrue(fnmatch.is_magic("test*", flags=fnmatch.BRACE)) self.assertTrue(fnmatch.is_magic("test[", flags=fnmatch.BRACE)) self.assertTrue(fnmatch.is_magic("test]", flags=fnmatch.BRACE)) self.assertTrue(fnmatch.is_magic("test?", flags=fnmatch.BRACE)) self.assertTrue(fnmatch.is_magic("test\\", flags=fnmatch.BRACE)) self.assertTrue(fnmatch.is_magic("test{", flags=fnmatch.BRACE)) self.assertTrue(fnmatch.is_magic("test}", flags=fnmatch.BRACE)) self.assertFalse(fnmatch.is_magic("test~!-/|", flags=fnmatch.BRACE)) def test_split(self): """Test split magic.""" self.assertTrue(fnmatch.is_magic("test*", flags=fnmatch.SPLIT)) self.assertTrue(fnmatch.is_magic("test[", flags=fnmatch.SPLIT)) self.assertTrue(fnmatch.is_magic("test]", flags=fnmatch.SPLIT)) self.assertTrue(fnmatch.is_magic("test?", flags=fnmatch.SPLIT)) self.assertTrue(fnmatch.is_magic("test\\", flags=fnmatch.SPLIT)) self.assertTrue(fnmatch.is_magic("test|", flags=fnmatch.SPLIT)) self.assertFalse(fnmatch.is_magic("test~()-!/", flags=fnmatch.SPLIT)) def test_all(self): """Test tilde magic.""" flags = ( fnmatch.EXTMATCH | fnmatch.NEGATE | fnmatch.BRACE | fnmatch.SPLIT ) self.assertTrue(fnmatch.is_magic("test*", flags=flags)) self.assertTrue(fnmatch.is_magic("test[", flags=flags)) self.assertTrue(fnmatch.is_magic("test]", flags=flags)) self.assertTrue(fnmatch.is_magic("test?", flags=flags)) self.assertTrue(fnmatch.is_magic(R"te\\st", flags=flags)) self.assertTrue(fnmatch.is_magic(R"te\st", flags=flags)) self.assertTrue(fnmatch.is_magic("test!", flags=flags)) self.assertTrue(fnmatch.is_magic("test|", flags=flags)) self.assertTrue(fnmatch.is_magic("test(", flags=flags)) self.assertTrue(fnmatch.is_magic("test)", flags=flags)) self.assertTrue(fnmatch.is_magic("test{", flags=flags)) self.assertTrue(fnmatch.is_magic("test}", flags=flags)) self.assertTrue(fnmatch.is_magic("test-", flags=flags | fnmatch.MINUSNEGATE)) self.assertFalse(fnmatch.is_magic("test-~", flags=flags)) self.assertFalse(fnmatch.is_magic("test!~", flags=flags | fnmatch.MINUSNEGATE)) def test_all_bytes(self): """Test tilde magic.""" flags = ( fnmatch.EXTMATCH | fnmatch.NEGATE | fnmatch.BRACE | fnmatch.SPLIT ) self.assertTrue(fnmatch.is_magic(b"test*", flags=flags)) self.assertTrue(fnmatch.is_magic(b"test[", flags=flags)) self.assertTrue(fnmatch.is_magic(b"test]", flags=flags)) self.assertTrue(fnmatch.is_magic(b"test?", flags=flags)) self.assertTrue(fnmatch.is_magic(rb"te\\st", flags=flags)) self.assertTrue(fnmatch.is_magic(rb"te\st", flags=flags)) self.assertTrue(fnmatch.is_magic(b"test!", flags=flags)) self.assertTrue(fnmatch.is_magic(b"test|", flags=flags)) self.assertTrue(fnmatch.is_magic(b"test(", flags=flags)) self.assertTrue(fnmatch.is_magic(b"test)", flags=flags)) self.assertTrue(fnmatch.is_magic(b"test{", flags=flags)) self.assertTrue(fnmatch.is_magic(b"test}", flags=flags)) self.assertTrue(fnmatch.is_magic(b"test-", flags=flags | fnmatch.MINUSNEGATE)) self.assertFalse(fnmatch.is_magic(b"test-~", flags=flags)) self.assertFalse(fnmatch.is_magic(b"test!~", flags=flags | fnmatch.MINUSNEGATE)) class TestFnMatchEscapes(unittest.TestCase): """Test escaping.""" def check_escape(self, arg, expected, unix=None, raw_chars=True): """Verify escapes.""" flags = 0 if unix is False: flags = fnmatch.FORCEWIN elif unix is True: flags = fnmatch.FORCEUNIX self.assertEqual(fnmatch.escape(arg), expected) self.assertEqual(fnmatch.escape(os.fsencode(arg)), os.fsencode(expected)) self.assertTrue( fnmatch.fnmatch( arg, fnmatch.escape(arg), flags=flags ) ) def test_escape(self): """Test path escapes.""" check = self.check_escape check('abc', 'abc') check('[', R'\[') check('?', R'\?') check('*', R'\*') check('[[_/*?*/_]]', R'\[\[_/\*\?\*/_\]\]') check('/[[_/*?*/_]]/', R'/\[\[_/\*\?\*/_\]\]/') @unittest.skipUnless(sys.platform.startswith('win'), "Windows specific test") def test_escape_windows(self): """Test windows escapes.""" check = self.check_escape # `fnmatch` doesn't care about drives check('a:\\?', R'a:\\\?') check('b:\\*', R'b:\\\*') check('\\\\?\\c:\\?', R'\\\\\?\\c:\\\?') check('\\\\*\\*\\*', R'\\\\\*\\\*\\\*') check('//?/c:/?', R'//\?/c:/\?') check('//*/*/*', R'//\*/\*/\*') check('//[^what]/name/temp', R'//\[^what\]/name/temp') def test_escape_forced_windows(self): """Test forced windows escapes.""" check = self.check_escape # `fnmatch` doesn't care about drives check('a:\\?', R'a:\\\?', unix=False) check('b:\\*', R'b:\\\*', unix=False) check('\\\\?\\c:\\?', R'\\\\\?\\c:\\\?', unix=True) check('\\\\*\\*\\*', R'\\\\\*\\\*\\\*', unix=True) check('//?/c:/?', R'//\?/c:/\?', unix=True) check('//*/*/*', R'//\*/\*/\*', unix=True) check('//[^what]/name/temp', R'//\[^what\]/name/temp', unix=True) def test_escape_forced_unix(self): """Test forced windows Unix.""" check = self.check_escape # `fnmatch` doesn't care about drives check('a:\\?', R'a:\\\?', unix=True) check('b:\\*', R'b:\\\*', unix=True) check('\\\\?\\c:\\?', R'\\\\\?\\c:\\\?', unix=True) check('\\\\*\\*\\*', R'\\\\\*\\\*\\\*', unix=True) check('//?/c:/?', R'//\?/c:/\?', unix=True) check('//*/*/*', R'//\*/\*/\*', unix=True) check('//[^what]/name/temp', R'//\[^what\]/name/temp', unix=True) class TestExpansionLimit(unittest.TestCase): """Test expansion limits.""" def test_limit_fnmatch(self): """Test expansion limit of `fnmatch`.""" with self.assertRaises(_wcparse.PatternLimitException): fnmatch.fnmatch('name', '{1..11}', flags=fnmatch.BRACE, limit=10) def test_limit_filter(self): """Test expansion limit of `filter`.""" with self.assertRaises(_wcparse.PatternLimitException): fnmatch.filter(['name'], '{1..11}', flags=fnmatch.BRACE, limit=10) def test_limit_translate(self): """Test expansion limit of `translate`.""" with self.assertRaises(_wcparse.PatternLimitException): fnmatch.translate('{1..11}', flags=fnmatch.BRACE, limit=10) class TestTypes(unittest.TestCase): """Test basic sequences.""" def test_match_set(self): """Test `set` matching.""" self.assertTrue(fnmatch.fnmatch('a', {'a'})) def test_match_tuple(self): """Test `tuple` matching.""" self.assertTrue(fnmatch.fnmatch('a', ('a',))) def test_match_list(self): """Test `list` matching.""" self.assertTrue(fnmatch.fnmatch('a', ['a'])) wcmatch-10.0/tests/test_glob.py000066400000000000000000001714411467532413500166000ustar00rootroot00000000000000""" These test cases are taken straight from `cpython` to ensure our glob works as good as the builtin. Matches close to the normal glob implementation, but there are a few consciously made difference in implementation. 1. Our glob will often return things like `.` and `..` matching Bash's behavior. 2. We do not normalize out `.` and `..`, so the norm function below just joins. 3. We escape with backslashes not `[]`. 4. A Window's path separator will be two backslashes in a pattern due to escape logic, not one. """ import contextlib from wcmatch import glob from wcmatch import pathlib from wcmatch import _wcparse from wcmatch import util import re import types import pytest import os import shutil import sys import unittest import warnings import getpass from collections.abc import MutableMapping PY310 = (3, 10) <= sys.version_info # Below is general helper stuff that Python uses in `unittests`. As these # not meant for users, and could change without notice, include them # ourselves so we aren't surprised later. TESTFN = '@test' # Disambiguate `TESTFN` for parallel testing, while letting it remain a valid # module name. TESTFN = "{}_{}_tmp".format(TESTFN, os.getpid()) class EnvironmentVarGuard(MutableMapping): """ Class to help protect the environment variable properly. Can be used as a context manager. Directly ripped from Python support tools so that we can actually try and test `~user` expansion. Taken from: https://github.com/python/cpython/blob/main/Lib/test/support/os_helper.py. """ def __init__(self): """Initialize.""" self._environ = os.environ self._changed = {} def __getitem__(self, envvar): """Get item.""" return self._environ[envvar] def __setitem__(self, envvar, value): """Set item.""" # Remember the initial value on the first access if envvar not in self._changed: self._changed[envvar] = self._environ.get(envvar) self._environ[envvar] = value def __delitem__(self, envvar): """Delete item.""" # Remember the initial value on the first access if envvar not in self._changed: self._changed[envvar] = self._environ.get(envvar) if envvar in self._environ: del self._environ[envvar] def keys(self): """Get keys.""" return self._environ.keys() def __iter__(self): """Iterate.""" return iter(self._environ) def __len__(self): """Get length.""" return len(self._environ) def set(self, envvar, value): # noqa: A003 """Set variable.""" self[envvar] = value def unset(self, envvar): """Unset variable.""" del self[envvar] def copy(self): """Copy environment.""" # We do what `os.environ.copy()` does. return dict(self) def __enter__(self): """Enter.""" return self def __exit__(self, *ignore_exc): """Exit.""" for (k, v) in self._changed.items(): if v is None: if k in self._environ: del self._environ[k] else: self._environ[k] = v os.environ = self._environ # noqa: B003 @contextlib.contextmanager def change_cwd(path, quiet=False): """ Return a context manager that changes the current working directory. Arguments: --------- path: the directory to use as the temporary current working directory. quiet: if False (the default), the context manager raises an exception on error. Otherwise, it issues only a warning and keeps the current working directory the same. """ saved_dir = os.getcwd() try: os.chdir(path) except OSError: if not quiet: raise warnings.warn('tests may fail, unable to change CWD to: ' + path, RuntimeWarning, stacklevel=3) try: yield os.getcwd() finally: os.chdir(saved_dir) def create_empty_file(filename): """Create an empty file. If the file already exists, truncate it.""" fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) os.close(fd) _can_symlink = None def can_symlink(): """Check if we can symlink.""" global _can_symlink if _can_symlink is not None: return _can_symlink symlink_path = TESTFN + "can_symlink" try: os.symlink(TESTFN, symlink_path) can = True except (OSError, NotImplementedError, AttributeError): can = False else: os.remove(symlink_path) _can_symlink = can return can def skip_unless_symlink(test): """Skip decorator for tests that require functional symlink.""" ok = can_symlink() msg = "Requires functional symlink implementation" return test if ok else unittest.skip(msg)(test) class Options(): """Test options.""" def __init__(self, **kwargs): """Initialize.""" self._options = kwargs def get(self, key, default=None): """Get option value.""" return self._options.get(key, default) class _TestGlob: """ Test glob. Each list entry in `cases` is run through the `glob`, then the pattern and results are converted to bytes and run trough `glob` again. Results are checked against the provided result list. There are a couple special types that can be inserted in the case list that can alter the behavior of the cases that follow. * `Strings`: These will be printed and then the next case will be processed. * `Options`: This object takes keyword parameters that are used to alter the next tests options: * `absolute`: When joining path parts, due not append to the temporary directory. * `skip`: Skip tests when this is enabled. * `cwd_temp`: Switch the current working directory to the temp directory instead of having to prepend the temp directory to patterns and results. Each test case entry (list) is an array of up to 3 parameters (2 minimum). * Pattern: a list of path parts that are to be joined with the current OS separator. * Expected result (file names matched by the pattern): a list of sub-lists where each sub-list contains path parts that are to be joined with the current OS separator. Each path represents a full file path to match. * Flags The default flags are: `GLOBSTAR` | `EXTGLOB` | `BRACE` | `FOLLOW`. If any of these flags are provided in a test case, they will disable the default of the same name. All other flags will enable flags as expected. """ DEFAULT_FLAGS = glob.BRACE | glob.EXTGLOB | glob.GLOBSTAR | glob.FOLLOW cases = [] @classmethod def norm(cls, *parts): """Normalizes file path (in relation to temp directory).""" return os.path.join(cls.tempdir, *parts) @classmethod def res_norm(cls, *parts, absolute=False, mark=False): """Normalize results adding a trailing slash if mark flag is enabled.""" if not absolute: path = os.path.join(cls.tempdir, *parts) else: path = os.path.join(*parts) if mark and os.path.isdir(os.path.join(cls.tempdir, *parts)): path = os.path.join(path, b'' if isinstance(path, bytes) else '') return path @classmethod def globjoin(cls, *parts): """Joins glob path.""" sep = cls.globsep return sep.join(list(parts)) @classmethod def mktemp(cls, *parts): """Make temp directory.""" filename = cls.norm(*parts) base, file = os.path.split(filename) if not os.path.exists(base): retry = 3 while retry: try: os.makedirs(base) retry = 0 except Exception: # noqa: PERF203 retry -= 1 create_empty_file(filename) @classmethod def setup_class(cls): """Setup.""" cls.default_negate = '**' cls.absolute = False cls.skip = False cls.cwd_temp = False cls.just_negative = False if os.sep == '/': cls.globsep = os.sep else: cls.globsep = r'\\' cls.tempdir = TESTFN + "_dir" cls.setup_fs() @classmethod def setup_fs(cls): """Setup file system.""" @classmethod def teardown_class(cls): """Cleanup.""" retry = 3 while retry: try: shutil.rmtree(cls.tempdir) retry = 0 except Exception: # noqa: PERF203 retry -= 1 @staticmethod def assert_equal(a, b): """Assert equal.""" assert a == b, "Comparison between objects yielded false." @staticmethod def assert_count_equal(a, b): """Assert count equal.""" c1 = len(list(a)) if isinstance(a, types.GeneratorType) else len(a) c2 = len(list(b)) if isinstance(b, types.GeneratorType) else len(b) assert c1 == c2, "Length of %d does not equal %d" % (c1, c2) @classmethod def glob(cls, *parts, **kwargs): """Perform a glob with validation.""" if parts: if len(parts) == 1: p = parts[0] else: p = cls.globjoin(*parts) if not cls.absolute: p = cls.globjoin(cls.tempdir, p) else: p = cls.tempdir res = glob.glob(p, **kwargs) print("RESULTS: ", res) if res: cls.assert_equal({type(r) for r in res}, {str}) cls.assert_count_equal(glob.iglob(p, **kwargs), res) if 'root_dir' in kwargs and kwargs['root_dir'] is not None: kwargs['root_dir'] = os.fsencode(kwargs['root_dir']) bres = [os.fsencode(x) for x in res] cls.assert_count_equal(glob.glob(os.fsencode(p), **kwargs), bres) cls.assert_count_equal(glob.iglob(os.fsencode(p), **kwargs), bres) if bres: cls.assert_equal({type(r) for r in bres}, {bytes}) return res @classmethod def nglob(cls, *parts, **kwargs): """Perform a glob with validation.""" if parts: if len(parts) == 1: p = parts[0] else: p = cls.globjoin(*parts) if not cls.absolute: p = cls.globjoin(cls.tempdir, p) else: p = cls.tempdir p = '!' + p if not cls.just_negative: if not cls.absolute: p = [cls.globjoin(cls.tempdir, cls.default_negate), p] else: p = [cls.default_negate, p] else: p = [p] res = glob.glob(p, **kwargs) print("RESULTS: ", sorted(res)) if res: cls.assert_equal({type(r) for r in res}, {str}) cls.assert_count_equal(glob.iglob(p, **kwargs), res) if 'root_dir' in kwargs and kwargs['root_dir'] is not None: kwargs['root_dir'] = os.fsencode(kwargs['root_dir']) bres = [os.fsencode(x) for x in res] cls.assert_count_equal(glob.glob([os.fsencode(x) for x in p], **kwargs), bres) cls.assert_count_equal(glob.iglob([os.fsencode(x) for x in p], **kwargs), bres) if bres: cls.assert_equal({type(r) for r in bres}, {bytes}) return res @classmethod def assertSequencesEqual_noorder(cls, l1, l2): """Verify lists match (unordered).""" l1 = list(l1) l2 = list(l2) cls.assert_equal(set(l1), set(l2)) cls.assert_equal(sorted(l1), sorted(l2)) @classmethod def eval_glob_cases(cls, case): """Evaluate glob cases.""" eq = cls.assertSequencesEqual_noorder # for case in self.cases: if isinstance(case, Options): absolute = case.get('absolute') if absolute is not None: cls.absolute = absolute skip = case.get('skip') if skip is not None: cls.skip = skip cwd_temp = case.get('cwd_temp') if cwd_temp is not None: cls.cwd_temp = cwd_temp just_negative = case.get('just_negative') if just_negative is not None: cls.just_negative = just_negative default_negate = case.get('default_negate') if default_negate is not None: cls.default_negate = default_negate pytest.skip("Change Options") if cls.skip: pytest.skip("Skipped") pattern = case[0] flags = cls.DEFAULT_FLAGS if len(case) > 2: flags ^= case[2] negative = flags & glob.N results = [ cls.res_norm(*x, absolute=cls.absolute, mark=flags & glob.MARK) for x in case[1] ] if case[1] is not None else None print("PATTERN: ", pattern) print("FLAGS: ", bin(flags)) print("NEGATIVE: ", bin(negative)) print("EXPECTED: ", sorted(results) if results is not None else results) if cls.cwd_temp: if negative: res = cls.nglob(*pattern, flags=flags, root_dir=cls.tempdir) else: res = cls.glob(*pattern, flags=flags, root_dir=cls.tempdir) else: res = cls.nglob(*pattern, flags=flags) if negative else cls.glob(*pattern, flags=flags) if results is not None: eq(res, results) print('\n') class Testglob(_TestGlob): """ Test glob. See `_TestGlob` class for more information in regards to test case format. """ cases = [ # Test literal. [('a',), [('a',)]], [('a', 'D'), [('a', 'D')]], [('aab',), [('aab',)]], [('zymurgy',), []], Options(absolute=True), [['*'], None], [[os.curdir, '*'], None], Options(absolute=False), # Glob one directory [('a*',), [('a',), ('aab',), ('aaa',)]], [('*a',), [('a',), ('aaa',)]], [('.*',), [('.',), ('..',), ('.aa',), ('.bb',)], glob.SCANDOTDIR], [('.*',), [('.aa',), ('.bb',)]], [('?aa',), [('aaa',)]], [('aa?',), [('aaa',), ('aab',)]], [('aa[ab]',), [('aaa',), ('aab',)]], [('*q',), []], [('.',), [('.',)]], [('?',), [('a',)]], [('[.a]',), [('a',)]], [('*.',), []], # Glob with braces [('{a*,a*}',), [('a',), ('aab',), ('aaa',)]], # Glob with braces and "unique" turned off [('{a*,a*}',), [('a',), ('aab',), ('aaa',), ('a',), ('aab',), ('aaa',)], glob.Q], # Test recursive glob logic with no symlink following. [ ('**', '*'), [ ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',) ] if not can_symlink() else [ ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',), ('sym1',), ('sym2',), ('sym3',) ], glob.L ], [ ('**',), [ ('',), ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',) ] if not can_symlink() else [ ('',), ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',), ('sym1',), ('sym2',), ('sym3',) ], glob.L ], Options(default_negate='**'), # Glob inverse [ ('a*', '**'), [ ('EF',), ('ZZZ',), ('',) ] if not can_symlink() else [ ('EF',), ('ZZZ',), ('',), ('sym1',), ('sym3',), ('sym2',), ('sym3', 'efg'), ('sym3', 'efg', 'ha'), ('sym3', 'EF') ], glob.N ], Options(default_negate='sym3/EF'), [ ('**', 'EF'), [ ] if not can_symlink() else [ ], glob.N | glob.L ], [ ('**', 'EF'), [ ] if not can_symlink() else [ ], glob.N ], Options(default_negate='**'), # Disable symlinks [ ('a*', '**'), [ ('EF',), ('ZZZ',), ('',) ] if not can_symlink() else [ ('EF',), ('ZZZ',), ('',), ('sym1',), ('sym2',), ('sym3',) ], glob.N | glob.L ], Options(cwd_temp=True, absolute=True), # Test base matching [ ('*',), [ ('EF',), ('ZZZ',), ('a',), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('aaa',), ('aaa', 'zzzF'), ('aab',), ('aab', 'F') ] if not can_symlink() else [ ('EF',), ('ZZZ',), ('a',), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('aaa',), ('aaa', 'zzzF'), ('aab',), ('aab', 'F'), ('sym1',), ('sym2',), ('sym3',) ], glob.L | glob.X ], # Test that base matching does not occur with a path pattern [ ('aab', '*'), [('aab', 'F')], glob.L | glob.X ], Options(cwd_temp=False, absolute=False), [ ('**',), [ ('a', 'bcd', 'EF',), ('a', 'bcd', 'efg', 'ha',), ('a', 'D',), ('aaa', 'zzzF',), ('aab', 'F',), ('EF',), ('ZZZ',) ] if not can_symlink() else [ ('a', 'bcd', 'EF',), ('a', 'bcd', 'efg', 'ha',), ('a', 'D',), ('aaa', 'zzzF',), ('aab', 'F',), ('EF',), ('sym1',), ('sym2',), ('ZZZ',) ], glob.L | glob.O ], # Test nested glob directory [ ('a', 'bcd', 'E*'), [('a', 'bcd', 'EF')] if util.is_case_sensitive() else [('a', 'bcd', 'EF'), ('a', 'bcd', 'efg')] ], [('a', 'bcd', '*g'), [('a', 'bcd', 'efg')]], # Test case sensitive and insensitive [ ('a', 'bcd', 'E*'), [('a', 'bcd', 'EF')], glob.C ], [ ('a', 'bcd', 'E*'), [('a', 'bcd', 'EF'), ('a', 'bcd', 'efg')], glob.I ], # Test glob directory names. [('*', 'D'), [('a', 'D')]], [('*', '*a'), []], [('a', '*', '*', '*a'), [('a', 'bcd', 'efg', 'ha')]], [('?a?', '*F'), [('aaa', 'zzzF'), ('aab', 'F')]], # Test glob magic in drive name. Options(absolute=True, skip=sys.platform != "win32"), [('*:',), []], [('?:',), []], [(R'\\\\?\\c:\\',), [('\\\\?\\c:\\',)]], [(R'\\\\*\\*\\',), []], Options(absolute=False, skip=False), Options(skip=not can_symlink()), # Test broken symlinks [('sym*',), [('sym1',), ('sym2',), ('sym3',)]], [('sym1',), [('sym1',)]], [('sym2',), [('sym2',)]], # Test glob symlinks., [('sym3',), [('sym3',)]], [('sym3', '*'), [('sym3', 'EF'), ('sym3', 'efg')]], [('sym3', ''), [('sym3', '')]], [('*', '*F'), [('aaa', 'zzzF'), ('aab', 'F'), ('sym3', 'EF')]], Options(skip=False), # Test only directories [ ('*', ''), [ ('aab', ''), ('aaa', ''), ('a', '') ] if not can_symlink() else [ ('aab', ''), ('aaa', ''), ('a', ''), ('sym3', '') ] ], Options(skip=util.is_case_sensitive()), [ ('*\\',), [ ("a",), ("aab",), ("aaa",), ("ZZZ",), ("EF",) ] if not can_symlink() else [ ("a",), ("aab",), ("aaa",), ("ZZZ",), ("EF",), ('sym1',), ('sym2',), ('sym3',) ] ], [ (R'*\\',), [ ('aab', ''), ('aaa', ''), ('a', '') ] if not can_symlink() else [ ('aab', ''), ('aaa', ''), ('a', ''), ('sym3', '') ] ], Options(skip=False), # Test `extglob`. [('@(a|aa*(a|b))',), [('aab',), ('aaa',), ('a',)]], # Test sequences. [('[a]',), [('a',)]], [('[!b]',), [('a',)]], [('[^b]',), [('a',)]], [(R'@([\a]|\aaa)',), [('a',), ('aaa',)]], Options(absolute=True), # Test empty. [('',), []], Options(absolute=False), # Patterns ending with a slash shouldn't match non-directories. [('Z*Z', ''), []], [('ZZZ', ''), []], [('aa*', ''), [('aaa', ''), ('aab', '')]], # Test recursion. [ ('**',), [ ('',), ('EF',), ('ZZZ',), ('a',), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('aaa',), ('aaa', 'zzzF'), ('aab',), ('aab', 'F') ] if not can_symlink() else [ ('',), ('EF',), ('ZZZ',), ('a',), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('aaa',), ('aaa', 'zzzF'), ('aab',), ('aab', 'F'), ('sym1',), ('sym2',), ('sym3',), ('sym3', 'EF'), ('sym3', 'efg'), ('sym3', 'efg', 'ha') ] ], [ ('**', '**'), [ ('',), ('EF',), ('ZZZ',), ('a',), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('aaa',), ('aaa', 'zzzF'), ('aab',), ('aab', 'F'), ] if not can_symlink() else [ ('',), ('EF',), ('ZZZ',), ('a',), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('aaa',), ('aaa', 'zzzF'), ('aab',), ('aab', 'F'), ('sym1',), ('sym2',), ('sym3',), ('sym3', 'EF'), ('sym3', 'efg'), ('sym3', 'efg', 'ha') ] ], [ ('.', '**'), [ ('.', ''), ('.', 'EF'), ('.', 'ZZZ'), ('.', 'a'), ('.', 'a', 'D'), ('.', 'a', 'bcd'), ('.', 'a', 'bcd', 'EF'), ('.', 'a', 'bcd', 'efg'), ('.', 'a', 'bcd', 'efg', 'ha'), ('.', 'aaa'), ('.', 'aaa', 'zzzF'), ('.', 'aab'), ('.', 'aab', 'F'), ] if not can_symlink() else [ ('.', ''), ('.', 'EF'), ('.', 'ZZZ'), ('.', 'a'), ('.', 'a', 'D'), ('.', 'a', 'bcd'), ('.', 'a', 'bcd', 'EF'), ('.', 'a', 'bcd', 'efg'), ('.', 'a', 'bcd', 'efg', 'ha'), ('.', 'aaa'), ('.', 'aaa', 'zzzF'), ('.', 'aab'), ('.', 'aab', 'F'), ('.', 'sym1'), ('.', 'sym2'), ('.', 'sym3'), ('.', 'sym3', 'EF'), ('.', 'sym3', 'efg'), ('.', 'sym3', 'efg', 'ha') ] ], [ ('**', ''), # Directories [ ('',), ('a', ''), ('a', 'bcd', ''), ('a', 'bcd', 'efg', ''), ('aaa', ''), ('aab', '') ] if not can_symlink() else [ ('',), ('a', ''), ('a', 'bcd', ''), ('a', 'bcd', 'efg', ''), ('aaa', ''), ('aab', ''), ('sym3', ''), ('sym3', 'efg', '') ] ], [ ('a', '**'), [('a', ''), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha')] ], [('a**',), [('a',), ('aaa',), ('aab',)]], [ ('**', 'EF'), [('a', 'bcd', 'EF'), ('EF',)] if not can_symlink() else [('a', 'bcd', 'EF'), ('EF',), ('sym3', 'EF')] ], [ ('**', '*F'), [ ('a', 'bcd', 'EF'), ('aaa', 'zzzF'), ('aab', 'F'), ('EF',) ] if not can_symlink() else [ ('a', 'bcd', 'EF'), ('aaa', 'zzzF'), ('aab', 'F'), ('EF',), ('sym3', 'EF') ] ], [('**', '*F', ''), []], [('**', 'bcd', '*'), [('a', 'bcd', 'EF'), ('a', 'bcd', 'efg')]], [('a', '**', 'bcd'), [('a', 'bcd')]], Options(cwd_temp=True, absolute=True), [ ('**',), [ ('EF',), ('ZZZ',), ('a',), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('aaa',), ('aaa', 'zzzF'), ('aab',), ('aab', 'F') ] if not can_symlink() else [ ('EF',), ('ZZZ',), ('a',), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('aaa',), ('aaa', 'zzzF'), ('aab',), ('aab', 'F'), ('sym1',), ('sym2',), ('sym3',), ('sym3', 'EF'), ('sym3', 'efg'), ('sym3', 'efg', 'ha') ] ], [ ('**', '*'), [ ('EF',), ('ZZZ',), ('a',), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('aaa',), ('aaa', 'zzzF'), ('aab',), ('aab', 'F') ] if not can_symlink() else [ ('EF',), ('ZZZ',), ('a',), ('a', 'D'), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('aaa',), ('aaa', 'zzzF'), ('aab',), ('aab', 'F'), ('sym1',), ('sym2',), ('sym3',), ('sym3', 'EF'), ('sym3', 'efg'), ('sym3', 'efg', 'ha') ] ], [ (os.curdir, '**'), [ ('.', ''), ('.', 'EF'), ('.', 'ZZZ'), ('.', 'a',), ('.', 'a', 'D'), ('.', 'a', 'bcd'), ('.', 'a', 'bcd', 'EF'), ('.', 'a', 'bcd', 'efg'), ('.', 'a', 'bcd', 'efg', 'ha'), ('.', 'aaa'), ('.', 'aaa', 'zzzF'), ('.', 'aab'), ('.', 'aab', 'F') ] if not can_symlink() else [ ('.', ''), ('.', 'EF',), ('.', 'ZZZ'), ('.', 'a',), ('.', 'a', 'D'), ('.', 'a', 'bcd'), ('.', 'a', 'bcd', 'EF'), ('.', 'a', 'bcd', 'efg'), ('.', 'a', 'bcd', 'efg', 'ha'), ('.', 'aaa'), ('.', 'aaa', 'zzzF'), ('.', 'aab'), ('.', 'aab', 'F'), ('.', 'sym1'), ('.', 'sym2'), ('.', 'sym3'), ('.', 'sym3', 'EF'), ('.', 'sym3', 'efg'), ('.', 'sym3', 'efg', 'ha') ] ], [ (os.curdir, '**', '*'), [ ('.', 'EF'), ('.', 'ZZZ'), ('.', 'a',), ('.', 'a', 'D'), ('.', 'a', 'bcd'), ('.', 'a', 'bcd', 'EF'), ('.', 'a', 'bcd', 'efg'), ('.', 'a', 'bcd', 'efg', 'ha'), ('.', 'aaa'), ('.', 'aaa', 'zzzF'), ('.', 'aab'), ('.', 'aab', 'F') ] if not can_symlink() else [ ('.', 'EF',), ('.', 'ZZZ'), ('.', 'a',), ('.', 'a', 'D'), ('.', 'a', 'bcd'), ('.', 'a', 'bcd', 'EF'), ('.', 'a', 'bcd', 'efg'), ('.', 'a', 'bcd', 'efg', 'ha'), ('.', 'aaa'), ('.', 'aaa', 'zzzF'), ('.', 'aab'), ('.', 'aab', 'F'), ('.', 'sym1'), ('.', 'sym2'), ('.', 'sym3'), ('.', 'sym3', 'EF'), ('.', 'sym3', 'efg'), ('.', 'sym3', 'efg', 'ha') ] ], [ ('**', ''), [ ('a', ''), ('a', 'bcd', ''), ('a', 'bcd', 'efg', ''), ('aaa', ''), ('aab', '') ] if not can_symlink() else [ ('a', ''), ('a', 'bcd', ''), ('a', 'bcd', 'efg', ''), ('aaa', ''), ('aab', ''), ('sym3', ''), ('sym3', 'efg', '') ] ], [ (os.curdir, '**', ''), [ ('.', ''), ('.', 'a', ''), ('.', 'a', 'bcd', ''), ('.', 'a', 'bcd', 'efg', ''), ('.', 'aaa', ''), ('.', 'aab', '') ] if not can_symlink() else [ ('.', ''), ('.', 'a', ''), ('.', 'a', 'bcd', ''), ('.', 'a', 'bcd', 'efg', ''), ('.', 'aaa', ''), ('.', 'aab', ''), ('.', 'sym3', ''), ('.', 'sym3', 'efg', '') ] ], [('**', 'zz*F'), [('aaa', 'zzzF')]], [('**zz*F',), []], [ ('**', 'EF'), [('a', 'bcd', 'EF'), ('EF',)] if not can_symlink() else [('a', 'bcd', 'EF'), ('EF',), ('sym3', 'EF')] ], Options(just_negative=True, default_negate='**'), [ ('a*', '**'), [ ] if not can_symlink() else [ ], glob.N ], Options(just_negative=False, cwd_temp=False, absolute=False), # Test the file directly -- without magic. [[], [[]]] ] @classmethod def setup_fs(cls): """Setup file system.""" cls.mktemp('a', 'D') cls.mktemp('aab', 'F') cls.mktemp('.aa', 'G') cls.mktemp('.bb', 'H') cls.mktemp('aaa', 'zzzF') cls.mktemp('ZZZ') cls.mktemp('EF') cls.mktemp('a', 'bcd', 'EF') cls.mktemp('a', 'bcd', 'efg', 'ha') cls.can_symlink = can_symlink() if cls.can_symlink: os.symlink(cls.norm('broken'), cls.norm('sym1')) os.symlink('broken', cls.norm('sym2')) os.symlink(os.path.join('a', 'bcd'), cls.norm('sym3')) @pytest.mark.parametrize("case", cases) def test_glob_cases(self, case): """Test glob cases.""" self.eval_glob_cases(case) def test_negateall(self): """Negate applied to all files.""" for file in glob.glob('!**/', flags=glob.N | glob.NEGATEALL | glob.G, root_dir=self.tempdir): self.assert_equal(os.path.isdir(file), False) def test_negateall_bytes(self): """Negate applied to all files.""" for file in glob.glob(b'!**/', flags=glob.N | glob.NEGATEALL | glob.G, root_dir=os.fsencode(self.tempdir)): self.assert_equal(os.path.isdir(file), False) def test_magic_non_magic(self): """Test logic when switching from magic to non-magic patterns.""" with change_cwd(self.tempdir): self.assert_equal(sorted(glob.glob(['**/aab', 'dummy'], flags=glob.G)), ['aab',]) def test_non_magic_magic(self): """Test logic when switching from non-magic to magic patterns.""" with change_cwd(self.tempdir): self.assert_equal(sorted(glob.glob(['dummy', '**/aab'], flags=glob.G)), ['aab',]) class TestGlobStarLong(_TestGlob): """Test `***` cases.""" DEFAULT_FLAGS = glob.BRACE | glob.EXTGLOB | glob.GLOBSTARLONG | glob.MARK | glob.NOUNIQUE cases = [ # Test `globstar` with `globstarlong` enabled [ ('**', '*'), [ ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',) ] if not can_symlink() else [ ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',), ('sym1',), ('sym2',), ('sym3',) ] ], # Test `globstarlong` [ ('***', '*'), [ ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',) ] if not can_symlink() else [ ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',), ('sym1',), ('sym2',), ('sym3',), ('sym3', 'EF'), ('sym3', 'efg'), ('sym3', 'efg', 'ha') ] ], # Enable `FOLLOW`. Should be no changes. [ ('**', '*'), [ ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',) ] if not can_symlink() else [ ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',), ('sym1',), ('sym2',), ('sym3',) ], glob.L ], [ ('***', '*'), [ ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',) ] if not can_symlink() else [ ('aab',), ('aab', 'F'), ('a',), ('a', 'bcd'), ('a', 'bcd', 'EF'), ('a', 'bcd', 'efg'), ('a', 'bcd', 'efg', 'ha'), ('a', 'D'), ('aaa',), ('aaa', 'zzzF'), ('EF',), ('ZZZ',), ('sym1',), ('sym2',), ('sym3',), ('sym3', 'EF'), ('sym3', 'efg'), ('sym3', 'efg', 'ha') ], glob.L ] ] @classmethod def setup_fs(cls): """Setup file system.""" cls.mktemp('a', 'D') cls.mktemp('aab', 'F') cls.mktemp('.aa', 'G') cls.mktemp('.bb', 'H') cls.mktemp('aaa', 'zzzF') cls.mktemp('ZZZ') cls.mktemp('EF') cls.mktemp('a', 'bcd', 'EF') cls.mktemp('a', 'bcd', 'efg', 'ha') cls.can_symlink = can_symlink() if cls.can_symlink: os.symlink(cls.norm('broken'), cls.norm('sym1')) os.symlink('broken', cls.norm('sym2')) os.symlink(os.path.join('a', 'bcd'), cls.norm('sym3')) @pytest.mark.parametrize("case", cases) def test_glob_cases(self, case): """Test glob cases.""" self.eval_glob_cases(case) class TestGlobMarked(Testglob): """Test glob marked.""" DEFAULT_FLAGS = glob.BRACE | glob.EXTGLOB | glob.GLOBSTAR | glob.FOLLOW | glob.MARK class TestPathlibNorm(unittest.TestCase): """Test normalization cases.""" def test_norm(self): """Test normalization.""" self.assertEqual(glob.Glob('.')._pathlib_norm('/./test'), '/test') self.assertEqual(glob.Glob('.')._pathlib_norm('/.'), '/') self.assertEqual(glob.Glob('.')._pathlib_norm('test/.'), 'test') self.assertEqual(glob.Glob('.')._pathlib_norm('test/./'), 'test') self.assertEqual(glob.Glob('.')._pathlib_norm('./.'), '') self.assertEqual(glob.Glob('.')._pathlib_norm('.'), '') self.assertEqual(glob.Glob('.')._pathlib_norm('test/./.test/'), 'test/.test') self.assertEqual(glob.Glob('.')._pathlib_norm('/.test/'), '/.test') self.assertEqual(glob.Glob('.')._pathlib_norm('/../../././.'), '/../..') self.assertEqual(glob.Glob('.')._pathlib_norm('./././././'), '') self.assertEqual(glob.Glob('.')._pathlib_norm('././../../'), '../..') self.assertEqual(glob.Glob('.')._pathlib_norm('/././../../'), '/../..') self.assertEqual(glob.Glob('.')._pathlib_norm('/'), '/') self.assertEqual(glob.Glob('.')._pathlib_norm('/.'), '/') self.assertEqual(glob.Glob('.')._pathlib_norm('./'), '') self.assertEqual(glob.Glob('.')._pathlib_norm('./test'), 'test') @unittest.skipUnless(sys.platform.startswith('win'), "Windows specific test") def test_norm_windows(self): """Test normalization on Windows.""" self.assertEqual(glob.Glob('.')._pathlib_norm('\\.\\test'), '\\test') self.assertEqual(glob.Glob('.')._pathlib_norm('\\.'), '\\') self.assertEqual(glob.Glob('.')._pathlib_norm('test\\.'), 'test') self.assertEqual(glob.Glob('.')._pathlib_norm('test\\.\\'), 'test') self.assertEqual(glob.Glob('.')._pathlib_norm('.\\.'), '') self.assertEqual(glob.Glob('.')._pathlib_norm('.'), '') self.assertEqual(glob.Glob('.')._pathlib_norm('test\\.\\.test\\'), 'test\\.test') self.assertEqual(glob.Glob('.')._pathlib_norm('\\.test\\'), '\\.test') self.assertEqual(glob.Glob('.')._pathlib_norm('\\..\\..\\.\\.\\.'), '\\..\\..') self.assertEqual(glob.Glob('.')._pathlib_norm('.\\.\\.\\.\\.\\'), '') self.assertEqual(glob.Glob('.')._pathlib_norm('.\\.\\..\\..\\'), '..\\..') self.assertEqual(glob.Glob('.')._pathlib_norm('\\.\\.\\..\\..\\'), '\\..\\..') self.assertEqual(glob.Glob('.')._pathlib_norm('\\'), '\\') self.assertEqual(glob.Glob('.')._pathlib_norm('\\.'), '\\') self.assertEqual(glob.Glob('.')._pathlib_norm('.\\'), '') self.assertEqual(glob.Glob('.')._pathlib_norm('.\\test'), 'test') class TestHidden(_TestGlob): """Test hidden specific cases.""" cases = [ [('**', '.*'), [('a', '.'), ('a', '..'), ('.aa',), ('.bb',), ('.',), ('..',)], glob.SCANDOTDIR], [('*', '.*'), [('a', '.'), ('a', '..')], glob.SCANDOTDIR], [('.*',), [('.aa',), ('.bb',), ('.',), ('..',)], glob.SCANDOTDIR], [ ('**', '.*'), [ ('a', '.'), ('a', '..'), ('.aa',), ('.aa', '.'), ('.aa', '..'), ('.bb',), ('.bb', '.'), ('.bb', '..'), ('.',), ('..',) ], glob.D | glob.SCANDOTDIR ], [ ('**', '.*|**', '.', '.aa', '.'), [ ('a', '.'), ('a', '..'), ('.aa',), ('.aa', '.'), ('.aa', '..'), ('.bb',), ('.bb', '.'), ('.bb', '..'), ('.',), ('..',), ('.', '.aa', '.') ], glob.D | glob.S | glob.SCANDOTDIR ], # Test `pathlib` mode. `pathlib` normalizes out `.` directories, so when evaluating unique values, # normalize paths with `.`. # Prevent matching `.aa` and `.aa/.` (same with `.bb`) [('**', '.*'), [('.aa',), ('.bb',)]], [('**', '.*'), [('.aa',), ('.bb',)], glob.Z], [('**', '.*'), [('.aa',), ('.bb',)], glob.SCANDOTDIR | glob.Z], [ ('**', '.*'), [ ('.aa',), ('.bb',) ], glob.D ], # Prevent matching `.aa/.` and `./.aa/.` as they are all the same as `.aa` [ ('**', '.*|**', '.', '.aa', '.'), [ ('.aa',), ('.bb',), ('.', '.aa', '.') ], glob.D | glob.S ], # Unique logic is disabled, so we can match `.aa` from one pattern and `./.aa/.` from another pattern. # Duplicates are still restricted from a single pattern, so `.aa/.` is not found in the first pattern as # `.aa` was already found, but unique results across multi-patterns is not enforced. [ ('**', '.*|**', '.', '.aa', '.'), [ ('.aa',), ('.bb',), ('.', '.aa', '.') ], glob.D | glob.S | glob.Q ], # Enable `pathlib` mode to ensure unique across multiple `pathlib` patterns. [ ('**', '.*|**', '.', '.aa', '.'), [ ('.aa',), ('.bb',) ], glob.D | glob.S | glob._PATHLIB ], # `NOUNIQUE` disables `pathlib` mode unique filtering. [ ('**', '.*|**', '.', '.aa', '.'), [ ('.aa',), ('.bb',), ('.', '.aa', '.') ], glob.D | glob.S | glob.Q | glob._PATHLIB ], # `pathlib` should still filter out duplicates if `.` and trailing slashes are normalized and # a single patter is used. [ ('**', '.*'), [ ('.', ), ('..', ), ('.aa',), ('.bb',), ('.bb', '..'), ('a', '.'), ('a', '..'), ('.aa', '..') ], glob.D | glob.S | glob.SCANDOTDIR | glob._PATHLIB ], Options(default_negate='**/./.*/*'), # `SCANDOTDIR` does not change our patterns (the negate pattern for instance), # just what is returned when scanning a folder with a wildcard. [ ('**', '.*', '.aa', '*'), [ ('.', '.bb', 'H') ], glob.D | glob.S | glob.N ], # Should we allow this? Or should `NODOTDIR` not apply to `NEGATE` patterns. [ ('**', '.*', '.aa', '*'), [ ('.', '.bb', 'H'), ('.', '.aa', 'G') ], glob.D | glob.S | glob.N | glob.Z ], Options(default_negate='**') ] @classmethod def setup_fs(cls): """Setup file system.""" cls.mktemp('a', 'D') cls.mktemp('a', 'a') cls.mktemp('.aa', 'G') cls.mktemp('.bb', 'H') @pytest.mark.parametrize("case", cases) def test_glob_cases(self, case): """Test glob cases.""" self.eval_glob_cases(case) class TestCWD(_TestGlob): """Test files in the current working directory.""" @classmethod def setup_fs(cls): """Setup file system.""" cls.mktemp('a', 'D') cls.mktemp('aab', 'F') cls.mktemp('.aa', 'G') cls.mktemp('.bb', 'H') cls.mktemp('aaa', 'zzzF') cls.mktemp('ZZZ') cls.mktemp('EF') cls.mktemp('a', 'bcd', 'EF') cls.mktemp('a', 'bcd', 'efg', 'ha') cls.can_symlink = can_symlink() if cls.can_symlink: os.symlink(cls.norm('broken'), cls.norm('sym1')) os.symlink('broken', cls.norm('sym2')) os.symlink(os.path.join('a', 'bcd'), cls.norm('sym3')) def test_dots_cwd(self): """Test capture of dot files with recursive glob.""" with change_cwd(self.tempdir): self.assert_equal(sorted(glob.glob(['**/.*', '!**/.', '!**/..'], flags=glob.G | glob.N)), ['.aa', '.bb']) def test_cwd(self): """Test root level glob on current working directory.""" with change_cwd(self.tempdir): self.assert_equal(glob.glob('EF'), ['EF']) def test_cwd_root_dir(self): """Test root level glob when we switch directory via `root_dir`.""" self.assert_equal(glob.glob('EF', root_dir=self.tempdir), ['EF']) def test_cwd_root_dir_pathlike(self): """Test root level glob when we switch directory via `root_dir` with a path-like object.""" self.assert_equal(glob.glob('EF', root_dir=pathlib.Path(self.tempdir)), ['EF']) @pytest.mark.skipif(not glob.SUPPORT_DIR_FD, reason="dir_fd is not supported on this system") def test_cwd_fd_dir(self): """Test file descriptor.""" dir_fd = os.open(self.tempdir, os.O_RDONLY | os.O_DIRECTORY) self.assert_equal(glob.glob('EF', dir_fd=dir_fd), ['EF']) os.close(dir_fd) @pytest.mark.skipif(not glob.SUPPORT_DIR_FD, reason="dir_fd is not supported on this system") def test_cwd_dir_fd_globmatch(self): """Test file descriptor on `globmatch`.""" dir_fd = os.open(self.tempdir, os.O_RDONLY | os.O_DIRECTORY) self.assert_equal(glob.globmatch('EF', 'EF', dir_fd=dir_fd, flags=glob.REALPATH), True) os.close(dir_fd) @pytest.mark.skipif(glob.SUPPORT_DIR_FD, reason="dir_fd is supported on this system") def test_cwd_dir_fd_globmatch_unsupported(self): """Test file descriptor on unsupported system.""" # Windows doesn't support `dir_fd`, let's fabricate a scenario and verify it doesn't match. dir_fd = 1 self.assert_equal(glob.globmatch('EF', 'EF', dir_fd=dir_fd, flags=glob.REALPATH), False) @pytest.mark.skipif(not glob.SUPPORT_DIR_FD, reason="dir_fd is not supported on this system") def test_cwd_dir_fd_globmatch_no_follow(self): """Test file descriptor with `globmatch`, but cover link logic.""" dir_fd = os.open(self.tempdir, os.O_RDONLY | os.O_DIRECTORY) self.assert_equal( glob.globmatch('a/bcd/EF', 'a/**/EF', dir_fd=dir_fd, flags=glob.REALPATH | glob.GLOBSTAR), True ) os.close(dir_fd) @pytest.mark.skipif(glob.SUPPORT_DIR_FD, reason="dir_fd is supported on this system") def test_cwd_dir_fd_globmatch_no_follow_unsupported(self): """Test file descriptor with `globmatch` on unsupported systems, but cover link logic.""" # Windows doesn't support `dir_fd`, let's fabricate a scenario and verify it doesn't match. dir_fd = 1 self.assert_equal( glob.globmatch('a/bcd/EF', 'a/**/EF', dir_fd=dir_fd, flags=glob.REALPATH | glob.GLOBSTAR), False ) @pytest.mark.skipif(not glob.SUPPORT_DIR_FD, reason="dir_fd is not supported on this system") def test_cwd_dir_fd_root_dir(self): """Test file descriptor and root directory together.""" dir_fd = os.open(self.tempdir, os.O_RDONLY | os.O_DIRECTORY) root_dir = 'a' self.assert_equal(glob.glob('bcd/EF', dir_fd=dir_fd, root_dir=root_dir), [os.path.join('bcd', 'EF')]) os.close(dir_fd) @pytest.mark.skipif(not glob.SUPPORT_DIR_FD, reason="dir_fd is not supported on this system") def test_cwd_dir_fd_root_dir_globmatch_no_follow(self): """Test file descriptor and root directory on `globmatch`, but cover link logic.""" dir_fd = os.open(self.tempdir, os.O_RDONLY | os.O_DIRECTORY) root_dir = 'a' self.assert_equal( glob.globmatch('bcd/EF', '**/EF', dir_fd=dir_fd, root_dir=root_dir, flags=glob.REALPATH | glob.GLOBSTAR), True ) os.close(dir_fd) class TestCase(_TestGlob): """Test files in the current working directory.""" @classmethod def setup_fs(cls): """Setup file system.""" cls.mktemp('a') cls.mktemp('A') def test_case(self): """Test case.""" # Python actually just assumes Windows is case insensitive and everything else isn't. # By default, we assume what Python assumes, but let's check to be sure before we assert. if len(os.listdir(self.tempdir)) == 1: pytest.skip("Skipped") else: assert len(glob.glob('*', root_dir=self.tempdir)) == 2 class TestGlobCornerCase(_TestGlob): """ Some tests that need a very specific file set to test against for corner cases. See `_TestGlob` class for more information in regards to test case format. """ cases = [ # Test very specific, special cases. [('a[/]b',), [('a[', ']b',)]], [('@(a/b)',), []], [('@(a[/]b)',), []], [('test[',), [('test[',)]], [(R'a\/b',), [('a', 'b')]], [(R'a[\/]b',), [('a[', ']b')]], Options(skip=util.is_case_sensitive()), [('a[\\',), [('a[',)]], [('@(a[\\',), [('@(a[',)]], [(R'a[\\',), [('a[', '')]], [(R'@(a[\\',), [('@(a[', '')]], Options(skip=False) ] @classmethod def setup_fs(cls): """Setup file system.""" cls.mktemp('test[') cls.mktemp('a', 'b') cls.mktemp('a[', ']b') cls.mktemp('@(a', 'b)') cls.mktemp('@(a[', ']b)') cls.can_symlink = can_symlink() @pytest.mark.parametrize("case", cases) def test_glob_cases(self, case): """Test glob cases.""" self.eval_glob_cases(case) class TestGlobCornerCaseMarked(Testglob): """Test glob marked.""" DEFAULT_FLAGS = glob.BRACE | glob.EXTGLOB | glob.GLOBSTAR | glob.FOLLOW | glob.MARK class TestGlobEscapes(unittest.TestCase): """Test escaping.""" def check_escape(self, arg, expected, unix=None): """Verify escapes.""" flags = 0 if unix is False: flags = glob.FORCEWIN elif unix is True: flags = glob.FORCEUNIX self.assertEqual(glob.escape(arg, unix=unix), expected) self.assertEqual(glob.escape(os.fsencode(arg), unix=unix), os.fsencode(expected)) self.assertTrue( glob.globmatch( arg, glob.escape(arg, unix=unix), flags=flags ) ) def test_escape(self): """Test path escapes.""" check = self.check_escape check('abc', 'abc') check('[', R'\[') check('?', R'\?') check('*', R'\*') check('[[_/*?*/_]]', R'\[\[_/\*\?\*/_\]\]') check('/[[_/*?*/_]]/', R'/\[\[_/\*\?\*/_\]\]/') @unittest.skipUnless(sys.platform.startswith('win'), "Windows specific test") def test_escape_windows(self): """Test windows escapes.""" check = self.check_escape check('a:\\?', R'a:\\\?') check('b:\\*', R'b:\\\*') check('\\\\?\\c:\\?', R'\\\\?\\c:\\\?') check('\\\\*\\*\\*', R'\\\\*\\*\\\*') check('//?/c:/?', R'//?/c:/\?') check('//*/*/*', R'//*/*/\*') check('//[^what]/name/temp', R'//[^what]/name/temp') def test_escape_forced_windows(self): """Test forced windows escapes.""" check = self.check_escape check('a:\\?', R'a:\\\?', unix=False) check('b:\\*', R'b:\\\*', unix=False) check('\\\\?\\c:\\?', R'\\\\?\\c:\\\?', unix=False) check('\\\\*\\*\\*', R'\\\\*\\*\\\*', unix=False) check('//?/c:/?', R'//?/c:/\?', unix=False) check('//*/*/*', R'//*/*/\*', unix=False) check( '//./Volume{b75e2c83-0000-0000-0000-602f00000000}/temp', R'//./Volume\{b75e2c83-0000-0000-0000-602f00000000\}/temp', unix=False ) check('//[^what]/name/temp', R'//[^what]/name/temp', unix=False) def test_escape_forced_unix(self): """Test forced windows Unix.""" check = self.check_escape check('a:\\?', R'a:\\\?', unix=True) check('b:\\*', R'b:\\\*', unix=True) check('\\\\?\\c:\\?', R'\\\\\?\\c:\\\?', unix=True) check('\\\\*\\*\\*', R'\\\\\*\\\*\\\*', unix=True) check('//?/c:/?', R'//\?/c:/\?', unix=True) check('//*/*/*', R'//\*/\*/\*', unix=True) check('//[^what]/name/temp', R'//\[^what\]/name/temp', unix=True) @unittest.skipUnless(sys.platform.startswith('win'), "Windows specific test") class TestWindowsDriveCase(unittest.TestCase): """Test Windows drive case.""" RE_DRIVE = re.compile(r'((?:\\|/){2}[^\\/]+(?:\\|/){1}[^\\/]+|[a-z]:)((?:\\|/){1}|$)', re.I) def test_drive_insensitive(self): """Test drive case insensitivity.""" cwd = os.getcwd() filepath = os.path.join(cwd, 'README.md') self.assertEqual([filepath], glob.glob(filepath.replace('\\', '\\\\'))) self.assertEqual( [self.RE_DRIVE.sub(lambda m: m.group(0).upper(), filepath)], glob.glob(filepath.replace('\\', '\\\\').upper()) ) self.assertEqual( [self.RE_DRIVE.sub(lambda m: m.group(0).lower(), filepath)], glob.glob(filepath.replace('\\', '\\\\').lower()) ) def test_drive_sensitive(self): """Test drive case sensitivity (they'll be insensitive regardless of case flag).""" cwd = os.getcwd() filepath = os.path.join(cwd, 'README.md') self.assertEqual([filepath], glob.glob(filepath.replace('\\', '\\\\'), flags=glob.C)) self.assertEqual( [self.RE_DRIVE.sub(lambda m: m.group(0).upper(), filepath)], glob.glob(self.RE_DRIVE.sub(lambda m: m.group(0).upper(), filepath).replace('\\', '\\\\'), flags=glob.C) ) self.assertEqual( [self.RE_DRIVE.sub(lambda m: m.group(0).lower(), filepath)], glob.glob(self.RE_DRIVE.sub(lambda m: m.group(0).lower(), filepath).replace('\\', '\\\\'), flags=glob.C) ) @skip_unless_symlink class TestSymlinkLoopGlob(unittest.TestCase): """Symlink loop test case.""" DEFAULT_FLAGS = glob.BRACE | glob.EXTGLOB | glob.GLOBSTAR | glob.FOLLOW def globjoin(self, *parts): """Joins glob path.""" sep = os.fsencode(self.globsep) if isinstance(parts[0], bytes) else self.globsep return sep.join(list(parts)) def setUp(self): """Setup.""" if os.sep == '/': self.globsep = os.sep else: self.globsep = r'\\' def test_selflink(self): """Test self links.""" tempdir = TESTFN + "_dir" os.makedirs(tempdir) self.addCleanup(shutil.rmtree, tempdir) with change_cwd(tempdir): os.makedirs('dir') create_empty_file(os.path.join('dir', 'file')) os.symlink(os.curdir, os.path.join('dir', 'link')) results = glob.glob('**', flags=self.DEFAULT_FLAGS) self.assertEqual(len(results), len(set(results))) results = set(results) depth = 0 while results: path = os.path.join(*(['dir'] + ['link'] * depth)) self.assertIn(path, results) results.remove(path) if not results: break path = os.path.join(path, 'file') self.assertIn(path, results) results.remove(path) depth += 1 results = glob.glob(os.path.join('**', 'file'), flags=self.DEFAULT_FLAGS) self.assertEqual(len(results), len(set(results))) results = set(results) depth = 0 while results: path = os.path.join(*(['dir'] + ['link'] * depth + ['file'])) self.assertIn(path, results) results.remove(path) depth += 1 results = glob.glob(self.globjoin('**', ''), flags=self.DEFAULT_FLAGS) self.assertEqual(len(results), len(set(results))) results = set(results) depth = 0 while results: path = os.path.join(*(['dir'] + ['link'] * depth + [''])) self.assertIn(path, results) results.remove(path) depth += 1 class TestGlobPaths(unittest.TestCase): """Test `glob` paths.""" @unittest.skipUnless(not sys.platform.startswith('win'), "Linux/Unix specific test") def test_root_unix(self): """Test that `glob` translates the root properly.""" # On Linux/Unix, this should translate to the root. # Basically, we should not return an empty set. results = glob.glob('/*') self.assertTrue(len(results) > 0) self.assertTrue('/' not in results) @unittest.skipUnless(sys.platform.startswith('win'), "Windows specific test") def test_root_win(self): """Test that `glob` translates the root properly.""" # On Windows, this should translate to the current drive. # Basically, we should not return an empty set. results = glob.glob('/*') self.assertTrue(len(results) > 0) self.assertTrue('\\' not in results) results = glob.glob(R'\\*') self.assertTrue(len(results) > 0) self.assertTrue('\\' not in results) def test_start(self): """Test that starting directory/files are handled properly.""" self.assertEqual( sorted(['docs', 'wcmatch', 'readme.md']), sorted([each.lower() for each in glob.glob(['BAD', 'docs', 'WCMATCH', 'readme.MD'], flags=glob.I)]) ) class TestExcludes(unittest.TestCase): """Test expansion limits.""" def test_translate_exclude(self): """Test excludes.""" results = glob.glob('**/*.md', exclude='**/README.md', flags=glob.GLOBSTAR) self.assertTrue('README.md' not in results) @unittest.skipUnless(os.path.expanduser('~') != '~', "Requires expand user functionality") class TestTilde(unittest.TestCase): """Test tilde cases.""" def test_tilde(self): """Test tilde.""" files = os.listdir(os.path.expanduser('~')) self.assertEqual(len(glob.glob('~/*', flags=glob.T | glob.D)), len(files)) def test_tilde_bytes(self): """Test tilde in bytes.""" files = os.listdir(os.path.expanduser(b'~')) self.assertEqual(len(glob.glob(b'~/*', flags=glob.T | glob.D)), len(files)) def test_tilde_user(self): """Test tilde user cases.""" if sys.platform.startswith('win') and PY310: # In CI, and maybe on other systems, we cannot be sure we'll be able to get the user. # So fake it by using our own user name with the current `USERPROFILE` path. with EnvironmentVarGuard() as env: userpath = os.environ.get('USERPROFILE') env.clear() user = 'test_user' env['USERPROFILE'] = userpath env['USERNAME'] = 'test_user' files = os.listdir(userpath) self.assertEqual(len(glob.glob('~{}/*'.format(user), flags=glob.T | glob.D)), len(files)) else: # Accommodate non-Windows user behavior user = None if not sys.platform.startswith('win'): try: user = getpass.getuser() except ModuleNotFoundError: pass if user is None: # Last ditch effort to get a user. user = os.path.basename(os.path.expanduser('~')) userpath = os.path.expanduser('~{}'.format(user)) files = os.listdir(userpath) self.assertEqual(len(glob.glob('~{}/*'.format(user), flags=glob.T | glob.D)), len(files)) def test_tilde_disabled(self): """Test when tilde is disabled.""" self.assertEqual(len(glob.glob('~/*', flags=glob.D)), 0) class TestExpansionLimit(unittest.TestCase): """Test expansion limits.""" def test_limit_glob(self): """Test expansion limit of `glob`.""" with self.assertRaises(_wcparse.PatternLimitException): glob.glob('{1..11}', flags=glob.BRACE, limit=10) def test_limit_iglob(self): """Test expansion limit of `iglob`.""" with self.assertRaises(_wcparse.PatternLimitException): list(glob.iglob('{1..11}', flags=glob.BRACE, limit=10)) def test_limit_split(self): """Test split limit.""" with self.assertRaises(_wcparse.PatternLimitException): list(glob.iglob('|'.join(['a'] * 11), flags=glob.SPLIT, limit=10)) def test_limit_brace_glob(self): """Test limit with brace and split.""" with self.assertRaises(_wcparse.PatternLimitException): list( glob.iglob( '{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5)), flags=glob.SPLIT | _wcparse.BRACE, limit=10 ) ) def test_limit_wrap(self): """Test limit that wraps internally.""" with self.assertRaises(_wcparse.PatternLimitException): list( glob.iglob( ['|'.join(['a'] * 10), '|'.join(['a'] * 5)], flags=glob.SPLIT | _wcparse.BRACE, limit=10 ) ) class TestInputTypes(unittest.TestCase): """Test input types.""" def test_cwd_root_dir_pathlike_bytes_str(self): """Test root level glob when we switch directory via `root_dir` with a path-like object.""" with self.assertRaises(TypeError): glob.glob(b'docs/*', root_dir=pathlib.Path('.')) def test_cwd_root_dir_bytes_str(self): """Test root level glob when we switch directory via `root_dir` with a path-like object.""" with self.assertRaises(TypeError): glob.glob(b'docs/*', root_dir='.') def test_cwd_root_dir_str_bytes(self): """Test root level glob when we switch directory via `root_dir` with a path-like object.""" with self.assertRaises(TypeError): glob.glob('docs/*', root_dir=b'.') def test_cwd_root_dir_empty(self): """Test empty patterns with current working directory.""" self.assertEqual(glob.glob([], root_dir='.'), []) wcmatch-10.0/tests/test_globmatch.py000066400000000000000000002115401467532413500176100ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Tests for `globmatch`.""" import unittest import pytest import re import os import sys import wcmatch.glob as glob import wcmatch._wcparse as _wcparse import wcmatch.util as util import shutil # Below is general helper stuff that Python uses in `unittests`. As these # not meant for users, and could change without notice, include them # ourselves so we aren't surprised later. TESTFN = '@test' # Disambiguate `TESTFN` for parallel testing, while letting it remain a valid # module name. TESTFN = "{}_{}_tmp".format(TESTFN, os.getpid()) def create_empty_file(filename): """Create an empty file. If the file already exists, truncate it.""" fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) os.close(fd) _can_symlink = None def can_symlink(): """Check if we can symlink.""" global _can_symlink if _can_symlink is not None: return _can_symlink symlink_path = TESTFN + "can_symlink" try: os.symlink(TESTFN, symlink_path) can = True except (OSError, NotImplementedError, AttributeError): can = False else: os.remove(symlink_path) _can_symlink = can return can def skip_unless_symlink(test): """Skip decorator for tests that require functional symlink.""" ok = can_symlink() msg = "Requires functional symlink implementation" return test if ok else unittest.skip(msg)(test) class _TestGlobmatch(unittest.TestCase): """Test the `WcMatch` class.""" def mktemp(self, *parts): """Make temp directory.""" filename = self.norm(*parts) base, file = os.path.split(filename) if not os.path.exists(base): retry = 3 while retry: try: os.makedirs(base) retry = 0 except Exception: # noqa: PERF203 retry -= 1 create_empty_file(filename) def force_err(self): """Force an error.""" raise TypeError def norm(self, *parts): """Normalizes file path (in relation to temp directory).""" tempdir = os.fsencode(self.tempdir) if isinstance(parts[0], bytes) else self.tempdir return os.path.join(tempdir, *parts) def norm_list(self, files): """Normalize file list.""" return sorted([self.norm(os.path.normpath(x)) for x in files]) def setUp(self): """Setup.""" self.tempdir = TESTFN + "_dir" self.default_flags = glob.G | glob.P def tearDown(self): """Cleanup.""" retry = 3 while retry: try: shutil.rmtree(self.tempdir) while os.path.exists(self.tempdir): pass retry = 0 except Exception: # noqa: PERF203 retry -= 1 class GlobFiles(): """List of glob files.""" def __init__(self, filelist, append=False): """File list object.""" self.filelist = filelist self.append = append class Options(): """Test options.""" def __init__(self, **kwargs): """Initialize.""" self._options = kwargs def get(self, key, default=None): """Get option value.""" return self._options.get(key, default) class TestGlobFilter: """ Test matches against `globfilter`. Each list entry in `cases` is run through the `globsplit` and then `globfilter`. Entries are run through `globsplit` ensure it does not add any unintended side effects. There are a couple special types that can be inserted in the case list that can alter the behavior of the cases that follow. * `Strings`: These will be printed and then the next case will be processed. * `Options`: This object takes keyword parameters that are used to alter the next tests options: * skip_split: If set to `True`, this will cause the next tests to be skipped when we are processing cases with `globsplit`. * `GlobFiles`: This object takes a list of file paths and will set them as the current file list to compare against. If `append` is set to `True`, it will extend the test's file list instead of replacing. Each test case entry (list) is an array of up to 4 parameters (2 minimum). * Pattern * Expected result (filenames matched by the pattern) * Flags * List of files that will temporarily override the current main file list just for this specific case. The default flags are: `NEGATE` | `GLOBSTAR` | `EXTGLOB` | `BRACE`. If any of these flags are provided in a test case, they will disable the default of the same name. All other flags will enable flags as expected. """ cases = [ Options(skip_split=False), GlobFiles( [ 'a', 'b', 'c', 'd', 'abc', 'abd', 'abe', 'bb', 'bcd', 'ca', 'cb', 'dd', 'de', 'bdir/', 'bdir/cfile' ] ), # http://www.bashcookbook.com/bashinfo/source/bash-1.14.7/tests/glob-test ['a*', ['a', 'abc', 'abd', 'abe']], ['X*', []], # Slightly different than `bash/sh/ksh` # \\* is not un-escaped to literal "*" in a failed match, # but it does make it get treated as a literal star ['\\*', []], ['\\**', []], ['\\*\\*', []], ['b*/', ['bdir/']], ['c*', ['c', 'ca', 'cb']], [ '**', [ 'a', 'b', 'c', 'd', 'abc', 'abd', 'abe', 'bb', 'bcd', 'ca', 'cb', 'dd', 'de', 'bdir/', 'bdir/cfile' ] ], [R'\\.\\./*/', []], [R's/\\..*//', []], # legendary Larry crashes bashes ['/^root:/{s/^[^:]*:[^:]*:\\([^:]*\\).*$/\\1/', []], ['/^root:/{s/^[^:]*:[^:]*:\\([^:]*\\).*$/\u0001/', []], # character classes ['[a-c]b*', ['abc', 'abd', 'abe', 'bb', 'cb']], ['[a-y]*[^c]', ['abd', 'abe', 'bb', 'bcd', 'bdir/', 'ca', 'cb', 'dd', 'de']], ['a*[^c]', ['abd', 'abe']], GlobFiles(['a-b', 'aXb'], append=True), ['a[X-]b', ['a-b', 'aXb']], GlobFiles(['.x', '.y'], append=True), ['[^a-c]*', ['d', 'dd', 'de']], GlobFiles(['a*b/', 'a*b/ooo'], append=True), ['a\\*b/*', ['a*b/ooo']], ['a\\*?/*', ['a*b/ooo']], ['*\\\\!*', [], 0, ['echo !7']], ['*\\!*', ['echo !7'], 0, ['echo !7']], ['*.\\*', ['r.*'], 0, ['r.*']], ['a[b]c', ['abc']], ['a[\\b]c', ['abc']], ['a?c', ['abc']], ['a\\*c', [], 0, ['abc']], ['', [''], 0, ['']], # http://www.opensource.apple.com/source/bash/bash-23/bash/tests/glob-test GlobFiles(['man/', 'man/man1/', 'man/man1/bash.1'], append=True), ['*/man*/bash.*', ['man/man1/bash.1']], ['man/man1/bash.1', ['man/man1/bash.1']], ['a***c', ['abc'], 0, ['abc']], ['a*****?c', ['abc'], 0, ['abc']], ['?*****??', ['abc'], 0, ['abc']], ['*****??', ['abc'], 0, ['abc']], ['?*****?c', ['abc'], 0, ['abc']], ['?***?****c', ['abc'], 0, ['abc']], ['?***?****?', ['abc'], 0, ['abc']], ['?***?****', ['abc'], 0, ['abc']], ['*******c', ['abc'], 0, ['abc']], ['*******?', ['abc'], 0, ['abc']], ['a*cd**?**??k', ['abcdecdhjk'], 0, ['abcdecdhjk']], ['a**?**cd**?**??k', ['abcdecdhjk'], 0, ['abcdecdhjk']], ['a**?**cd**?**??k***', ['abcdecdhjk'], 0, ['abcdecdhjk']], ['a**?**cd**?**??***k', ['abcdecdhjk'], 0, ['abcdecdhjk']], ['a**?**cd**?**??***k**', ['abcdecdhjk'], 0, ['abcdecdhjk']], ['a****c**?**??*****', ['abcdecdhjk'], 0, ['abcdecdhjk']], ['[-abc]', ['-'], 0, ['-']], ['[abc-]', ['-'], 0, ['-']], ['\\', [], 0, ['\\']], ['\\\\', ['\\'], 0, ['\\']], ['/', ['\\'], glob.W, ['\\']], ['/', ['/'], glob.U, ['/']], ['[\\\\]', (['\\'] if util.is_case_sensitive() else []), 0, ['\\']], ['[\\\\]', ['\\'], glob.U, ['\\']], ['[\\\\]', [], glob.W, ['\\']], ['[[]', ['['], 0, ['[']], ['[', ['['], 0, ['[']], ['[*', ['[abc'], 0, ['[abc']], # a right bracket shall lose its special meaning and\ # represent itself in a bracket expression if it occurs\ # first in the list. -- POSIX.2 2.8.3.2 ['[]]', [']'], 0, [']']], ['[]-]', [']'], 0, [']']], ['[a-\\z]', ['p'], 0, ['p']], ['??**********?****?', [], 0, ['abc']], ['??**********?****c', [], 0, ['abc']], ['?************c****?****', [], 0, ['abc']], ['*c*?**', [], 0, ['abc']], ['a*****c*?**', [], 0, ['abc']], ['a********???*******', [], 0, ['abc']], ['[]', [], 0, ['a']], ['[abc', [], 0, ['[']], # No case tests ['XYZ', ['xYz'], glob.I, ['xYz', 'ABC', 'IjK']], [ 'ab*', ['ABC'], glob.I, ['xYz', 'ABC', 'IjK'] ], [ '[ia]?[ck]', ['ABC', 'IjK'], glob.I, ['xYz', 'ABC', 'IjK'] ], # [ pattern, [matches], MM opts, files, TAP opts] # one star/two star ['{/*,*}', [], 0, ['/asdf/asdf/asdf']], ['{/?,*}', ['/a', 'bb'], 0, ['/a', '/b/b', '/a/b/c', 'bb']], # dots should not match unless requested ['**', ['a/b'], 0, ['a/b', 'a/.d', '.a/.d']], # .. and . can only match patterns starting with ., # even when options.dot is set. GlobFiles(['a/./b', 'a/../b', 'a/c/b', 'a/.d/b']), ['a/*/b', ['a/c/b', 'a/.d/b'], glob.D], ['a/.*/b', ['a/./b', 'a/../b', 'a/.d/b'], glob.D], ['a/*/b', ['a/c/b'], 0], ['a/.*/b', ['a/./b', 'a/../b', 'a/.d/b'], 0], ['a/.*/b', ['a/.d/b'], glob.Z], # Escaped `.` will still be treated as a `.` [R'a/\.*/b', ['a/./b', 'a/../b', 'a/.d/b'], 0], [R'a/\.*/b', ['a/.d/b'], glob.Z], # this also tests that changing the options needs # to change the cache key, even if the pattern is # the same! [ '**', ['a/b', 'a/.d', '.a/.d'], glob.D, ['.a/.d', 'a/.d', 'a/b'] ], # paren sets cannot contain slashes ['*(a/b)', [], 0, ['a/b']], # brace sets trump all else. # # invalid glob pattern. fails on bash4 and `bsdglob`. # however, in this implementation, it's easier just # to do the intuitive thing, and let brace-expansion # actually come before parsing any `extglob` patterns, # like the documentation seems to say. # # XXX: if anyone complains about this, either fix it # or tell them to grow up and stop complaining. # # `bash/bsdglob` says this: # , ["*(a|{b),c)}", ["*(a|{b),c)}"], {}, ["a", "ab", "ac", "ad"]] # but we do this instead: ['*(a|{b),c)}', ['a', 'ab', 'ac'], 0, ['a', 'ab', 'ac', 'ad']], # test partial parsing in the presence of comment/negation chars # NOTE: We don't support these so they should work fine. ['[!a*', ['[!ab'], 0, ['[!ab', '[ab']], ['[#a*', ['[#ab'], 0, ['[#ab', '[ab']], # The following tests have `|` not included in things like +(...) etc. # We run these tests through normally and through `glob.globsplit` which splits # patterns on unenclosed `|`, so disable these few tests during split tests. Options(skip_split=True), # like: {a,b|c\\,d\\\|e} except it's unclosed, so it has to be escaped. # NOTE: I don't know what the original test was doing because it was matching # something crazy. `Multimatch` regex expanded to escapes to like a 50. # I think ours expands them proper, so the original test has been altered. [ '+(a|*\\|c\\\\|d\\\\\\|e\\\\\\\\|f\\\\\\\\\\|g', (['+(a|b\\|c\\|d\\|e\\\\|f\\\\|g'] if util.is_case_sensitive() else []), 0, ['+(a|b\\|c\\|d\\|e\\\\|f\\\\|g', 'a', 'b\\c'] ], [ '+(a|*\\|c\\\\|d\\\\\\|e\\\\\\\\|f\\\\\\\\\\|g', ['+(a|b\\|c\\|d\\|e\\\\|f\\\\|g'], glob.U, ['+(a|b\\|c\\|d\\|e\\\\|f\\\\|g', 'a', 'b\\c'] ], [ '+(a|*\\|c\\\\|d\\\\\\|e\\\\\\\\|f\\\\\\\\\\|g', [], glob.W, ['+(a|b\\|c\\|d\\|e\\\\|f\\\\|g', 'a', 'b\\c'] ], # crazy nested {,,} and *(||) tests. GlobFiles( [ 'a', 'b', 'c', 'd', 'ab', 'ac', 'ad', 'bc', 'cb', 'bc,d', 'c,db', 'c,d', 'd)', '(b|c', '*(b|c', 'b|c', 'b|cc', 'cb|c', 'x(a|b|c)', 'x(a|c)', '(a|b|c)', '(a|c)' ] ), ['*(a|{b,c})', ['a', 'b', 'c', 'ab', 'ac']], ['{a,*(b|c,d)}', ['a', '(b|c', '*(b|c', 'd)']], # a # *(b|c) # *(b|d) ['{a,*(b|{c,d})}', ['a', 'b', 'bc', 'cb', 'c', 'd']], ['*(a|{b|c,c})', ['a', 'b', 'c', 'ab', 'ac', 'bc', 'cb']], # test various flag settings. [ '*(a|{b|c,c})', ['x(a|b|c)', 'x(a|c)', '(a|b|c)', '(a|c)'], glob.E ], Options(skip_split=False), # test `extglob` nested in `extglob` [ '@(a@(c|d)|c@(b|,d))', ['ac', 'ad', 'cb', 'c,d'] ], # Negation and extended glob together # `!` will be treated as an exclude pattern if it isn't followed by `(`. # `(` must be escaped to exclude a name that starts with `(`. # If `!(` doesn't start a valid extended glob pattern, # it will be treated as a literal, not an exclude pattern. Options(skip_split=True), [ R'!\(a|c)', [ '(a|b|c)', '(b|c', '*(b|c', 'a', 'ab', 'ac', 'ad', 'b', 'bc', 'bc,d', 'b|c', 'b|cc', 'c', 'c,d', 'c,db', 'cb', 'cb|c', 'd', 'd)', 'x(a|b|c)', 'x(a|c)' ], glob.A ], [ '!(a|c)', [ '(a|b|c)', '(a|c)', '(b|c', '*(b|c', 'ab', 'ac', 'ad', 'b', 'bc', 'bc,d', 'b|c', 'b|cc', 'c,d', 'c,db', 'cb', 'cb|c', 'd', 'd)', 'x(a|b|c)', 'x(a|c)' ], glob.A ], ['!!(a|c)', ['a', 'c'], glob.A], ['!(a|c*', [], glob.A], Options(skip_split=False), # Test `MATCHBASE`. [ 'a?b', ['x/y/acb', 'acb/'], glob.X, ['x/y/acb', 'acb/', 'acb/d/e', 'x/y/acb/d'] ], # Test that `MATCHBASE` isn't enabled after `GLOBSTAR` patterns with slashes # If `MATCHBASE` was still enabled, the `.y` folder would be gobbled up. [ '**/acb', ['acb/'], glob.X, ['x/.y/acb', 'acb/', 'acb/d/e', 'x/.y/acb/d'] ], [ '**/acb', ['x/.y/acb', 'acb/'], glob.X | glob.D, ['x/.y/acb', 'acb/', 'acb/d/e', 'x/.y/acb/d'] ], ['#*', ['#a', '#b'], 0, ['#a', '#b', 'c#d']], # begin channelling Boole and deMorgan... # NOTE: We changed these to `-` since our negation doesn't use `!`. # negation tests GlobFiles(['d', 'e', '!ab', '!abc', 'a!b', '\\!a']), # anything that is NOT a* matches. ['**|!a*', ['\\!a', 'd', 'e', '!ab', '!abc'], glob.S], # anything that IS !a* matches. ['!a*', ['!ab', '!abc'], glob.N], # NOTE: We don't allow negating negation. # # anything that IS a* matches # ['!!a*', ['a!b']], # anything that is NOT !a* matches ['**|!\\!a*', ['a!b', 'd', 'e', '\\!a'], glob.S], # negation nestled within a pattern GlobFiles( [ 'foo.js', 'foo.bar', 'foo.js.js', 'blar.js', 'foo.', 'boo.js.boo' ] ), # last one is tricky! * matches foo, . matches ., and `'js.js' != 'js'` # copy bash 4.3 behavior on this. ['*.!(js)', ['foo.bar', 'foo.', 'boo.js.boo', 'foo.js.js']], # https://github.com/isaacs/minimatch/issues/5 GlobFiles( [ 'a/b/.x/c', 'a/b/.x/c/d', 'a/b/.x/c/d/e', 'a/b/.x', 'a/b/.x/', 'a/.x/b', '.x', '.x/', '.x/a', '.x/a/b', 'a/.x/b/.x/c', '.x/.x', 'test.x/a' ] ), [ '**/.x/**', [ '.x/', '.x/a', '.x/a/b', 'a/.x/b', 'a/b/.x/', 'a/b/.x/c', 'a/b/.x/c/d', 'a/b/.x/c/d/e' ] ], [ '**.x/*', [ 'test.x/a', '.x/a' ] ], [ R'**\.x/*', [ 'test.x/a', '.x/a' ] ], # https://github.com/isaacs/minimatch/issues/59 ['[z-a]', []], ['a/[2015-03-10T00:23:08.647Z]/z', []], ['[a-0][a-\u0100]', []], # Consecutive slashes. GlobFiles( [ 'a/b/c', 'd/e/f', 'a/e/c' ] ), ['*//e///*', ['d/e/f', 'a/e/c']], [R'*//\e///*', ['d/e/f', 'a/e/c']], # Backslash trailing cases GlobFiles( [ 'a/b/c/', 'd/e/f/', 'a/e/c/' ] ), ['**\\', ['a/b/c/', 'd/e/f/', 'a/e/c/']], ['**\\', ['a/b/c/', 'd/e/f/', 'a/e/c/'], glob.U], ['**\\', ['a/b/c/', 'd/e/f/', 'a/e/c/'], glob.W], [R'**\\', [] if util.is_case_sensitive() else ['a/b/c/', 'd/e/f/', 'a/e/c/']], [R'**\\', [], glob.U], [R'**\\', ['a/b/c/', 'd/e/f/', 'a/e/c/'], glob.W], # Invalid `extglob` groups GlobFiles( [ '@([test', '@([test\\', '@(test\\', 'test[' ] ), ['@([test', ['@([test'] if util.is_case_sensitive() else ['@([test', '@([test\\']], ['@([test', ['@([test'], glob.U], ['@([test', ['@([test', '@([test\\'], glob.W], ['@([test\\', ['@([test'] if util.is_case_sensitive() else ['@([test', '@([test\\']], ['@(test\\', [] if util.is_case_sensitive() else ['@(test\\']], ['@(test[)', ['test[']], # Dot tests GlobFiles( [ '.', '..', '.abc', 'abc', '...', '..abc' ] ), # Basic dot tests ['[.]abc', []], [R'[\.]abc', []], ['.abc', ['.abc']], [R'\.abc', ['.abc']], ['[.]abc', ['.abc'], glob.D], [R'[\.]abc', ['.abc'], glob.D], ['.', ['.']], ['..', ['..']], ['.*', ['.', '..', '.abc', '...', '..abc']], [R'.\a*', ['.abc']], [R'\.', ['.']], [R'\..', ['..']], [R'\.\.', ['..']], ['..*', ['..', '...', '..abc']], [R'...', ['...']], [R'..\.', ['...']], ['.', ['.'], glob.Z], ['..', ['..'], glob.Z], ['.*', ['.abc', '...', '..abc'], glob.Z], [R'.\a*', ['.abc'], glob.Z], [R'\.', ['.'], glob.Z], [R'\..', ['..'], glob.Z], [R'\.\.', ['..'], glob.Z], ['..*', ['...', '..abc'], glob.Z], [R'...', ['...'], glob.Z], [R'..\.', ['...'], glob.Z], # Dot tests trailing slashes GlobFiles( [ './', '../', '.abc/', 'abc/', '.../', '..abc/' ] ), ['./', ['./']], ['../', ['../']], ['..\\', ['../']], ['./', ['./'], glob.Z], ['../', ['../'], glob.Z], ['..\\', ['../'], glob.Z], [R'.\\', ['./'], glob.W | glob.Z], # Inverse dot tests GlobFiles( [ '.', '..', '.abc', 'abc' ] ), # We enable glob.N by default, so staring with `!` # is a problem without glob.M ['!(test)', ['abc'], glob.M], ['!(test)', ['.abc', 'abc'], glob.D | glob.M], ['.!(test)', ['.', '..', '.abc'], glob.M], ['.!(test)', ['.', '..', '.abc'], glob.D | glob.M], ['!(.)', ['abc'], glob.M], [R'!(\.)', ['abc'], glob.M], [R'!(\x2e)', ['abc'], glob.M | glob.R], ['@(!(.))', ['abc'], glob.M], ['!(@(.))', ['abc'], glob.M], ['+(!(.))', ['abc'], glob.M], ['!(+(.))', ['abc'], glob.M], ['!(?)', ['abc'], glob.M], ['!(*)', [], glob.M], ['!([.])', ['abc'], glob.M], ['!(.)', ['..', '.abc', 'abc'], glob.M | glob.D], [R'!(\.)', ['..', '.abc', 'abc'], glob.M | glob.D], [R'!(\x2e)', ['..', '.abc', 'abc'], glob.M | glob.R | glob.D], ['@(!(.))', ['..', '.abc', 'abc'], glob.M | glob.D], ['!(@(.))', ['..', '.abc', 'abc'], glob.M | glob.D], ['+(!(.))', ['..', '.abc', 'abc'], glob.M | glob.D], ['+(!(.))', ['.abc', 'abc'], glob.M | glob.D | glob.Z], ['!(+(.))', ['.abc', 'abc'], glob.M | glob.D], ['!(?)', ['.abc', 'abc'], glob.M | glob.D], ['!(*)', [], glob.M | glob.D], ['!([.])', ['.abc', 'abc'], glob.M | glob.D], ['@(..|.)', ['.', '..']], ['@(..|.)', ['.', '..'], glob.Z], # More extended pattern dot related tests ['*(.)', ['.', '..']], [R'*(\.)', ['.', '..']], ['*([.])', []], ['*(?)', ['abc']], ['@(.?)', ['..']], ['@(?.)', []], ['*(.)', ['.', '..'], glob.D], [R'*(\.)', ['.', '..'], glob.D], ['*([.])', [], glob.D], ['*(?)', ['.abc', 'abc'], glob.D], ['@(.?)', ['..'], glob.D], ['@(?.)', [], glob.D], GlobFiles(['folder/abc', 'directory/abc', 'dir/abc']), # Test that inverse works properly mid path. ['!(folder)/*', ['directory/abc', 'dir/abc'], glob.M], ['!(folder)dir/abc', ['dir/abc'], glob.M], ['!(dir)/abc', ['directory/abc', 'folder/abc'], glob.M], # Slash exclusion GlobFiles(['test/test', 'test\\/test']), # Force Unix/Linux ['test/test', ['test/test'], glob.U], ['test\\/test', ['test/test'], glob.U], [R'test\\/test', ['test\\/test'], glob.U], ['@(test/test)', [], glob.U], [R'@(test\/test)', [], glob.U], ['test[/]test', [], glob.U], [R'test[\/]test', [], glob.U], # Force Windows ['test/test', ['test/test', 'test\\/test'], glob.W], ['test\\/test', ['test/test', 'test\\/test'], glob.W], ['@(test/test)', [], glob.W], [R'@(test\/test)', [], glob.W], ['test[/]test', [], glob.W], [R'test[\/]test', [], glob.W], # Case ['TEST/test', ['test/test', 'test\\/test'], glob.W], ['test\\/TEST', ['test/test', 'test\\/test'], glob.W], ['TEST/test', [], glob.W | glob.C], ['test\\/TEST', [], glob.W | glob.C], ['test/test', ['test/test', 'test\\/test'], glob.W | glob.C], ['test\\/test', ['test/test', 'test\\/test'], glob.W | glob.C], ['TEST/test', ['test/test'], glob.U | glob.I], ['test\\/TEST', ['test/test'], glob.U | glob.I], [R'test\\/TEST', ['test\\/test'], glob.U | glob.I], ['TEST/test', [], glob.U], ['test\\/TEST', [], glob.U], # Ensure we don't count slashes with `*`. GlobFiles(['test/test', 'test//']), ['test/*', ['test/test']], ['test/*', ['test/test'], glob.W], GlobFiles(['test\\test', 'test\\\\']), ['test/*', ['test\\test'], glob.W], GlobFiles(['c:/some/path', '//host/share/some/path']), # Test Windows drive and UNC host/share case sensitivity ['C:/**', ['c:/some/path'], glob.W], ['//HoSt/ShArE/**', ['//host/share/some/path'], glob.W], ['C:/SoMe/PaTh', ['c:/some/path'], glob.W], ['//HoSt/ShArE/SoMe/PaTh', ['//host/share/some/path'], glob.W], ['C:/**', ['c:/some/path'], glob.W | glob.C], ['//HoSt/ShArE/**', ['//host/share/some/path'], glob.W | glob.C], ['C:/SoMe/PaTh', [], glob.W | glob.C], ['//HoSt/ShArE/SoMe/PaTh', [], glob.W | glob.C], # Issue #24 GlobFiles( ["goo.cfg", "foo.bar", "foo.bar.cfg", "foo.cfg.bar"] ), ['*.bar', ["foo.bar", "foo.cfg.bar"]], ['*|!*.bar', ["goo.cfg", "foo.bar.cfg"], glob.S], # Test `NODIR` option GlobFiles( [ "test/..", "test/.", "test/...", "test/.file", "test/.file/", ".", "..", "...", '.../', "test/", "file", "/file" ] ), ['**/*', ['...', '.../', '/file', 'file', 'test/', 'test/...', 'test/.file', 'test/.file/'], glob.D], ['**/*', ['...', 'file', 'test/...', 'test/.file', "/file"], glob.O | glob.D], ['**/..', [], glob.O | glob.D], ['**/..', ['..', 'test/..'], glob.D], GlobFiles( [ b"test/..", b"test/.", b"test/...", b"test/.file", b"test/.file/", b".", b"..", b"...", b'.../', b"test/", b"file", b"/file" ] ), [b'**/*', [b'...', b'file', b'test/...', b'test/.file', b"/file"], glob.O | glob.D], # Test Windows drives GlobFiles( [ '//?/UNC/LOCALHOST/c$/temp', '//./UNC/LOCALHOST/c$/temp', '//?/GLOBAL/UNC/LOCALHOST/c$/temp', '//?/GLOBAL/global/UNC/LOCALHOST/c$/temp', '//?/C:/temp' ] ), ['//?/unc/localhost/c$/*', ['//?/UNC/LOCALHOST/c$/temp'], glob.W], ['//./unc/localhost/c$/*', ['//./UNC/LOCALHOST/c$/temp'], glob.W], ['//?/global/unc/localhost/c$/*', ['//?/GLOBAL/UNC/LOCALHOST/c$/temp'], glob.W], ['//?/global/global/unc/localhost/c$/*', ['//?/GLOBAL/global/UNC/LOCALHOST/c$/temp'], glob.W], ['//?/c:/*', ['//?/C:/temp'], glob.W], GlobFiles( [ b'//?/UNC/LOCALHOST/c$/temp', b'//./UNC/LOCALHOST/c$/temp', b'//?/GLOBAL/UNC/LOCALHOST/c$/temp', b'//?/GLOBAL/global/UNC/LOCALHOST/c$/temp', b'//?/C:/temp' ] ), [b'//?/unc/localhost/c$/*', [b'//?/UNC/LOCALHOST/c$/temp'], glob.W], [b'//./unc/localhost/c$/*', [b'//./UNC/LOCALHOST/c$/temp'], glob.W], [b'//?/global/unc/localhost/c$/*', [b'//?/GLOBAL/UNC/LOCALHOST/c$/temp'], glob.W], [b'//?/global/global/unc/localhost/c$/*', [b'//?/GLOBAL/global/UNC/LOCALHOST/c$/temp'], glob.W], [b'//?/c:/*', [b'//?/C:/temp'], glob.W] ] @classmethod def setup_class(cls): """Setup the tests.""" cls.files = [] # The tests we scraped were written with this assumed. cls.flags = glob.NEGATE | glob.GLOBSTAR | glob.EXTGLOB | glob.BRACE cls.skip_split = False @staticmethod def norm_files(files, flags): """Normalize files.""" flags = glob._flag_transform(flags) unix = _wcparse.is_unix_style(flags) return [(_wcparse.norm_slash(x, flags) if not unix else x) for x in files] @staticmethod def assert_equal(a, b): """Assert equal.""" assert a == b, "Comparison between objects yielded False." @classmethod def _filter(cls, case, split=False): """Filter with glob pattern.""" if isinstance(case, GlobFiles): if case.append: cls.files.extend(case.filelist) else: cls.files.clear() cls.files.extend(case.filelist) pytest.skip("Update file list") elif isinstance(case, Options): cls.skip_split = case.get('skip_split', False) pytest.skip("Change Options") files = cls.files if len(case) < 4 else case[3] flags = 0 if len(case) < 3 else case[2] print('Flags?') print(case) print(flags, cls.flags) flags = cls.flags ^ flags pat = case[0] if isinstance(case[0], list) else [case[0]] if split and cls.skip_split: return if split: flags |= glob.SPLIT print("PATTERN: ", case[0]) print("FILES: ", files) print("FLAGS: ", bin(flags)) result = sorted( glob.globfilter( files, pat, flags=flags ) ) source = sorted(case[1]) print("TEST: ", result, '<==>', source, '\n') cls.assert_equal(result, source) @pytest.mark.parametrize("case", cases) def test_glob_filter(self, case): """Test wildcard parsing.""" _wcparse._compile.cache_clear() self._filter(case) @pytest.mark.parametrize("case", cases) def test_glob_split_filter(self, case): """Test wildcard parsing by first splitting on `|`.""" _wcparse._compile.cache_clear() self._filter(case, split=True) class TestGlobMatch: """ Tests that are performed against `globmatch`. Each case entry is a list of 4 parameters. * Pattern * File name * Expected result (boolean of whether pattern matched file name) * Flags The default flags are `NEGATE` | `GLOBSTAR` | `EXTGLOB` | `BRACE`. Any flags passed through via entry are XORed. So if any of the default flags are passed via an entry, they will be disabled. All other flags will enable the feature. """ cases = [ ['*.!(js|css)', 'bar.min.js', True, glob.N], ['!*.+(js|css)', 'bar.min.js', False, glob.N], ['*.+(js|css)', 'bar.min.js', True, glob.N], ['*.!(j)', 'a-integration-test.js', True, glob.N], ['!(*-integration-test.js)', 'a-integration-test.js', False, glob.N], ['*-!(integration-)test.js', 'a-integration-test.js', True, glob.N], ['*-!(integration)-test.js', 'a-integration-test.js', False, glob.N], ['*!(-integration)-test.js', 'a-integration-test.js', True, glob.N], ['*!(-integration-)test.js', 'a-integration-test.js', True, glob.N], ['*!(integration)-test.js', 'a-integration-test.js', True, glob.N], ['*!(integration-test).js', 'a-integration-test.js', True, glob.N], ['*-!(integration-test).js', 'a-integration-test.js', True, glob.N], ['*-!(integration-test.js)', 'a-integration-test.js', True, glob.N], ['*-!(integra)tion-test.js', 'a-integration-test.js', False, glob.N], ['*-integr!(ation)-test.js', 'a-integration-test.js', False, glob.N], ['*-integr!(ation-t)est.js', 'a-integration-test.js', False, glob.N], ['*-i!(ntegration-)test.js', 'a-integration-test.js', False, glob.N], ['*i!(ntegration-)test.js', 'a-integration-test.js', True, glob.N], ['*te!(gration-te)st.js', 'a-integration-test.js', True, glob.N], ['*-!(integration)?test.js', 'a-integration-test.js', False, glob.N], ['*?!(integration)?test.js', 'a-integration-test.js', True, glob.N], ['foo-integration-test.js', 'foo-integration-test.js', True, glob.N], ['!(*-integration-test.js)', 'foo-integration-test.js', False, glob.N], ['*.!(js).js', 'foo.jszzz.js', True, glob.N], ['*.!(js)', 'asd.jss', True, glob.N], ['*.!(js).!(xy)', 'asd.jss.xyz', True, glob.N], ['*.!(js).!(xy)', 'asd.jss.xy', False, glob.N], ['*.!(js).!(xy)', 'asd.js.xyz', False, glob.N], ['*.!(js).!(xy)', 'asd.js.xy', False, glob.N], ['*.!(js).!(xy)', 'asd.sjs.zxy', True, glob.N], ['*.!(js).!(xy)', 'asd..xyz', True, glob.N], ['*.!(js).!(xy)', 'asd..xy', False, glob.N], ['*.!(js|x).!(xy)', 'asd..xy', False, glob.N], ['*.!(js)', 'foo.js.js', True, glob.N], ['*(*.json|!(*.js))', 'testjson.json', True, glob.N], ['+(*.json|!(*.js))', 'testjson.json', True, glob.N], ['@(*.json|!(*.js))', 'testjson.json', True, glob.N], ['?(*.json|!(*.js))', 'testjson.json', True, glob.N], ['*(*.json|!(*.js))', 'foojs.js', False, glob.N], # XXX bash 4.3 disagrees! ['+(*.json|!(*.js))', 'foojs.js', False, glob.N], # XXX bash 4.3 disagrees! ['@(*.json|!(*.js))', 'foojs.js', False, glob.N], ['?(*.json|!(*.js))', 'foojs.js', False, glob.N], ['*(*.json|!(*.js))', 'other.bar', True, glob.N], ['+(*.json|!(*.js))', 'other.bar', True, glob.N], ['@(*.json|!(*.js))', 'other.bar', True, glob.N], ['?(*.json|!(*.js))', 'other.bar', True, glob.N], # Complex inverse cases ['!(not )@(this)', 'not this', False, glob.N], ['!(not )@(this)', 'but this', True, glob.N], ['!(not)!( this)', 'not this', True, glob.N], ['!(not @(this ))@(okay)', 'not this okay', False, glob.N], ['!(not @(this ))@(okay)', 'but this okay', True, glob.N], ['!(not !(this ))@(okay)', 'but this okay', True, glob.N], ['!(but !(that ))@(okay)', 'but this okay', False, glob.N], ['!(but !(this ))@(okay)', 'but this okay', True, glob.N], ['!(not)!( this)@( okay)', 'but this okay', True, glob.N], ['@(but!( that))@( okay)', "but this okay", True, glob.N], ['!(@(but!( that))@( okay))', "but this okay", False, glob.N], ] @classmethod def setup_class(cls): """Setup default flag options.""" # The tests we scraped were written with this assumed. cls.flags = glob.NEGATE | glob.GLOBSTAR | glob.EXTGLOB | glob.BRACE @classmethod def evaluate(cls, case): """Evaluate case.""" pattern = case[0] filename = case[1] goal = case[2] flags = cls.flags if len(case) > 3: flags ^= case[3] print("PATTERN: ", pattern) print("FILE: ", filename) print("GOAL: ", goal) print("FLAGS: ", bin(flags)) assert glob.globmatch(filename, pattern, flags=flags) == goal, "Expression did not evaluate as %s" % goal @pytest.mark.parametrize("case", cases) def test_cases(self, case): """Test ignore cases.""" self.evaluate(case) class TestGlobMatchSpecial(unittest.TestCase): """Test special cases that cannot easily be covered in earlier tests.""" def setUp(self): """Setup default flag options.""" self.flags = glob.NEGATE | glob.GLOBSTAR | glob.EXTGLOB | glob.BRACE def test_unfinished_ext(self): """Test unfinished ext.""" flags = self.flags flags ^= glob.NEGATE for x in ['!', '?', '+', '*', '@']: self.assertTrue(glob.globmatch(x + '(a|B', x + '(a|B', flags=flags)) self.assertFalse(glob.globmatch(x + '(a|B', 'B', flags=flags)) def test_empty_pattern_lists(self): """Test empty pattern lists.""" self.assertFalse(glob.globmatch('test', [])) self.assertEqual(glob.globfilter(['test'], []), []) def test_windows_drives(self): """Test windows drives.""" flags = self.flags flags |= glob.FORCEWIN self.assertTrue( glob.globmatch( '//?/c:/somepath/to/match/file.txt', '//?/c:/**/*.txt', flags=flags ) ) self.assertTrue( glob.globmatch( 'c:/somepath/to/match/file.txt', 'c:/**/*.txt', flags=flags ) ) def test_glob_parsing_win(self): """Test windows style glob parsing.""" flags = self.flags flags |= glob.FORCEWIN _wcparse._compile.cache_clear() self.assertTrue( glob.globmatch( 'some/name/with/named/file/test.py', '**/named/file/*.py', flags=flags ) ) self.assertTrue( glob.globmatch( 'some/name/with/na[/]med/file/test.py', '**/na[/]med/file/*.py', flags=flags ) ) self.assertTrue( glob.globmatch( 'some/name/with/na[/]med\\/file/test.py', '**/na[/]med\\/file/*.py', flags=flags ) ) self.assertTrue( glob.globmatch( 'some/name/with/na[\\]med/file/test.py', R'**/na[\\]med/file/*.py', flags=flags | glob.R ) ) self.assertTrue( glob.globmatch( 'some\\name\\with\\na[\\]med\\file\\test.py', R'**/na[\\]med/file/*.py', flags=flags | glob.R ) ) self.assertTrue( glob.globmatch( 'some\\name\\with\\na[\\]med\\file*.py', R'**\\na[\\]med\\file\*.py', flags=flags | glob.R ) ) self.assertTrue( glob.globmatch( 'some\\name\\with\\na[\\]med\\file\\test.py', R'**\\na[\\]m\ed\\file\\*.py', flags=flags | glob.R ) ) self.assertTrue( glob.globmatch( 'some\\name\\with\\na[\\]med\\\\file\\test.py', R'**\\na[\\]m\ed\\/file\\*.py', flags=flags | glob.R ) ) self.assertTrue( glob.globmatch( 'some\\name\\with\\na[\\\\]med\\\\file\\test.py', R'**\\na[\/]m\ed\/file\\*.py', flags=flags | glob.R ) ) def test_glob_translate_no_dir(self): """Test that an additional pattern is injected in translate.""" pos, neg = glob.translate('**', flags=glob.G) self.assertEqual(1, len(pos)) self.assertEqual(0, len(neg)) pos, neg = glob.translate('**', flags=glob.G | glob.O) self.assertEqual(1, len(pos)) self.assertEqual(1, len(neg)) pos, neg = glob.translate(b'**', flags=glob.G) self.assertEqual(1, len(pos)) self.assertEqual(0, len(neg)) pos, neg = glob.translate(b'**', flags=glob.G | glob.O) self.assertEqual(1, len(pos)) self.assertEqual(1, len(neg)) def test_capture_groups(self): """Test capture groups.""" gpat = glob.translate("test/@(this)/+(many)/?(meh)*(!)/!(not this)@(.md)", flags=glob.E) pat = re.compile(gpat[0][0]) match = pat.match(os.path.normpath('test/this/manymanymany/meh!!!!!/okay.md')) self.assertEqual(('this', 'manymanymany', 'meh', '!!!!!', 'okay', '.md'), match.groups()) def test_nested_capture_groups(self): """Test nested capture groups.""" gpat = glob.translate("@(file)@(+([[:digit:]]))@(.*)", flags=glob.E) pat = re.compile(gpat[0][0]) match = pat.match('file33.test.txt') self.assertEqual(('file', '33', '33', '.test.txt'), match.groups()) def test_list_groups(self): """Test capture groups with lists.""" gpat = glob.translate("+(f|i|l|e)+([[:digit:]])@(.*)", flags=glob.E) pat = re.compile(gpat[0][0]) match = pat.match('file33.test.txt') self.assertEqual(('file', '33', '.test.txt'), match.groups()) def test_glob_translate(self): """Test glob translation.""" flags = self.flags flags |= glob.FORCEUNIX value = ( [ '^(?s:(?:(?!(?:[/]|^)\\.).)*?(?:^|$|[/])+' + ('(?!(?:\\.{1,2})(?:$|[/]))(?![/.])[\x00-\x7f][/]+stuff[/]+(?=[^/])') + '(?!(?:\\.{1,2})(?:$|[/]))(?:(?!\\.)[^/]*?)?[/]*?)$' ], [] ) self.assertEqual( glob.translate('**/[[:ascii:]]/stuff/*', flags=flags), value ) def test_glob_parsing_nix(self): """Test wildcard parsing.""" flags = self.flags flags |= glob.FORCEUNIX self.assertTrue( glob.globmatch( 'some/name/with/named/file/test.py', '**/named/file/*.py', flags=flags ) ) self.assertTrue( glob.globmatch( 'some/name/with/na[/]med/file/test.py', '**/na[/]med/file/*.py', flags=flags ) ) self.assertTrue( glob.globmatch( 'some/name/with/na[/]med\\/file/test.py', '**/na[/]med\\\\/file/*.py', flags=flags ) ) self.assertTrue( glob.globmatch( 'some/name/with/na\\med/file/test.py', R'**/na[\\]med/file/*.py', flags=flags | glob.R ) ) self.assertTrue( glob.globmatch( 'some/name/with/na[\\/]med\\/file/test.py', R'**/na[\\/]med\\/file/*.py', flags=flags | glob.R ) ) def test_glob_translate_real_has_no_positive_default(self): """Test that `REALPATH` translations provide a default positive pattern.""" pos, neg = glob.translate('!this', flags=self.flags) self.assertTrue(len(pos) == 0) self.assertTrue(len(neg) == 1) pos, neg = glob.translate('!this', flags=self.flags | glob.REALPATH) self.assertTrue(len(pos) == 0) self.assertTrue(len(neg) == 1) def test_glob_match_real(self): """Test real `globmatch` vs regular `globmatch`.""" # When there is no context from the file system, # `globmatch` can't determine folder with no trailing slash. self.assertFalse(glob.globmatch('docs/src', '**/src/**', flags=self.flags)) self.assertTrue(glob.globmatch('docs/src/', '**/src/**', flags=self.flags)) self.assertTrue(glob.globmatch('docs/src', '**/src/**', flags=self.flags | glob.REALPATH)) self.assertTrue(glob.globmatch('docs/src/', '**/src/**', flags=self.flags | glob.REALPATH)) # Missing files will only match in `globmatch` without context from file system. self.assertTrue(glob.globmatch('bad/src/', '**/src/**', flags=self.flags)) self.assertFalse(glob.globmatch('bad/src/', '**/src/**', flags=self.flags | glob.REALPATH)) def test_glob_match_real_bytes(self): """Test real `globmatch` vs regular `globmatch` with bytes strings.""" # When there is no context from the file system, # `globmatch` can't determine folder with no trailing slash. self.assertFalse(glob.globmatch(b'docs/src', b'**/src/**', flags=self.flags)) self.assertTrue(glob.globmatch(b'docs/src/', b'**/src/**', flags=self.flags)) self.assertTrue(glob.globmatch(b'docs/src', b'**/src/**', flags=self.flags | glob.REALPATH)) self.assertTrue(glob.globmatch(b'docs/src/', b'**/src/**', flags=self.flags | glob.REALPATH)) # Missing files will only match in `globmatch` without context from file system. self.assertTrue(glob.globmatch(b'bad/src/', b'**/src/**', flags=self.flags)) self.assertFalse(glob.globmatch(b'bad/src/', b'**/src/**', flags=self.flags | glob.REALPATH)) def test_glob_match_real_outside_curdir(self): """Test that real `globmatch` will not allow match outside current directory unless using an absolute path.""" # Let's find something predictable for this cross platform test. user_dir = os.path.expanduser('~') if user_dir != '~': glob_user = glob.escape(user_dir) self.assertFalse(glob.globmatch(user_dir, '**', flags=self.flags | glob.REALPATH)) self.assertTrue(glob.globmatch(user_dir, glob_user + '/**', flags=self.flags | glob.REALPATH)) def test_glob_integrity_bytes(self): """Test glob integrity to exercises the bytes portion of the code.""" self.assertTrue( all( glob.globmatch( x, b'!**/*.md', flags=self.flags | glob.SPLIT ) for x in glob.glob(b'!**/*.md', flags=self.flags | glob.SPLIT) ) ) def test_glob_integrity(self): """`globmatch` must match what glob globs.""" # Number of slashes is inconsequential # Glob really looks at what is in between. Multiple slashes are the same as one separator. # UNC mounts are special cases and it matters there. self.assertTrue( all( glob.globmatch( x, '**/../*.{md,py}', flags=self.flags ) for x in glob.glob('**/../*.{md,py}', flags=self.flags) ) ) self.assertTrue( all( glob.globmatch( x, './**/./../*.py', flags=self.flags ) for x in glob.glob('./**/./../*.py', flags=self.flags) ) ) self.assertTrue( all( glob.globmatch( x, './///**///./../*.py', flags=self.flags ) for x in glob.glob('./**/.//////..////*.py', flags=self.flags) ) ) self.assertTrue( all( glob.globmatch( x, '**/docs/**', flags=self.flags ) for x in glob.glob('**/docs/**', flags=self.flags) ) ) self.assertTrue( all( glob.globmatch( x, '**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT ) for x in glob.glob('**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT) ) ) self.assertTrue( all( glob.globmatch( x, '!**/*.md', flags=self.flags | glob.SPLIT ) for x in glob.glob('!**/*.md', flags=self.flags | glob.SPLIT) ) ) self.assertFalse( all( glob.globmatch( x, '**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT ) for x in glob.glob('**/docs/**', flags=self.flags | glob.SPLIT) ) ) def test_glob_integrity_marked(self): """`globmatch` must match what glob globs with marked directories.""" # Number of slashes is inconsequential # Glob really looks at what is in between. Multiple slashes are the same as one separator. # UNC mounts are special cases and it matters there. self.assertTrue( all( glob.globmatch( x, '**/docs/**', flags=self.flags | glob.MARK ) for x in glob.glob('**/docs/**', flags=self.flags | glob.MARK) ) ) self.assertTrue( all( glob.globmatch( x, '**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT | glob.MARK ) for x in glob.glob('**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT | glob.MARK) ) ) self.assertTrue( all( glob.globmatch( x, '!**/*.md', flags=self.flags | glob.SPLIT | glob.MARK ) for x in glob.glob('!**/*.md', flags=self.flags | glob.SPLIT | glob.MARK) ) ) self.assertFalse( all( glob.globmatch( x, '**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT | glob.MARK ) for x in glob.glob('**/docs/**', flags=self.flags | glob.SPLIT | glob.MARK) ) ) def test_glob_integrity_real(self): """`globmatch` must match what glob globs against the real file system.""" # Number of slashes is inconsequential # Glob really looks at what is in between. Multiple slashes are the same as one separator. # UNC mounts are special cases and it matters there. self.assertTrue( all( glob.globmatch( x, '**/../*.{md,py}', flags=self.flags | glob.REALPATH ) for x in glob.glob('**/../*.{md,py}', flags=self.flags) ) ) self.assertTrue( all( glob.globmatch( x, './**/./../*.py', flags=self.flags | glob.REALPATH ) for x in glob.glob('./**/./../*.py', flags=self.flags) ) ) self.assertTrue( all( glob.globmatch( x, './///**///./../*.py', flags=self.flags | glob.REALPATH ) for x in glob.glob('./**/.//////..////*.py', flags=self.flags) ) ) self.assertTrue( all( glob.globmatch( x, '**/docs/**', flags=self.flags | glob.REALPATH ) for x in glob.glob('**/docs/**', flags=self.flags) ) ) self.assertTrue( all( glob.globmatch( x, '**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT | glob.REALPATH ) for x in glob.glob('**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT) ) ) self.assertTrue( all( glob.globmatch( x, '!**/*.md', flags=self.flags | glob.SPLIT | glob.REALPATH ) for x in glob.glob('!**/*.md', flags=self.flags | glob.SPLIT) ) ) self.assertFalse( all( glob.globmatch( x, '**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT | glob.REALPATH ) for x in glob.glob('**/docs/**', flags=self.flags | glob.SPLIT) ) ) def test_glob_integrity_real_marked(self): """`globmatch` must match what glob globs against the real file system and marked directories.""" # Number of slashes is inconsequential # Glob really looks at what is in between. Multiple slashes are the same as one separator. # UNC mounts are special cases and it matters there. self.assertTrue( all( glob.globmatch( x, '**/docs/**', flags=self.flags | glob.REALPATH | glob.MARK ) for x in glob.glob('**/docs/**', flags=self.flags | glob.MARK) ) ) self.assertTrue( all( glob.globmatch( x, '**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT | glob.REALPATH | glob.MARK ) for x in glob.glob('**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT | glob.MARK) ) ) self.assertTrue( all( glob.globmatch( x, '!**/*.md', flags=self.flags | glob.SPLIT | glob.REALPATH | glob.MARK ) for x in glob.glob('!**/*.md', flags=self.flags | glob.SPLIT | glob.MARK) ) ) self.assertFalse( all( glob.globmatch( x, '**/docs/**|!**/*.md', flags=self.flags | glob.SPLIT | glob.REALPATH | glob.MARK ) for x in glob.glob('**/docs/**', flags=self.flags | glob.SPLIT | glob.MARK) ) ) @unittest.skipUnless(sys.platform.startswith('win'), "Windows specific test") def test_glob_match_real_ignore_forceunix(self): """Ignore `FORCEUNIX` when using `globmatch` real.""" self.assertTrue(glob.globmatch('docs/', '**/DOCS/**', flags=self.flags | glob.REALPATH | glob.FORCEUNIX)) @unittest.skipUnless(not sys.platform.startswith('win'), "Non Windows test") def test_glob_match_real_ignore_forcewin(self): """Ignore `FORCEWIN` when using `globmatch` real.""" self.assertFalse(glob.globmatch('docs/', '**/DOCS/**', flags=self.flags | glob.REALPATH | glob.FORCEWIN)) self.assertTrue( glob.globmatch('docs/', '**/DOCS/**', flags=self.flags | glob.REALPATH | glob.FORCEWIN | glob.I) ) def test_glob_match_ignore_forcewin_forceunix(self): """Ignore `FORCEUNIX` and `FORCEWIN` when both are used.""" if sys.platform.startswith('win'): self.assertTrue(glob.globmatch('docs/', '**/DOCS/**', flags=self.flags | glob.FORCEWIN | glob.FORCEUNIX)) else: self.assertFalse(glob.globmatch('docs/', '**/DOCS/**', flags=self.flags | glob.FORCEWIN | glob.FORCEUNIX)) self.assertTrue(glob.globmatch('docs/', '**/docs/**', flags=self.flags | glob.FORCEWIN | glob.FORCEUNIX)) def test_root_dir(self): """Test root directory with `globmatch`.""" self.assertFalse(glob.globmatch('markdown', 'markdown', flags=glob.REALPATH)) self.assertTrue(glob.globmatch('markdown', 'markdown', flags=glob.REALPATH, root_dir='docs/src')) def test_match_root_dir_pathlib(self): """Test root directory with `globmatch` using `pathlib`.""" from wcmatch import pathlib self.assertFalse(glob.globmatch(pathlib.Path('markdown'), 'markdown', flags=glob.REALPATH)) self.assertTrue( glob.globmatch(pathlib.Path('markdown'), 'markdown', flags=glob.REALPATH, root_dir=pathlib.Path('docs/src')) ) def test_match_pathlib_str_bytes(self): """Test that mismatch type of `pathlib` and `bytes` asserts.""" from wcmatch import pathlib with self.assertRaises(TypeError): glob.globmatch(pathlib.Path('markdown'), b'markdown') def test_match_str_bytes(self): """Test that mismatch type of `str` and `bytes` asserts.""" with self.assertRaises(TypeError): glob.globmatch('markdown', b'markdown') def test_match_bytes_pathlib_str_realpath(self): """Test that mismatch type of `pathlib` and bytes asserts.""" from wcmatch import pathlib with self.assertRaises(TypeError): glob.globmatch( pathlib.Path('markdown'), b'markdown', flags=glob.REALPATH ) def test_match_bytes_root_dir_pathlib_realpath(self): """Test that mismatch type of root directory `pathlib` and `bytes` asserts.""" from wcmatch import pathlib with self.assertRaises(TypeError): glob.globmatch( b'markdown', b'markdown', flags=glob.REALPATH, root_dir=pathlib.Path('.') ) def test_match_bytes_root_dir_str_realpath(self): """Test that mismatch type of root directory `pathlib` and `bytes` asserts.""" with self.assertRaises(TypeError): glob.globmatch( b'markdown', b'markdown', flags=glob.REALPATH, root_dir='.' ) def test_match_str_root_dir_bytes_realpath(self): """Test that mismatch type of root directory of `bytes` and `str` asserts.""" with self.assertRaises(TypeError): glob.globmatch( 'markdown', 'markdown', flags=glob.REALPATH, root_dir=b'.' ) def test_filter_root_dir_pathlib(self): """Test root directory with `globfilter`.""" from wcmatch import pathlib results = glob.globfilter( [pathlib.Path('markdown')], 'markdown', flags=glob.REALPATH, root_dir=pathlib.Path('docs/src') ) self.assertTrue(all(isinstance(result, pathlib.Path) for result in results)) def test_filter_root_dir_pathlib_bytes(self): """Test root directory with `globfilter`.""" from wcmatch import pathlib with self.assertRaises(TypeError): glob.globfilter( [pathlib.Path('markdown')], b'markdown', flags=glob.REALPATH, root_dir=pathlib.Path('docs/src') ) @skip_unless_symlink class TestGlobmatchSymlink(_TestGlobmatch): """Test symlinks.""" def mksymlink(self, original, link): """Make symlink.""" if not os.path.lexists(link): os.symlink(original, link) def setUp(self): """Setup.""" self.tempdir = TESTFN + "_dir" self.mktemp('.hidden', 'a.txt') self.mktemp('.hidden', 'b.file') self.mktemp('.hidden_file') self.mktemp('a.txt') self.mktemp('b.file') self.mktemp('c.txt.bak') self.can_symlink = can_symlink() if self.can_symlink: self.mksymlink('.hidden', self.norm('sym1')) self.mksymlink(os.path.join('.hidden', 'a.txt'), self.norm('sym2')) self.default_flags = glob.G | glob.P | glob.B def test_globmatch_symlink(self): """Test `globmatch` with symlinks.""" self.assertFalse(glob.globmatch(self.tempdir + '/sym1/a.txt', '**/*.txt}', flags=self.default_flags)) self.assertTrue(glob.globmatch(self.tempdir + '/a.txt', '**/*.txt', flags=self.default_flags)) self.assertTrue(glob.globmatch(self.tempdir + '/sym1/', '**', flags=self.default_flags)) def test_globmatch_follow_symlink(self): """Test `globmatch` with symlinks that we follow.""" self.assertTrue(glob.globmatch(self.tempdir + '/sym1/a.txt', '**/*.txt', flags=self.default_flags | glob.L)) self.assertTrue(glob.globmatch(self.tempdir + '/a.txt', '**/*.txt', flags=self.default_flags | glob.L)) self.assertTrue(glob.globmatch(self.tempdir + '/sym1/', '**', flags=self.default_flags)) def test_globmatch_trigger_symlink_cache(self): """Use a pattern that exercises the symlink cache.""" self.assertFalse(glob.globmatch(self.tempdir + '/sym1/a.txt', '**/{*.txt,*.t*}', flags=self.default_flags)) def test_globmatch_globstarlong(self): """Test `***`.""" flags = glob.GL | glob.P self.assertTrue(glob.globmatch(self.tempdir + '/sym1/a.txt', '***/*.txt', flags=flags)) self.assertFalse(glob.globmatch(self.tempdir + '/sym1/a.txt', '**/*.txt', flags=flags)) def test_globmatch_globstarlong_follow(self): """Test `***` with `FOLLOW`.""" flags = glob.GL | glob.P | glob.L self.assertTrue(glob.globmatch(self.tempdir + '/sym1/a.txt', '***/*.txt', flags=flags)) self.assertFalse(glob.globmatch(self.tempdir + '/sym1/a.txt', '**/*.txt', flags=flags)) def test_globmatch_globstarlong_matchbase(self): """Test `***` with `MATCHBASE`.""" flags = glob.GL | glob.P | glob.X self.assertFalse(glob.globmatch(self.tempdir + '/sym1/a.txt', '*.txt', flags=flags)) def test_globmatch_globstarlong_matchbase_follow(self): """Test `***` with `MATCHBASE`.""" flags = glob.GL | glob.P | glob.X | glob.L self.assertTrue(glob.globmatch(self.tempdir + '/sym1/a.txt', '*.txt', flags=flags)) @unittest.skipUnless(os.path.expanduser('~') != '~', "Requires expand user functionality") class TestTilde(unittest.TestCase): """Test tilde cases.""" def test_tilde_globmatch(self): """Test tilde in `globmatch` environment.""" files = os.listdir(os.path.expanduser('~')) gfiles = glob.globfilter( glob.glob('~/*', flags=glob.T | glob.D), '~/*', flags=glob.T | glob.D | glob.P ) self.assertEqual(len(files), len(gfiles)) def test_tilde_globmatch_no_realpath(self): """Test tilde in `globmatch` environment but with real path disabled.""" files = os.listdir(os.path.expanduser('~')) gfiles = glob.globfilter( glob.glob('~/*', flags=glob.T | glob.D), '~/*', flags=glob.T | glob.D ) self.assertNotEqual(len(files), len(gfiles)) def test_tilde_globmatch_no_tilde(self): """Test tilde in `globmatch` environment but with tilde disabled.""" files = os.listdir(os.path.expanduser('~')) gfiles = glob.globfilter( glob.glob('~/*', flags=glob.T | glob.D), '~/*', flags=glob.D | glob.P ) self.assertNotEqual(len(files), len(gfiles)) class TestIsMagic(unittest.TestCase): """Test "is magic" logic.""" def test_default(self): """Test default magic.""" self.assertTrue(glob.is_magic("test*")) self.assertTrue(glob.is_magic("test[")) self.assertTrue(glob.is_magic("test]")) self.assertTrue(glob.is_magic("test?")) self.assertTrue(glob.is_magic("test\\")) self.assertFalse(glob.is_magic("test~!()-/|{}")) def test_extmatch(self): """Test extended match magic.""" self.assertTrue(glob.is_magic("test*", flags=glob.EXTGLOB)) self.assertTrue(glob.is_magic("test[", flags=glob.EXTGLOB)) self.assertTrue(glob.is_magic("test]", flags=glob.EXTGLOB)) self.assertTrue(glob.is_magic("test?", flags=glob.EXTGLOB)) self.assertTrue(glob.is_magic("test\\", flags=glob.EXTGLOB)) self.assertTrue(glob.is_magic("test(", flags=glob.EXTGLOB)) self.assertTrue(glob.is_magic("test)", flags=glob.EXTGLOB)) self.assertFalse(glob.is_magic("test~!-/|{}", flags=glob.EXTGLOB)) def test_negate(self): """Test negate magic.""" self.assertTrue(glob.is_magic("test*", flags=glob.NEGATE)) self.assertTrue(glob.is_magic("test[", flags=glob.NEGATE)) self.assertTrue(glob.is_magic("test]", flags=glob.NEGATE)) self.assertTrue(glob.is_magic("test?", flags=glob.NEGATE)) self.assertTrue(glob.is_magic("test\\", flags=glob.NEGATE)) self.assertTrue(glob.is_magic("test!", flags=glob.NEGATE)) self.assertFalse(glob.is_magic("test~()-/|{}", flags=glob.NEGATE)) def test_minusnegate(self): """Test minus negate magic.""" self.assertTrue(glob.is_magic("test*", flags=glob.NEGATE | glob.MINUSNEGATE)) self.assertTrue(glob.is_magic("test[", flags=glob.NEGATE | glob.MINUSNEGATE)) self.assertTrue(glob.is_magic("test]", flags=glob.NEGATE | glob.MINUSNEGATE)) self.assertTrue(glob.is_magic("test?", flags=glob.NEGATE | glob.MINUSNEGATE)) self.assertTrue(glob.is_magic("test\\", flags=glob.NEGATE | glob.MINUSNEGATE)) self.assertTrue(glob.is_magic("test-", flags=glob.NEGATE | glob.MINUSNEGATE)) self.assertFalse(glob.is_magic("test~()!/|{}", flags=glob.NEGATE | glob.MINUSNEGATE)) def test_brace(self): """Test brace magic.""" self.assertTrue(glob.is_magic("test*", flags=glob.BRACE)) self.assertTrue(glob.is_magic("test[", flags=glob.BRACE)) self.assertTrue(glob.is_magic("test]", flags=glob.BRACE)) self.assertTrue(glob.is_magic("test?", flags=glob.BRACE)) self.assertTrue(glob.is_magic("test\\", flags=glob.BRACE)) self.assertTrue(glob.is_magic("test{", flags=glob.BRACE)) self.assertTrue(glob.is_magic("test}", flags=glob.BRACE)) self.assertFalse(glob.is_magic("test~!-/|", flags=glob.BRACE)) def test_split(self): """Test split magic.""" self.assertTrue(glob.is_magic("test*", flags=glob.SPLIT)) self.assertTrue(glob.is_magic("test[", flags=glob.SPLIT)) self.assertTrue(glob.is_magic("test]", flags=glob.SPLIT)) self.assertTrue(glob.is_magic("test?", flags=glob.SPLIT)) self.assertTrue(glob.is_magic("test\\", flags=glob.SPLIT)) self.assertTrue(glob.is_magic("test|", flags=glob.SPLIT)) self.assertFalse(glob.is_magic("test~()-!/", flags=glob.SPLIT)) def test_tilde(self): """Test tilde magic.""" self.assertTrue(glob.is_magic("test*", flags=glob.GLOBTILDE)) self.assertTrue(glob.is_magic("test[", flags=glob.GLOBTILDE)) self.assertTrue(glob.is_magic("test]", flags=glob.GLOBTILDE)) self.assertTrue(glob.is_magic("test?", flags=glob.GLOBTILDE)) self.assertTrue(glob.is_magic("test\\", flags=glob.GLOBTILDE)) self.assertTrue(glob.is_magic("test~", flags=glob.GLOBTILDE)) self.assertFalse(glob.is_magic("test|()-!/", flags=glob.GLOBTILDE)) def test_all(self): """Test tilde magic.""" flags = ( glob.EXTGLOB | glob.NEGATE | glob.BRACE | glob.SPLIT | glob.GLOBTILDE ) self.assertTrue(glob.is_magic("test*", flags=flags)) self.assertTrue(glob.is_magic("test[", flags=flags)) self.assertTrue(glob.is_magic("test]", flags=flags)) self.assertTrue(glob.is_magic("test?", flags=flags)) self.assertTrue(glob.is_magic(r"te\\st", flags=flags)) self.assertTrue(glob.is_magic(r"te\st", flags=flags)) self.assertTrue(glob.is_magic("test!", flags=flags)) self.assertTrue(glob.is_magic("test|", flags=flags)) self.assertTrue(glob.is_magic("test(", flags=flags)) self.assertTrue(glob.is_magic("test)", flags=flags)) self.assertTrue(glob.is_magic("test{", flags=flags)) self.assertTrue(glob.is_magic("test}", flags=flags)) self.assertTrue(glob.is_magic("test~", flags=flags)) self.assertTrue(glob.is_magic("test-", flags=flags | glob.MINUSNEGATE)) self.assertFalse(glob.is_magic("test-", flags=flags)) self.assertFalse(glob.is_magic("test!", flags=flags | glob.MINUSNEGATE)) def test_all_bytes(self): """Test tilde magic.""" flags = ( glob.EXTGLOB | glob.NEGATE | glob.BRACE | glob.SPLIT | glob.GLOBTILDE ) self.assertTrue(glob.is_magic(b"test*", flags=flags)) self.assertTrue(glob.is_magic(b"test[", flags=flags)) self.assertTrue(glob.is_magic(b"test]", flags=flags)) self.assertTrue(glob.is_magic(b"test?", flags=flags)) self.assertTrue(glob.is_magic(rb"te\\st", flags=flags)) self.assertTrue(glob.is_magic(rb"te\st", flags=flags)) self.assertTrue(glob.is_magic(b"test!", flags=flags)) self.assertTrue(glob.is_magic(b"test|", flags=flags)) self.assertTrue(glob.is_magic(b"test(", flags=flags)) self.assertTrue(glob.is_magic(b"test)", flags=flags)) self.assertTrue(glob.is_magic(b"test{", flags=flags)) self.assertTrue(glob.is_magic(b"test}", flags=flags)) self.assertTrue(glob.is_magic(b"test~", flags=flags)) self.assertTrue(glob.is_magic(b"test-", flags=flags | glob.MINUSNEGATE)) self.assertFalse(glob.is_magic(b"test-", flags=flags)) self.assertFalse(glob.is_magic(b"test!", flags=flags | glob.MINUSNEGATE)) def test_win_path(self): """Test windows path.""" flags = ( glob.EXTGLOB | glob.NEGATE | glob.FORCEWIN | glob.GLOBTILDE ) self.assertFalse(glob.is_magic('//?/UNC/server/*[]!|(){}~-/', flags=flags)) self.assertFalse(glob.is_magic('//?/UNC/server/*[]!|()~-/', flags=flags | glob.BRACE)) self.assertFalse(glob.is_magic('//?/UNC/server/*[]!(){}~-/', flags=flags | glob.SPLIT)) self.assertTrue(glob.is_magic('//?/UNC/server/*[]!|(){}|~-/', flags=flags | glob.BRACE)) self.assertTrue(glob.is_magic('//?/UNC/server/*[]!(){}|~-/', flags=flags | glob.SPLIT)) self.assertTrue(glob.is_magic(R'\\\\server\\mount\\', flags=flags)) class TestExpansionLimit(unittest.TestCase): """Test expansion limits.""" def test_limit_globmatch(self): """Test expansion limit of `globmatch`.""" with self.assertRaises(_wcparse.PatternLimitException): glob.globmatch('name', '{1..11}', flags=glob.BRACE, limit=10) def test_limit_filter(self): """Test expansion limit of `globfilter`.""" with self.assertRaises(_wcparse.PatternLimitException): glob.globfilter(['name'], '{1..11}', flags=glob.BRACE, limit=10) def test_limit_translate(self): """Test expansion limit of `translate`.""" with self.assertRaises(_wcparse.PatternLimitException): glob.translate('{1..11}', flags=glob.BRACE, limit=10) class TestExcludes(unittest.TestCase): """Test expansion limits.""" def test_translate_exclude(self): """Test exclusion in translation.""" results = glob.translate('*/somepath', exclude='test/somepath') self.assertTrue(len(results[0]) == 1 and len(results[1]) == 1) results = glob.translate(b'*/somepath', exclude=b'test/somepath') self.assertTrue(len(results[0]) == 1 and len(results[1]) == 1) def test_translate_exclude_mix(self): """ Test translate exclude mix. If both are given, flags are ignored. """ results = glob.translate(['*/somepath', '!test/somepath'], exclude=b'test/somepath', flags=glob.N | glob.A) self.assertTrue(len(results[0]) == 2 and len(results[1]) == 1) def test_exclude(self): """Test exclude parameter.""" self.assertTrue(glob.globmatch('path/name', '*/*', exclude='*/test')) self.assertTrue(glob.globmatch(b'path/name', b'*/*', exclude=b'*/test')) self.assertFalse(glob.globmatch('path/test', '*/*', exclude='*/test')) self.assertFalse(glob.globmatch(b'path/test', b'*/*', exclude=b'*/test')) def test_exclude_mix(self): """ Test exclusion flags mixed with exclusion parameter. If both are given, flags are ignored. """ self.assertTrue(glob.globmatch('path/name', '*/*', exclude='*/test', flags=glob.N | glob.A)) self.assertTrue(glob.globmatch(b'path/name', b'*/*', exclude=b'*/test', flags=glob.N | glob.A)) self.assertFalse(glob.globmatch('path/test', '*/*', exclude='*/test', flags=glob.N | glob.A)) self.assertFalse(glob.globmatch(b'path/test', b'*/*', exclude=b'*/test', flags=glob.N | glob.A)) self.assertTrue(glob.globmatch('path/name', ['*/*', '!path/name'], exclude='*/test', flags=glob.N | glob.A)) self.assertFalse(glob.globmatch('path/test', ['*/*', '!path/name'], exclude='*/test', flags=glob.N | glob.A)) self.assertTrue(glob.globmatch('!path/name', ['*/*', '!path/name'], exclude='*/test', flags=glob.N | glob.A)) def test_filter(self): """Test exclusion with filter.""" self.assertEqual(glob.globfilter(['path/name', 'path/test'], '*/*', exclude='path/test'), ['path/name']) wcmatch-10.0/tests/test_pathlib.py000066400000000000000000000425501467532413500172760ustar00rootroot00000000000000"""Test `pathlib`.""" import contextlib import pytest import unittest import os from wcmatch import pathlib, glob, _wcparse import pathlib as pypathlib import pickle import warnings import shutil # Below is general helper stuff that Python uses in `unittests`. As these # not meant for users, and could change without notice, include them # ourselves so we aren't surprised later. TESTFN = '@test' # Disambiguate `TESTFN` for parallel testing, while letting it remain a valid # module name. TESTFN = "{}_{}_tmp".format(TESTFN, os.getpid()) def create_empty_file(filename): """Create an empty file. If the file already exists, truncate it.""" fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) os.close(fd) _can_symlink = None def can_symlink(): """Check if we can symlink.""" global _can_symlink if _can_symlink is not None: return _can_symlink symlink_path = TESTFN + "can_symlink" try: os.symlink(TESTFN, symlink_path) can = True except (OSError, NotImplementedError, AttributeError): can = False else: os.remove(symlink_path) _can_symlink = can return can def skip_unless_symlink(test): """Skip decorator for tests that require functional symlink.""" ok = can_symlink() msg = "Requires functional symlink implementation" return test if ok else unittest.skip(msg)(test) @contextlib.contextmanager def change_cwd(path, quiet=False): """ Return a context manager that changes the current working directory. Arguments: --------- path: the directory to use as the temporary current working directory. quiet: if False (the default), the context manager raises an exception on error. Otherwise, it issues only a warning and keeps the current working directory the same. """ saved_dir = os.getcwd() try: os.chdir(path) except OSError: if not quiet: raise warnings.warn('tests may fail, unable to change CWD to: ' + path, RuntimeWarning, stacklevel=3) try: yield os.getcwd() finally: os.chdir(saved_dir) class TestGlob(unittest.TestCase): """ Test file globbing. NOTE: We are not testing the actual `glob` library, just the interface on the `pathlib` object and specifics introduced by the particular function. """ def test_relative(self): """Test relative path.""" abspath = os.path.abspath('.') p = pathlib.Path(abspath) with change_cwd(os.path.dirname(abspath)): results = list(p.glob('docs/**/*.md', flags=pathlib.GLOBSTAR)) self.assertTrue(len(results)) self.assertTrue(all(file.suffix == '.md' for file in results)) def test_relative_exclude(self): """Test relative path exclude.""" abspath = os.path.abspath('.') p = pathlib.Path(abspath) with change_cwd(os.path.dirname(abspath)): results = list(p.glob('docs/**/*.md|!**/index.md', flags=pathlib.GLOBSTAR | pathlib.NEGATE | pathlib.SPLIT)) self.assertTrue(len(results)) self.assertTrue(all(file.name != 'index.md' for file in results)) def test_glob(self): """Test globbing function.""" p = pathlib.Path('docs') results = list(p.glob('*.md')) self.assertTrue(not results) results = list(p.glob('**/*.md', flags=pathlib.GLOBSTAR)) self.assertTrue(len(results)) self.assertTrue(all(file.suffix == '.md' for file in results)) def test_rglob(self): """Test globbing function.""" p = pathlib.Path('docs') results = list(p.rglob('*.md')) self.assertTrue(len(results)) self.assertTrue(all(file.suffix == '.md' for file in results)) results = list(p.rglob('*.md')) self.assertTrue(len(results)) self.assertTrue(all(file.suffix == '.md' for file in results)) results = list(p.rglob('markdown/*.md')) self.assertTrue(len(results)) self.assertTrue(all(file.suffix == '.md' for file in results)) def test_integrity(self): """Test glob integrity, or better put, test the path structure comes out sane.""" orig = [pathlib.Path(x) for x in glob.iglob('docs/**/*.md', flags=glob.GLOBSTAR)] results = list(pathlib.Path('docs').glob('**/*.md', flags=glob.GLOBSTAR)) self.assertEqual(orig, results) orig = [pathlib.Path(x) for x in glob.iglob('**/*.md', flags=glob.GLOBSTAR)] results = list(pathlib.Path('').glob('**/*.md', flags=glob.GLOBSTAR)) self.assertEqual(orig, results) class TestPathlibGlobmatch: """ Tests that are performed against `globmatch`. Each case entry is a list of 4 parameters. * Pattern * File name * Expected result (boolean of whether pattern matched file name) * Flags * Force Windows or Unix (string with `windows` or `unix`) The default flags are `NEGATE` | `EXTGLOB` | `BRACE`. Any flags passed through via entry are XORed. So if any of the default flags are passed via an entry, they will be disabled. All other flags will enable the feature. NOTE: We are not testing the actual `globmatch` library, just the interface on the `pathlib` object. """ cases = [ ['some/*/*/match', 'some/path/to/match', True, pathlib.G], ['some/**/match', 'some/path/to/match', False], ['some/**/match', 'some/path/to/match', True, pathlib.G], # `pathlib` doesn't keep trailing slash, so we can't tell it's a directory ['some/**/match/', 'some/path/to/match/', False, pathlib.G], ['.', '.', True], ['.', '', True], # `PurePath` ['some/*/*/match', 'some/path/to/match', True, pathlib.G, "pure"], ['some/**/match', 'some/path/to/match', False, 0, "pure"], ['some/**/match', 'some/path/to/match', True, pathlib.G, "pure"], ['some/**/match/', 'some/path/to/match/', False, pathlib.G, "pure"], ['.', '.', True, 0, "pure"], ['.', '', True, 0, "pure"], # Force a specific platform with a specific `PurePath`. ['//?/C:/**/file.log', R'\\?\C:\Path\path\file.log', True, pathlib.G, "windows"], ['/usr/*/bin', '/usr/local/bin', True, pathlib.G, "unix"] ] @classmethod def setup_class(cls): """Setup default flag options.""" # The tests we scraped were written with this assumed. cls.flags = pathlib.NEGATE | pathlib.EXTGLOB | pathlib.BRACE @classmethod def evaluate(cls, case): """Evaluate case.""" pattern = case[0] name = case[1] goal = case[2] flags = cls.flags path = None platform = "auto" if len(case) > 3: flags ^= case[3] if len(case) > 4: if case[4] == "windows": path = pathlib.PureWindowsPath(name) platform = case[4] elif case[4] == "unix": path = pathlib.PurePosixPath(name) platform = case[4] elif case[4] == "pure": path = pathlib.PurePath(name) if path is None: path = pathlib.Path(name) print('PATH: ', str(path)) print("PATTERN: ", pattern) print("FILE: ", name) print("GOAL: ", goal) print("FLAGS: ", bin(flags)) print("Platform: ", platform) cls.run(path, pattern, flags, goal) @classmethod def run(cls, path, pattern, flags, goal): """Run the command.""" assert path.globmatch(pattern, flags=flags) == goal, "Expression did not evaluate as %s" % goal @pytest.mark.parametrize("case", cases) def test_cases(self, case): """Test ignore cases.""" self.evaluate(case) def test_full_match_alias(self): """Test `full_match` alias.""" pathlib.PurePath('this/path/here').full_match('**/path/here', flags=pathlib.G) class TestPathlibMatch(TestPathlibGlobmatch): """ Test match method. NOTE: We are not testing the actual `globmatch` library, just the interface on the `pathlib` object and the additional behavior that match injects (recursive logic). """ cases = [ ['match', 'some/path/to/match', True], ['to/match', 'some/path/to/match', True], ['path/to/match', 'some/path/to/match', True], ['some/**/match', 'some/path/to/match', False], ['some/**/match', 'some/path/to/match', True, pathlib.G] ] @classmethod def run(cls, path, pattern, flags, goal): """Run the command.""" assert path.match(pattern, flags=flags) == goal, "Expression did not evaluate as %s" % goal class TestRealpath(unittest.TestCase): """Test real path of pure paths.""" def test_real_directory(self): """Test real directory.""" p = pathlib.PurePath('wcmatch') self.assertTrue(p.globmatch('*/', flags=pathlib.REALPATH)) self.assertTrue(p.globmatch('*', flags=pathlib.REALPATH)) def test_real_file(self): """Test real file.""" p = pathlib.PurePath('pyproject.toml') self.assertFalse(p.globmatch('*/', flags=pathlib.REALPATH)) self.assertTrue(p.globmatch('*', flags=pathlib.REALPATH)) class TestExceptions(unittest.TestCase): """Test exceptions.""" def test_bad_path(self): """Test bad path.""" with self.assertRaises(NotImplementedError): obj = pathlib.PosixPath if os.name == 'nt' else pathlib.WindowsPath obj('name') def test_bad_realpath(self): """Test bad real path.""" with self.assertRaises(ValueError): obj = pathlib.PurePosixPath if os.name == 'nt' else pathlib.PureWindowsPath p = obj('wcmatch') p.globmatch('*', flags=pathlib.REALPATH) def test_absolute_glob(self): """Test absolute patterns in `pathlib` glob.""" with self.assertRaises(ValueError): p = pathlib.Path('wcmatch') list(p.glob('/*')) def test_inverse_absolute_glob(self): """Test inverse absolute patterns in `pathlib` glob.""" with self.assertRaises(ValueError): p = pathlib.Path('wcmatch') list(p.glob('!/*', flags=pathlib.NEGATE)) class TestComparisons(unittest.TestCase): """Test comparison.""" def test_instance(self): """Test instance.""" p1 = pathlib.Path('wcmatch') p2 = pypathlib.Path('wcmatch') self.assertTrue(isinstance(p1, pathlib.Path)) self.assertTrue(isinstance(p1, pypathlib.Path)) self.assertFalse(isinstance(p2, pathlib.Path)) self.assertTrue(isinstance(p2, pypathlib.Path)) def test_equal(self): """Test equivalence.""" p1 = pathlib.Path('wcmatch') p2 = pypathlib.Path('wcmatch') p3 = pathlib.Path('docs') self.assertTrue(p1 == p2) self.assertFalse(p1 == p3) self.assertFalse(p3 == p2) def test_pure_equal(self): """Test equivalence.""" p1 = pathlib.PureWindowsPath('wcmatch') p2 = pathlib.PurePosixPath('wcmatch') p3 = pypathlib.PureWindowsPath('wcmatch') p4 = pypathlib.PurePosixPath('wcmatch') self.assertTrue(p1 != p2) self.assertTrue(p3 != p4) self.assertTrue(p1 == p3) self.assertTrue(p2 == p4) def test_flavour_equal(self): """Test that the same flavours equal each other, regardless of path type.""" p1 = pathlib.PurePath('wcmatch') p2 = pathlib.Path('wcmatch') p3 = pypathlib.PurePath('wcmatch') p4 = pypathlib.Path('wcmatch') self.assertTrue(p1 == p2) self.assertTrue(p3 == p4) self.assertTrue(p1 == p3) self.assertTrue(p2 == p4) self.assertTrue(p1 == p4) self.assertTrue(p2 == p3) def test_pickle(self): """Test pickling.""" p1 = pathlib.PurePath('wcmatch') p2 = pathlib.Path('wcmatch') p3 = pickle.loads(pickle.dumps(p1)) p4 = pickle.loads(pickle.dumps(p2)) self.assertTrue(type(p1) == type(p3)) # noqa: E721 self.assertTrue(type(p2) == type(p4)) # noqa: E721 self.assertTrue(type(p1) != type(p2)) # noqa: E721 self.assertTrue(type(p3) != type(p4)) # noqa: E721 class TestExpansionLimit(unittest.TestCase): """Test expansion limits.""" def test_limit_globmatch(self): """Test expansion limit of `globmatch`.""" with self.assertRaises(_wcparse.PatternLimitException): pathlib.PurePath('name').globmatch('{1..11}', flags=pathlib.BRACE, limit=10) def test_limit_match(self): """Test expansion limit of `match`.""" with self.assertRaises(_wcparse.PatternLimitException): pathlib.PurePath('name').match('{1..11}', flags=pathlib.BRACE, limit=10) def test_limit_glob(self): """Test expansion limit of `glob`.""" with self.assertRaises(_wcparse.PatternLimitException): list(pathlib.Path('.').glob('{1..11}', flags=pathlib.BRACE, limit=10)) def test_limit_rglob(self): """Test expansion limit of `rglob`.""" with self.assertRaises(_wcparse.PatternLimitException): list(pathlib.Path('.').rglob('{1..11}', flags=pathlib.BRACE, limit=10)) class TestExcludes(unittest.TestCase): """Test expansion limits.""" def test_exclude(self): """Test exclude parameter.""" self.assertTrue(pathlib.PurePath('path/name').globmatch('*/*', exclude='*/test')) self.assertFalse(pathlib.PurePath('path/test').globmatch('*/*', exclude='*/test')) @skip_unless_symlink class TestPathlibSymlink(unittest.TestCase): """Test the `pathlib` symlink class.""" def mktemp(self, *parts): """Make temp directory.""" filename = self.norm(*parts) base, file = os.path.split(filename) if not os.path.exists(base): retry = 3 while retry: try: os.makedirs(base) retry = 0 except Exception: # noqa: PERF203 retry -= 1 create_empty_file(filename) def norm(self, *parts): """Normalizes file path (in relation to temp directory).""" tempdir = os.fsencode(self.tempdir) if isinstance(parts[0], bytes) else self.tempdir return os.path.join(tempdir, *parts) def mksymlink(self, original, link): """Make symlink.""" if not os.path.lexists(link): os.symlink(original, link) def setUp(self): """Setup.""" self.tempdir = TESTFN + "_dir" self.mktemp('subfolder', 'a.txt') self.mktemp('subfolder', 'b.file') self.mktemp('a.txt') self.mktemp('b.file') self.mktemp('c.txt.bak') self.can_symlink = can_symlink() if self.can_symlink: self.mksymlink('subfolder', self.norm('sym1')) self.mksymlink(os.path.join('subfolder', 'a.txt'), self.norm('sym2')) self.default_flags = glob.G | glob.P | glob.B def tearDown(self): """Cleanup.""" retry = 3 while retry: try: shutil.rmtree(self.tempdir) while os.path.exists(self.tempdir): pass retry = 0 except Exception: # noqa: PERF203 retry -= 1 @staticmethod def assert_equal(a, b): """Assert equal.""" assert a == b, "Comparison between objects yielded false." @classmethod def assertSequencesEqual_noorder(cls, l1, l2): """Verify lists match (unordered).""" l1 = list(l1) l2 = list(l2) cls.assert_equal(set(l1), set(l2)) cls.assert_equal(sorted(l1), sorted(l2)) def test_rglob_globstar(self): """Test `rglob` with `GLOBSTARLONG`.""" flags = pathlib.GL expected = [pathlib.Path(self.tempdir + '/a.txt'), pathlib.Path(self.tempdir + '/subfolder/a.txt')] result = pathlib.Path(self.tempdir).rglob('a.txt', flags=flags) self.assertSequencesEqual_noorder(expected, result) def test_rglob_globstarlong_symlink(self): """Test `rglob` with `GLOBSTARLONG` and `FOLLOW`.""" flags = pathlib.GL | pathlib.L expected = [ pathlib.Path(self.tempdir + '/a.txt'), pathlib.Path(self.tempdir + '/subfolder/a.txt'), pathlib.Path(self.tempdir + '/sym1/a.txt') ] result = pathlib.Path(self.tempdir).rglob('a.txt', flags=flags) self.assertSequencesEqual_noorder(expected, result) def test_match_globstarlong_no_follow(self): """Test `match` with `GLOBSTARLONG`.""" flags = pathlib.GL self.assertTrue(pathlib.Path(self.tempdir + '/sym1/a.txt').match('a.txt', flags=flags)) def test_match_globstarlong_symlink(self): """Test `match` with `GLOBSTARLONG` and `FOLLOW`.""" flags = pathlib.GL | pathlib.L self.assertTrue(pathlib.Path(self.tempdir + '/sym1/a.txt').match('a.txt', flags=flags)) def test_match_globstarlong_no_follow_real_path(self): """Test `match` with `GLOBSTARLONG` and `REALPATH`.""" flags = pathlib.GL | pathlib.P self.assertFalse(pathlib.Path(self.tempdir + '/sym1/a.txt').match('a.txt', flags=flags)) def test_match_globstarlong_symlink_real_path(self): """Test `match` with `GLOBSTARLONG` and `FOLLOW` and `REALPATH`.""" flags = pathlib.GL | pathlib.L | pathlib.P self.assertTrue(pathlib.Path(self.tempdir + '/sym1/a.txt').match('a.txt', flags=flags)) wcmatch-10.0/tests/test_versions.py000066400000000000000000000077721467532413500175320ustar00rootroot00000000000000"""Version tests.""" from __future__ import unicode_literals import unittest from wcmatch.__meta__ import Version, parse_version class TestVersion(unittest.TestCase): """Test versions.""" def test_version_output(self): """Test that versions generate proper strings.""" assert Version(1, 0, 0, "final")._get_canonical() == "1.0" assert Version(1, 2, 0, "final")._get_canonical() == "1.2" assert Version(1, 2, 3, "final")._get_canonical() == "1.2.3" assert Version(1, 2, 0, "alpha", pre=4)._get_canonical() == "1.2a4" assert Version(1, 2, 0, "beta", pre=4)._get_canonical() == "1.2b4" assert Version(1, 2, 0, "candidate", pre=4)._get_canonical() == "1.2rc4" assert Version(1, 2, 0, "final", post=1)._get_canonical() == "1.2.post1" assert Version(1, 2, 3, ".dev-alpha", pre=1)._get_canonical() == "1.2.3a1.dev0" assert Version(1, 2, 3, ".dev")._get_canonical() == "1.2.3.dev0" assert Version(1, 2, 3, ".dev", dev=1)._get_canonical() == "1.2.3.dev1" def test_version_comparison(self): """Test that versions compare proper.""" assert Version(1, 0, 0, "final") < Version(1, 2, 0, "final") assert Version(1, 2, 0, "alpha", pre=4) < Version(1, 2, 0, "final") assert Version(1, 2, 0, "final") < Version(1, 2, 0, "final", post=1) assert Version(1, 2, 3, ".dev-beta", pre=2) < Version(1, 2, 3, "beta", pre=2) assert Version(1, 2, 3, ".dev") < Version(1, 2, 3, ".dev-beta", pre=2) assert Version(1, 2, 3, ".dev") < Version(1, 2, 3, ".dev", dev=1) def test_version_parsing(self): """Test version parsing.""" assert parse_version( Version(1, 0, 0, "final")._get_canonical() ) == Version(1, 0, 0, "final") assert parse_version( Version(1, 2, 0, "final")._get_canonical() ) == Version(1, 2, 0, "final") assert parse_version( Version(1, 2, 3, "final")._get_canonical() ) == Version(1, 2, 3, "final") assert parse_version( Version(1, 2, 0, "alpha", pre=4)._get_canonical() ) == Version(1, 2, 0, "alpha", pre=4) assert parse_version( Version(1, 2, 0, "beta", pre=4)._get_canonical() ) == Version(1, 2, 0, "beta", pre=4) assert parse_version( Version(1, 2, 0, "candidate", pre=4)._get_canonical() ) == Version(1, 2, 0, "candidate", pre=4) assert parse_version( Version(1, 2, 0, "final", post=1)._get_canonical() ) == Version(1, 2, 0, "final", post=1) assert parse_version( Version(1, 2, 3, ".dev-alpha", pre=1)._get_canonical() ) == Version(1, 2, 3, ".dev-alpha", pre=1) assert parse_version( Version(1, 2, 3, ".dev")._get_canonical() ) == Version(1, 2, 3, ".dev") assert parse_version( Version(1, 2, 3, ".dev", dev=1)._get_canonical() ) == Version(1, 2, 3, ".dev", dev=1) def test_asserts(self): """Test asserts.""" with self.assertRaises(ValueError): Version("1", "2", "3") with self.assertRaises(ValueError): Version(1, 2, 3, 1) with self.assertRaises(ValueError): Version("1", "2", "3") with self.assertRaises(ValueError): Version(1, 2, 3, "bad") with self.assertRaises(ValueError): Version(1, 2, 3, "alpha") with self.assertRaises(ValueError): Version(1, 2, 3, "alpha", pre=1, dev=1) with self.assertRaises(ValueError): Version(1, 2, 3, "alpha", pre=1, post=1) with self.assertRaises(ValueError): Version(1, 2, 3, ".dev-alpha") with self.assertRaises(ValueError): Version(1, 2, 3, ".dev-alpha", pre=1, post=1) with self.assertRaises(ValueError): Version(1, 2, 3, pre=1) with self.assertRaises(ValueError): Version(1, 2, 3, dev=1) with self.assertRaises(ValueError): parse_version('bad&version') wcmatch-10.0/tests/test_wcmatch.py000066400000000000000000000403101467532413500172710ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Tests for `wcmatch`.""" import unittest import os import wcmatch.wcmatch as wcmatch import shutil from wcmatch import _wcparse # Below is general helper stuff that Python uses in `unittests`. As these # not meant for users, and could change without notice, include them # ourselves so we aren't surprised later. TESTFN = '@test' # Disambiguate `TESTFN` for parallel testing, while letting it remain a valid # module name. TESTFN = "{}_{}_tmp".format(TESTFN, os.getpid()) def create_empty_file(filename): """Create an empty file. If the file already exists, truncate it.""" fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) os.close(fd) _can_symlink = None def can_symlink(): """Check if we can symlink.""" global _can_symlink if _can_symlink is not None: return _can_symlink symlink_path = TESTFN + "can_symlink" try: os.symlink(TESTFN, symlink_path) can = True except (OSError, NotImplementedError, AttributeError): can = False else: os.remove(symlink_path) _can_symlink = can return can def skip_unless_symlink(test): """Skip decorator for tests that require functional symlink.""" ok = can_symlink() msg = "Requires functional symlink implementation" return test if ok else unittest.skip(msg)(test) class _TestWcmatch(unittest.TestCase): """Test the `WcMatch` class.""" def mktemp(self, *parts): """Make temp directory.""" filename = self.norm(*parts) base, file = os.path.split(filename) if not os.path.exists(base): retry = 3 while retry: try: os.makedirs(base) retry = 0 except Exception: # noqa: PERF203 retry -= 1 create_empty_file(filename) def force_err(self): """Force an error.""" raise TypeError def norm(self, *parts): """Normalizes file path (in relation to temp directory).""" tempdir = os.fsencode(self.tempdir) if isinstance(parts[0], bytes) else self.tempdir return os.path.join(tempdir, *parts) def norm_list(self, files): """Normalize file list.""" return sorted([self.norm(os.path.normpath(x)) for x in files]) def setUp(self): """Setup.""" self.tempdir = TESTFN + "_dir" self.default_flags = wcmatch.R | wcmatch.I | wcmatch.M | wcmatch.SL self.errors = [] self.skipped = 0 self.skip_records = [] self.error_records = [] self.files = [] def tearDown(self): """Cleanup.""" retry = 3 while retry: try: shutil.rmtree(self.tempdir) retry = 0 except Exception: # noqa: PERF203 retry -= 1 def crawl_files(self, walker): """Crawl the files.""" for f in walker.match(): if f == '': self.skip_records.append(f) elif f == '': self.error_records.append(f) else: self.files.append(f) self.skipped = walker.get_skipped() class TestWcmatch(_TestWcmatch): """Test the `WcMatch` class.""" def setUp(self): """Setup.""" self.tempdir = TESTFN + "_dir" self.mktemp('.hidden', 'a.txt') self.mktemp('.hidden', 'b.file') self.mktemp('.hidden_file') self.mktemp('a.txt') self.mktemp('b.file') self.mktemp('c.txt.bak') self.default_flags = wcmatch.R | wcmatch.I | wcmatch.M | wcmatch.SL self.errors = [] self.skipped = 0 self.skip_records = [] self.error_records = [] self.files = [] def test_full_path_exclude(self): """Test full path exclude.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', exclude_pattern='**/.hidden', flags=self.default_flags | wcmatch.DIRPATHNAME | wcmatch.GLOBSTAR | wcmatch.RECURSIVE | wcmatch.HIDDEN ) self.crawl_files(walker) self.assertEqual(sorted(self.files), self.norm_list(['a.txt'])) def test_full_file(self): """Test full file.""" walker = wcmatch.WcMatch( self.tempdir, '**/*.txt|-**/.hidden/*', flags=self.default_flags | wcmatch.FILEPATHNAME | wcmatch.GLOBSTAR | wcmatch.RECURSIVE | wcmatch.HIDDEN ) self.crawl_files(walker) self.assertEqual(sorted(self.files), self.norm_list(['a.txt'])) def test_non_recursive(self): """Test non-recursive search.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags ) self.crawl_files(walker) self.assertEqual(self.skipped, 3) self.assertEqual(sorted(self.files), self.norm_list(['a.txt'])) def test_non_recursive_inverse(self): """Test non-recursive inverse search.""" walker = wcmatch.WcMatch( self.tempdir, '*.*|-*.file', flags=self.default_flags ) self.crawl_files(walker) self.assertEqual(self.skipped, 2) self.assertEqual(sorted(self.files), self.norm_list(['a.txt', 'c.txt.bak'])) def test_recursive(self): """Test non-recursive search.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags | wcmatch.RECURSIVE ) self.crawl_files(walker) self.assertEqual(self.skipped, 3) self.assertEqual(sorted(self.files), self.norm_list(['a.txt'])) def test_recursive_bytes(self): """Test non-recursive search.""" walker = wcmatch.WcMatch( os.fsencode(self.tempdir), b'*.txt', flags=self.default_flags | wcmatch.RECURSIVE ) self.crawl_files(walker) self.assertEqual(self.skipped, 3) self.assertEqual(sorted(self.files), self.norm_list([b'a.txt'])) def test_recursive_hidden(self): """Test non-recursive search.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) self.crawl_files(walker) self.assertEqual(self.skipped, 4) self.assertEqual(sorted(self.files), self.norm_list(['.hidden/a.txt', 'a.txt'])) def test_recursive_hidden_bytes(self): """Test non-recursive search with byte strings.""" walker = wcmatch.WcMatch( os.fsencode(self.tempdir), b'*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) self.crawl_files(walker) self.assertEqual(self.skipped, 4) self.assertEqual(sorted(self.files), self.norm_list([b'.hidden/a.txt', b'a.txt'])) def test_recursive_hidden_folder_exclude(self): """Test non-recursive search.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', exclude_pattern='.hidden', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) self.crawl_files(walker) self.assertEqual(self.skipped, 3) self.assertEqual(sorted(self.files), self.norm_list(['a.txt'])) def test_recursive_hidden_folder_exclude_inverse(self): """Test non-recursive search with inverse.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', exclude_pattern='*|-.hidden', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) self.crawl_files(walker) self.assertEqual(self.skipped, 4) self.assertEqual(sorted(self.files), self.norm_list(['.hidden/a.txt', 'a.txt'])) def test_abort(self): """Test aborting.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) records = 0 for _f in walker.imatch(): records += 1 walker.kill() self.assertEqual(records, 1) # Reset our test tracker along with the walker object self.errors = [] self.skipped = 0 self.files = [] records = 0 walker.reset() self.crawl_files(walker) self.assertEqual(sorted(self.files), self.norm_list(['.hidden/a.txt', 'a.txt'])) def test_abort_early(self): """Test aborting early.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt*', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) walker.kill() records = 0 for _f in walker.imatch(): records += 1 self.assertTrue(records == 0 or walker.get_skipped() == 0) def test_empty_string_dir(self): """Test when directory is an empty string.""" target = '.' + os.sep walker = wcmatch.WcMatch( '', '*.txt*', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) self.assertEqual(walker._root_dir, target) walker = wcmatch.WcMatch( b'', b'*.txt*', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) self.assertEqual(walker._root_dir, os.fsencode(target)) def test_empty_string_file(self): """Test when file pattern is an empty string.""" walker = wcmatch.WcMatch( self.tempdir, '', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) self.crawl_files(walker) self.assertEqual( sorted(self.files), self.norm_list( ['a.txt', '.hidden/a.txt', '.hidden/b.file', 'b.file', '.hidden_file', 'c.txt.bak'] ) ) def test_skip_override(self): """Test `on_skip` override.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) walker.on_skip = lambda base, name: '' self.crawl_files(walker) self.assertEqual(len(self.skip_records), 4) def test_errors(self): """Test errors.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) walker.on_validate_file = lambda base, name: self.force_err() self.crawl_files(walker) self.assertEqual(sorted(self.files), self.norm_list([])) self.errors = [] self.skipped = 0 self.files = [] walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) walker.on_validate_directory = lambda base, name: self.force_err() self.crawl_files(walker) self.assertEqual(sorted(self.files), self.norm_list(['a.txt'])) def test_error_override(self): """Test `on_eror` override.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) walker.on_validate_file = lambda base, name: self.force_err() walker.on_error = lambda base, name: '' self.crawl_files(walker) self.assertEqual(len(self.error_records), 2) def test_match_base_filepath(self): """Test `MATCHBASE` with filepath.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN | wcmatch.FILEPATHNAME | wcmatch.MATCHBASE ) self.crawl_files(walker) self.assertEqual( sorted(self.files), self.norm_list( ['a.txt', '.hidden/a.txt'] ) ) def test_match_base_absolute_filepath(self): """Test `MATCHBASE` with filepath and an absolute path.""" walker = wcmatch.WcMatch( self.tempdir, '.hidden/*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN | wcmatch.FILEPATHNAME | wcmatch.MATCHBASE ) self.crawl_files(walker) self.assertEqual( sorted(self.files), self.norm_list( ['.hidden/a.txt'] ) ) def test_match_base_anchored_filepath(self): """Test `MATCHBASE` with filepath and an anchored pattern.""" walker = wcmatch.WcMatch( self.tempdir, '/*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN | wcmatch.FILEPATHNAME | wcmatch.MATCHBASE ) self.crawl_files(walker) self.assertEqual( sorted(self.files), self.norm_list( ['a.txt'] ) ) def test_match_insensitive(self): """Test case insensitive.""" walker = wcmatch.WcMatch( self.tempdir, 'A.TXT', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.FILEPATHNAME | wcmatch.IGNORECASE ) self.crawl_files(walker) self.assertEqual( sorted(self.files), self.norm_list( ['a.txt'] ) ) def test_nomatch_sensitive(self): """Test case sensitive does not match.""" walker = wcmatch.WcMatch( self.tempdir, 'A.TXT', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.FILEPATHNAME | wcmatch.CASE ) self.crawl_files(walker) self.assertEqual( sorted(self.files), self.norm_list( [] ) ) def test_match_sensitive(self): """Test case sensitive.""" walker = wcmatch.WcMatch( self.tempdir, 'a.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.FILEPATHNAME | wcmatch.CASE ) self.crawl_files(walker) self.assertEqual( sorted(self.files), self.norm_list( ['a.txt'] ) ) @skip_unless_symlink class TestWcmatchSymlink(_TestWcmatch): """Test symlinks.""" def mksymlink(self, original, link): """Make symlink.""" if not os.path.lexists(link): os.symlink(original, link) def setUp(self): """Setup.""" self.tempdir = TESTFN + "_dir" self.mktemp('.hidden', 'a.txt') self.mktemp('.hidden', 'b.file') self.mktemp('.hidden_file') self.mktemp('a.txt') self.mktemp('b.file') self.mktemp('c.txt.bak') self.can_symlink = can_symlink() if self.can_symlink: self.mksymlink('.hidden', self.norm('sym1')) self.mksymlink(os.path.join('.hidden', 'a.txt'), self.norm('sym2')) self.default_flags = wcmatch.R | wcmatch.I | wcmatch.M self.errors = [] self.skipped = 0 self.skip_records = [] self.error_records = [] self.files = [] def test_symlinks(self): """Test symlinks.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN | wcmatch.SYMLINKS ) self.crawl_files(walker) self.assertEqual( sorted(self.files), self.norm_list( ['a.txt', '.hidden/a.txt', 'sym1/a.txt'] ) ) def test_avoid_symlinks(self): """Test avoiding symlinks.""" walker = wcmatch.WcMatch( self.tempdir, '*.txt', flags=self.default_flags | wcmatch.RECURSIVE | wcmatch.HIDDEN ) self.crawl_files(walker) self.assertEqual( sorted(self.files), self.norm_list( ['a.txt', '.hidden/a.txt'] ) ) class TestExpansionLimit(unittest.TestCase): """Test expansion limits.""" def test_limit_wcmatch(self): """Test expansion limit of `globmatch`.""" with self.assertRaises(_wcparse.PatternLimitException): wcmatch.WcMatch('.', '{1..11}', flags=wcmatch.BRACE, limit=10) wcmatch-10.0/tests/test_wcparse.py000066400000000000000000000150741467532413500173200ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Tests for `wcparse`.""" import unittest import re import copy import wcmatch._wcparse as _wcparse class TestWcparse(unittest.TestCase): """Test `wcparse`.""" def test_hash(self): """Test hashing of search.""" p1 = re.compile('test') p2 = re.compile('test') p3 = re.compile('test', re.X) p4 = re.compile(b'test') w1 = _wcparse.WcRegexp((p1,)) w2 = _wcparse.WcRegexp((p2,)) w3 = _wcparse.WcRegexp((p3,)) w4 = _wcparse.WcRegexp((p4,)) w5 = _wcparse.WcRegexp((p1,), (p3,)) self.assertTrue(w1 == w2) self.assertTrue(w1 != w3) self.assertTrue(w1 != w4) self.assertTrue(w1 != w5) w6 = copy.copy(w1) self.assertTrue(w1 == w6) self.assertTrue(w6 in {w1}) def test_preprocessor_sequence(self): """Test the integrity of the order of preprocessors.""" results = _wcparse.expand( 'test@(this{|that,|other})|*.py', _wcparse.BRACE | _wcparse.SPLIT | _wcparse.EXTMATCH, 0 ) self.assertEqual(sorted(results), sorted(['test@(this|that)', 'test@(this|other)', '*.py', '*.py'])) def test_compile_expansion_okay(self): """Test expansion is okay.""" self.assertEqual(len(_wcparse.compile(['{1..10}'], _wcparse.BRACE)), 10) def test_compile_unique_optimization_okay(self): """Test that redundant patterns are reduced in compile.""" self.assertEqual(len(_wcparse.compile(['|'.join(['a'] * 10)], _wcparse.SPLIT, 10)), 1) def test_translate_expansion_okay(self): """Test expansion is okay.""" p1, p2 = _wcparse.translate(['{1..10}'], _wcparse.BRACE, 10) count = len(p1) + len(p2) self.assertEqual(count, 10) def test_translate_unique_optimization_okay(self): """Test that redundant patterns are reduced in translate.""" p1, p2 = _wcparse.translate(['|'.join(['a'] * 10)], _wcparse.SPLIT, 10) count = len(p1) + len(p2) self.assertEqual(count, 1) def test_expansion_limt(self): """Test expansion limit.""" with self.assertRaises(_wcparse.PatternLimitException): _wcparse.compile(['{1..11}'], _wcparse.BRACE, 10) with self.assertRaises(_wcparse.PatternLimitException): _wcparse.compile(['|'.join(['a'] * 11)], _wcparse.SPLIT, 10) with self.assertRaises(_wcparse.PatternLimitException): _wcparse.compile( ['{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5))], _wcparse.SPLIT | _wcparse.BRACE, 10 ) with self.assertRaises(_wcparse.PatternLimitException): _wcparse.compile( ['|'.join(['a'] * 10), '|'.join(['a'] * 5)], _wcparse.SPLIT | _wcparse.BRACE, 10 ) def test_expansion_limt_translation(self): """Test expansion limit.""" with self.assertRaises(_wcparse.PatternLimitException): _wcparse.translate(['{1..11}'], _wcparse.BRACE, 10) with self.assertRaises(_wcparse.PatternLimitException): _wcparse.translate(['|'.join(['a'] * 11)], _wcparse.SPLIT, 10) with self.assertRaises(_wcparse.PatternLimitException): _wcparse.translate( ['{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5))], _wcparse.SPLIT | _wcparse.BRACE, 10 ) with self.assertRaises(_wcparse.PatternLimitException): _wcparse.translate( ['|'.join(['a'] * 10), '|'.join(['a'] * 5)], _wcparse.SPLIT | _wcparse.BRACE, 10 ) def test_expansion_no_limit_compile(self): """Test no expansion limit compile.""" self.assertEqual(len(_wcparse.compile(['{1..11}'], _wcparse.BRACE, -1)), 11) def test_expansion_no_limit_translate(self): """Test no expansion limit translate.""" p1, p2 = _wcparse.translate(['{1..11}'], _wcparse.BRACE, 0) count = len(p1) + len(p2) self.assertEqual(count, 11) def test_tilde_pos_none(self): """Test that tilde position gives -1 when no tilde found.""" self.assertEqual(_wcparse.tilde_pos('pattern', _wcparse.GLOBTILDE | _wcparse.REALPATH), -1) def test_tilde_pos_normal(self): """Test that tilde position gives 0 when tilde found.""" self.assertEqual(_wcparse.tilde_pos('~pattern', _wcparse.GLOBTILDE | _wcparse.REALPATH), 0) def test_tilde_pos_negative_normal(self): """Test that tilde position gives 0 when tilde is found and `NEGATE` is enabled.""" self.assertEqual(_wcparse.tilde_pos('~pattern', _wcparse.GLOBTILDE | _wcparse.REALPATH | _wcparse.NEGATE), 0) def test_tilde_pos_negative_negate(self): """Test that tilde position gives 1 when tilde is found and `NEGATE` is enabled.""" self.assertEqual(_wcparse.tilde_pos('!~pattern', _wcparse.GLOBTILDE | _wcparse.REALPATH | _wcparse.NEGATE), 1) def test_unc_pattern(self): """Test UNC pattern.""" self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//?/UNC/server/mount').group(0), '//?/UNC/server/mount') self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//?/UNC/server/mount/').group(0), '//?/UNC/server/mount/') self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//?/UNC/server/mount/path').group(0), '//?/UNC/server/mount/') self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//./UNC/server/mount/path').group(0), '//./UNC/server/mount/') self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//?/UNC/c:').group(0), '//?/UNC/') self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//?/UNC/c:/').group(0), '//?/UNC/') self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//?/c:').group(0), '//?/c:') self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//?/c:/').group(0), '//?/c:/') self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//?/what').group(0), '//?/what') self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//server/mount').group(0), '//server/mount') self.assertEqual(_wcparse.RE_WIN_DRIVE[0].match('//server/mount/').group(0), '//server/mount/') self.assertIsNone(_wcparse.RE_WIN_DRIVE[0].match('//server')) self.assertEqual( _wcparse.RE_WIN_DRIVE[0].match('//?/GLOBAL/UNC/server/mount/temp').group(0), '//?/GLOBAL/UNC/server/mount/' ) def test_bad_root_dir(self): """Test bad root directory.""" with self.assertRaises(TypeError): _wcparse.compile(['string'], _wcparse.PATHNAME | _wcparse.REALPATH, 0).match('string', b'rdir', None) wcmatch-10.0/wcmatch/000077500000000000000000000000001467532413500145205ustar00rootroot00000000000000wcmatch-10.0/wcmatch/__init__.py000066400000000000000000000002151467532413500166270ustar00rootroot00000000000000""" Wild Card Match. A custom implementation of `fnmatch` and `glob`. """ from .__meta__ import __version_info__, __version__ # noqa: F401 wcmatch-10.0/wcmatch/__meta__.py000066400000000000000000000151571467532413500166250ustar00rootroot00000000000000"""Meta related things.""" from __future__ import annotations from collections import namedtuple import re RE_VER = re.compile( r'''(?x) (?P\d+)(?:\.(?P\d+))?(?:\.(?P\d+))? (?:(?Pa|b|rc)(?P
\d+))?
    (?:\.post(?P\d+))?
    (?:\.dev(?P\d+))?
    '''
)

REL_MAP = {
    ".dev": "",
    ".dev-alpha": "a",
    ".dev-beta": "b",
    ".dev-candidate": "rc",
    "alpha": "a",
    "beta": "b",
    "candidate": "rc",
    "final": ""
}

DEV_STATUS = {
    ".dev": "2 - Pre-Alpha",
    ".dev-alpha": "2 - Pre-Alpha",
    ".dev-beta": "2 - Pre-Alpha",
    ".dev-candidate": "2 - Pre-Alpha",
    "alpha": "3 - Alpha",
    "beta": "4 - Beta",
    "candidate": "4 - Beta",
    "final": "5 - Production/Stable"
}

PRE_REL_MAP = {"a": 'alpha', "b": 'beta', "rc": 'candidate'}


class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre", "post", "dev"])):
    """
    Get the version (PEP 440).

    A biased approach to the PEP 440 semantic version.

    Provides a tuple structure which is sorted for comparisons `v1 > v2` etc.
      (major, minor, micro, release type, pre-release build, post-release build, development release build)
    Release types are named in is such a way they are comparable with ease.
    Accessors to check if a development, pre-release, or post-release build. Also provides accessor to get
    development status for setup files.

    How it works (currently):

    - You must specify a release type as either `final`, `alpha`, `beta`, or `candidate`.
    - To define a development release, you can use either `.dev`, `.dev-alpha`, `.dev-beta`, or `.dev-candidate`.
      The dot is used to ensure all development specifiers are sorted before `alpha`.
      You can specify a `dev` number for development builds, but do not have to as implicit development releases
      are allowed.
    - You must specify a `pre` value greater than zero if using a prerelease as this project (not PEP 440) does not
      allow implicit prereleases.
    - You can optionally set `post` to a value greater than zero to make the build a post release. While post releases
      are technically allowed in prereleases, it is strongly discouraged, so we are rejecting them. It should be
      noted that we do not allow `post0` even though PEP 440 does not restrict this. This project specifically
      does not allow implicit post releases.
    - It should be noted that we do not support epochs `1!` or local versions `+some-custom.version-1`.

    Acceptable version releases:

    ```
    Version(1, 0, 0, "final")                    1.0
    Version(1, 2, 0, "final")                    1.2
    Version(1, 2, 3, "final")                    1.2.3
    Version(1, 2, 0, ".dev-alpha", pre=4)        1.2a4
    Version(1, 2, 0, ".dev-beta", pre=4)         1.2b4
    Version(1, 2, 0, ".dev-candidate", pre=4)    1.2rc4
    Version(1, 2, 0, "final", post=1)            1.2.post1
    Version(1, 2, 3, ".dev")                     1.2.3.dev0
    Version(1, 2, 3, ".dev", dev=1)              1.2.3.dev1
    ```

    """

    def __new__(
        cls,
        major: int, minor: int, micro: int, release: str = "final",
        pre: int = 0, post: int = 0, dev: int = 0
    ) -> Version:
        """Validate version info."""

        # Ensure all parts are positive integers.
        for value in (major, minor, micro, pre, post):
            if not (isinstance(value, int) and value >= 0):
                raise ValueError("All version parts except 'release' should be integers.")

        if release not in REL_MAP:
            raise ValueError(f"'{release}' is not a valid release type.")

        # Ensure valid pre-release (we do not allow implicit pre-releases).
        if ".dev-candidate" < release < "final":
            if pre == 0:
                raise ValueError("Implicit pre-releases not allowed.")
            elif dev:
                raise ValueError("Version is not a development release.")
            elif post:
                raise ValueError("Post-releases are not allowed with pre-releases.")

        # Ensure valid development or development/pre release
        elif release < "alpha":
            if release > ".dev" and pre == 0:
                raise ValueError("Implicit pre-release not allowed.")
            elif post:
                raise ValueError("Post-releases are not allowed with pre-releases.")

        # Ensure a valid normal release
        else:
            if pre:
                raise ValueError("Version is not a pre-release.")
            elif dev:
                raise ValueError("Version is not a development release.")

        return super().__new__(cls, major, minor, micro, release, pre, post, dev)

    def _is_pre(self) -> bool:
        """Is prerelease."""

        return bool(self.pre > 0)

    def _is_dev(self) -> bool:
        """Is development."""

        return bool(self.release < "alpha")

    def _is_post(self) -> bool:
        """Is post."""

        return bool(self.post > 0)

    def _get_dev_status(self) -> str:  # pragma: no cover
        """Get development status string."""

        return DEV_STATUS[self.release]

    def _get_canonical(self) -> str:
        """Get the canonical output string."""

        # Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed..
        if self.micro == 0:
            ver = f"{self.major}.{self.minor}"
        else:
            ver = f"{self.major}.{self.minor}.{self.micro}"
        if self._is_pre():
            ver += f'{REL_MAP[self.release]}{self.pre}'
        if self._is_post():
            ver += f".post{self.post}"
        if self._is_dev():
            ver += f".dev{self.dev}"

        return ver


def parse_version(ver: str) -> Version:
    """Parse version into a comparable Version tuple."""

    m = RE_VER.match(ver)

    if m is None:
        raise ValueError(f"'{ver}' is not a valid version")

    # Handle major, minor, micro
    major = int(m.group('major'))
    minor = int(m.group('minor')) if m.group('minor') else 0
    micro = int(m.group('micro')) if m.group('micro') else 0

    # Handle pre releases
    if m.group('type'):
        release = PRE_REL_MAP[m.group('type')]
        pre = int(m.group('pre'))
    else:
        release = "final"
        pre = 0

    # Handle development releases
    dev = m.group('dev') if m.group('dev') else 0
    if m.group('dev'):
        dev = int(m.group('dev'))
        release = '.dev-' + release if pre else '.dev'
    else:
        dev = 0

    # Handle post
    post = int(m.group('post')) if m.group('post') else 0

    return Version(major, minor, micro, release, pre, post, dev)


__version_info__ = Version(10, 0, 0, "final")
__version__ = __version_info__._get_canonical()
wcmatch-10.0/wcmatch/_wcmatch.py000066400000000000000000000263071467532413500166670ustar00rootroot00000000000000"""Handle path matching."""
from __future__ import annotations
import re
import os
import stat
import copyreg
from . import util
from typing import Pattern, AnyStr, Generic, Any

# `O_DIRECTORY` may not always be defined
DIR_FLAGS = os.O_RDONLY | getattr(os, 'O_DIRECTORY', 0)
# Right half can return an empty set if not supported
SUPPORT_DIR_FD = {os.open, os.stat} <= os.supports_dir_fd and os.scandir in os.supports_fd

RE_WIN_MOUNT = (
    re.compile(r'\\|/|[a-z]:(?:\\|/|$)', re.I),
    re.compile(br'\\|/|[a-z]:(?:\\|/|$)', re.I)
)
RE_MOUNT = (
    re.compile(r'/'),
    re.compile(br'/')
)
RE_WIN_SPLIT = (
    re.compile(r'\\|/'),
    re.compile(br'\\|/')
)
RE_SPLIT = (
    re.compile(r'/'),
    re.compile(br'/')
)
RE_WIN_STRIP = (
    r'\\/',
    br'\\/'
)
RE_STRIP = (
    r'/',
    br'/'
)


class _Match(Generic[AnyStr]):
    """Match the given pattern."""

    def __init__(
        self,
        filename: AnyStr,
        include: tuple[Pattern[AnyStr], ...],
        exclude: tuple[Pattern[AnyStr], ...] | None,
        real: bool,
        path: bool,
        follow: bool
    ) -> None:
        """Initialize."""

        self.filename = filename  # type: AnyStr
        self.include = include  # type: tuple[Pattern[AnyStr], ...]
        self.exclude = exclude  # type: tuple[Pattern[AnyStr], ...] | None
        self.real = real
        self.path = path
        self.follow = follow
        self.ptype = util.BYTES if isinstance(self.filename, bytes) else util.UNICODE

    def _fs_match(
        self,
        pattern: Pattern[AnyStr],
        filename: AnyStr,
        is_win: bool,
        follow: bool,
        symlinks: dict[tuple[int | None, AnyStr], bool],
        root: AnyStr,
        dir_fd: int | None
    ) -> bool:
        """
        Match path against the pattern.

        Since `globstar` doesn't match symlinks (unless `FOLLOW` is enabled), we must look for symlinks.
        If we identify a symlink in a `globstar` match, we know this result should not actually match.

        We only check for the symlink if we know we are looking at a directory.
        And we only call `lstat` if we can't find it in the cache.

        We know we need to check the directory if:

        1. If the match has not reached the end of the path and directory is in `globstar` match.
        2. Or the match is at the end of the path and the directory is not the last part of `globstar` match.

        """

        matched = False
        split = (RE_WIN_SPLIT if is_win else RE_SPLIT)[self.ptype]  # type: Any
        strip = (RE_WIN_STRIP if is_win else RE_STRIP)[self.ptype]  # type: Any

        end = len(filename) - 1
        base = None
        m = pattern.fullmatch(filename)
        if m:
            matched = True
            # Lets look at the captured `globstar` groups and see if that part of the path
            # contains symlinks.
            if not follow:
                try:
                    for i, star in enumerate(m.groups(), 1):
                        if star:
                            at_end = m.end(i) == end
                            parts = split.split(star.strip(strip))
                            if base is None:
                                base = os.path.join(root, filename[:m.start(i)])
                            last_part = len(parts)
                            for j, part in enumerate(parts, 1):
                                base = os.path.join(base, part)
                                key = (dir_fd, base)
                                if not at_end or (at_end and j != last_part):
                                    is_link = symlinks.get(key, None)
                                    if is_link is None:
                                        if dir_fd is None:
                                            is_link = os.path.islink(base)
                                            symlinks[key] = is_link
                                        else:
                                            try:
                                                st = os.lstat(base, dir_fd=dir_fd)
                                            except (OSError, ValueError):  # pragma: no cover
                                                is_link = False
                                            else:
                                                is_link = stat.S_ISLNK(st.st_mode)
                                            symlinks[key] = is_link
                                    matched = not is_link
                                    if not matched:
                                        break
                        if not matched:
                            break
                except OSError:  # pragma: no cover
                    matched = False
        return matched

    def _match_real(
        self,
        symlinks: dict[tuple[int | None, AnyStr], bool],
        root: AnyStr,
        dir_fd: int | None
    ) -> bool:
        """Match real filename includes and excludes."""

        is_win = util.platform() == "windows"

        if isinstance(self.filename, bytes):
            sep = b'/'
            is_dir = (RE_WIN_SPLIT if is_win else RE_SPLIT)[1].match(self.filename[-1:]) is not None
        else:
            sep = '/'
            is_dir = (RE_WIN_SPLIT if is_win else RE_SPLIT)[0].match(self.filename[-1:]) is not None

        try:
            if dir_fd is None:
                is_file_dir = os.path.isdir(os.path.join(root, self.filename))
            else:
                try:
                    st = os.stat(os.path.join(root, self.filename), dir_fd=dir_fd)
                except (OSError, ValueError):  # pragma: no cover
                    is_file_dir = False
                else:
                    is_file_dir = stat.S_ISDIR(st.st_mode)
        except OSError:  # pragma: no cover
            return False

        if not is_dir and is_file_dir:
            is_dir = True
            filename = self.filename + sep
        else:
            filename = self.filename

        matched = False
        for pattern in self.include:
            if self._fs_match(pattern, filename, is_win, self.follow, symlinks, root, dir_fd):
                matched = True
                break

        if matched:
            if self.exclude:
                for pattern in self.exclude:
                    if self._fs_match(pattern, filename, is_win, True, symlinks, root, dir_fd):
                        matched = False
                        break

        return matched

    def match(self, root_dir: AnyStr | None = None, dir_fd: int | None = None) -> bool:
        """Match."""

        if self.real:
            if isinstance(self.filename, bytes):
                root = root_dir if root_dir is not None else b'.'  # type: AnyStr
            else:
                root = root_dir if root_dir is not None else '.'

            if dir_fd is not None and not SUPPORT_DIR_FD:
                dir_fd = None

            if not isinstance(self.filename, type(root)):
                raise TypeError(
                    "The filename and root directory should be of the same type, not {} and {}".format(
                        type(self.filename), type(root_dir)
                    )
                )

            if self.include and not isinstance(self.include[0].pattern, type(self.filename)):
                raise TypeError(
                    "The filename and pattern should be of the same type, not {} and {}".format(
                        type(self.filename), type(self.include[0].pattern)
                    )
                )

            re_mount = (RE_WIN_MOUNT if util.platform() == "windows" else RE_MOUNT)[self.ptype]  # type: Pattern[AnyStr]  # type: ignore[assignment]
            is_abs = re_mount.match(self.filename) is not None

            if is_abs:
                exists = os.path.lexists(self.filename)
            elif dir_fd is None:
                exists = os.path.lexists(os.path.join(root, self.filename))
            else:
                try:
                    os.lstat(os.path.join(root, self.filename), dir_fd=dir_fd)
                except (OSError, ValueError):  # pragma: no cover
                    exists = False
                else:
                    exists = True

            if exists:
                symlinks = {}  # type: dict[tuple[int | None, AnyStr], bool]
                return self._match_real(symlinks, root, dir_fd)
            else:
                return False

        matched = False
        for pattern in self.include:
            if pattern.fullmatch(self.filename):
                matched = True
                break

        if matched:
            matched = True
            if self.exclude:
                for pattern in self.exclude:
                    if pattern.fullmatch(self.filename):
                        matched = False
                        break
        return matched


class WcRegexp(util.Immutable, Generic[AnyStr]):
    """File name match object."""

    _include: tuple[Pattern[AnyStr], ...]
    _exclude: tuple[Pattern[AnyStr], ...] | None
    _real: bool
    _path: bool
    _follow: bool
    _hash: int

    __slots__ = ("_include", "_exclude", "_real", "_path", "_follow", "_hash")

    def __init__(
        self,
        include: tuple[Pattern[AnyStr], ...],
        exclude: tuple[Pattern[AnyStr], ...] | None = None,
        real: bool = False,
        path: bool = False,
        follow: bool = False
    ):
        """Initialization."""

        super().__init__(
            _include=include,
            _exclude=exclude,
            _real=real,
            _path=path,
            _follow=follow,
            _hash=hash(
                (
                    type(self),
                    type(include), include,
                    type(exclude), exclude,
                    type(real), real,
                    type(path), path,
                    type(follow), follow
                )
            )
        )

    def __hash__(self) -> int:
        """Hash."""

        return self._hash

    def __len__(self) -> int:
        """Length."""

        return len(self._include) + (len(self._exclude) if self._exclude is not None else 0)

    def __eq__(self, other: Any) -> bool:
        """Equal."""

        return (
            isinstance(other, WcRegexp) and
            self._include == other._include and
            self._exclude == other._exclude and
            self._real == other._real and
            self._path == other._path and
            self._follow == other._follow
        )

    def __ne__(self, other: Any) -> bool:
        """Equal."""

        return (
            not isinstance(other, WcRegexp) or
            self._include != other._include or
            self._exclude != other._exclude or
            self._real != other._real or
            self._path != other._path or
            self._follow != other._follow
        )

    def match(self, filename: AnyStr, root_dir: AnyStr | None = None, dir_fd: int | None = None) -> bool:
        """Match filename."""

        return _Match(
            filename,
            self._include,
            self._exclude,
            self._real,
            self._path,
            self._follow
        ).match(
            root_dir=root_dir,
            dir_fd=dir_fd
        )


def _pickle(p):  # type: ignore[no-untyped-def]
    return WcRegexp, (p._include, p._exclude, p._real, p._path, p._follow)


copyreg.pickle(WcRegexp, _pickle)
wcmatch-10.0/wcmatch/_wcparse.py000066400000000000000000001551411467532413500167040ustar00rootroot00000000000000"""Wildcard parsing."""
from __future__ import annotations
import re
import functools
import bracex
import os
from . import util
from . import posix
from . _wcmatch import WcRegexp
from typing import AnyStr, Iterable, Pattern, Generic, Sequence, overload

UNICODE_RANGE = '\u0000-\U0010ffff'
ASCII_RANGE = '\x00-\xff'

PATTERN_LIMIT = 1000

RE_WIN_DRIVE_START = re.compile(r'((?:\\\\|/){2}((?:\\[^\\/]|[^\\/])+)|([\\]?[a-z][\\]?:))((?:\\\\|/)|$)', re.I)
RE_WIN_DRIVE_LETTER = re.compile(r'([a-z]:)((?:\\|/)|$)', re.I)
RE_WIN_DRIVE_PART = re.compile(r'((?:\\[^\\/]|[^\\/])+)((?:\\\\|/)|$)', re.I)
RE_WIN_DRIVE_UNESCAPE = re.compile(r'\\(.)', re.I)

RE_WIN_DRIVE = (
    re.compile(
        r'''(?x)
        (
            (?:\\\\|/){2}[?.](?:\\\\|/)(?:
                [a-z]:|
                unc(?:(?:\\\\|/)[^\\/]+){2} |
                (?:global(?:\\\\|/))+(?:[a-z]:|unc(?:(?:\\\\|/)[^\\/]+){2}|[^\\/]+)
            ) |
            (?:\\\\|/){2}[^\\/]+(?:\\\\|/)[^\\/]+|
            [a-z]:
        )((?:\\\\|/){1}|$)
        ''',
        re.I
    ),
    re.compile(
        br'''(?x)
        (
            (?:\\\\|/){2}[?.](?:\\\\|/)(?:
                [a-z]:|
                unc(?:(?:\\\\|/)[^\\/]+){2} |
                (?:global(?:\\\\|/))+(?:[a-z]:|unc(?:(?:\\\\|/)[^\\/]+){2}|[^\\/]+)
            ) |
            (?:\\\\|/){2}[^\\/]+(?:\\\\|/)[^\\/]+|
            [a-z]:
        )((?:\\\\|/){1}|$)
        ''',
        re.I
    )
)

RE_MAGIC_ESCAPE = (
    re.compile(r'([-!~*?()\[\]|{}]|(? Iterable[str]:
    ...


@overload
def iter_patterns(patterns: bytes | Sequence[bytes]) -> Iterable[bytes]:
    ...


def iter_patterns(patterns: AnyStr | Sequence[AnyStr]) -> Iterable[AnyStr]:
    """Return a simple string sequence."""

    if isinstance(patterns, (str, bytes)):
        yield patterns
    else:
        yield from patterns


def escape(pattern: AnyStr, unix: bool | None = None, pathname: bool = True) -> AnyStr:
    """
    Escape.

    `unix`: use Unix style path logic.
    `pathname`: Use path logic.
    """

    if isinstance(pattern, bytes):
        drive_pat = RE_WIN_DRIVE[util.BYTES]  # type: Pattern[AnyStr]  # type: ignore[assignment]
        magic = RE_MAGIC_ESCAPE[util.BYTES]  # type: Pattern[AnyStr]  # type: ignore[assignment]
        drive_magic = RE_WIN_DRIVE_MAGIC[util.BYTES]  # type: Pattern[AnyStr]  # type: ignore[assignment]
        replace = br'\\\1'
        slash = b'\\'
        double_slash = b'\\\\'
        drive = b''
    else:
        drive_pat = RE_WIN_DRIVE[util.UNICODE]  # type: ignore[assignment]
        magic = RE_MAGIC_ESCAPE[util.UNICODE]  # type: ignore[assignment]
        drive_magic = RE_WIN_DRIVE_MAGIC[util.UNICODE]  # type: ignore[assignment]
        replace = r'\\\1'
        slash = '\\'
        double_slash = '\\\\'
        drive = ''

    pattern = pattern.replace(slash, double_slash)

    # Handle windows drives special.
    # Windows drives are handled special internally.
    # So we shouldn't escape them as we'll just have to
    # detect and undo it later.
    length = 0
    if pathname and ((unix is None and util.platform() == "windows") or unix is False):
        m = drive_pat.match(pattern)
        if m:
            # Replace splitting magic chars
            drive = m.group(0)
            length = len(drive)
            drive = drive_magic.sub(replace, m.group(0))
    pattern = pattern[length:]

    return drive + magic.sub(replace, pattern)


def _get_win_drive(
    pattern: str,
    regex: bool = False,
    case_sensitive: bool = False
) -> tuple[bool, str | None, bool, int]:
    """Get Windows drive."""

    drive = None
    slash = False
    end = 0
    root_specified = False
    m = RE_WIN_DRIVE_START.match(pattern)
    if m:
        end = m.end(0)
        if m.group(3) and RE_WIN_DRIVE_LETTER.match(m.group(0)):
            if regex:
                drive = escape_drive(RE_WIN_DRIVE_UNESCAPE.sub(r'\1', m.group(3)).replace('/', '\\'), case_sensitive)
            else:
                drive = RE_WIN_DRIVE_UNESCAPE.sub(r'\1', m.group(0)).replace('/', '\\')
            slash = bool(m.group(4))
            root_specified = True
        elif m.group(2):
            root_specified = True
            part = [RE_WIN_DRIVE_UNESCAPE.sub(r'\1', m.group(2))]
            is_special = part[-1].lower() in ('.', '?')
            complete = 1
            first = 1
            count = 0
            for count, m2 in enumerate(RE_WIN_DRIVE_PART.finditer(pattern, m.end(0)), 1):
                end = m2.end(0)
                part.append(RE_WIN_DRIVE_UNESCAPE.sub(r'\1', m2.group(1)))
                slash = bool(m2.group(2))
                if is_special:
                    if count == first and part[-1].lower() == 'unc':
                        complete += 2
                    elif count == first and part[-1].lower() == 'global':
                        first += 1
                        complete += 1
                if count == complete:
                    break
            if count == complete:
                if not regex:
                    drive = '\\\\{}{}'.format('\\'.join(part), '\\' if slash else '')
                else:
                    drive = r'[\\/]{2}' + r'[\\/]'.join([escape_drive(p, case_sensitive) for p in part])
    elif pattern.startswith(('\\\\', '/')):
        root_specified = True

    return root_specified, drive, slash, end


def _get_magic_symbols(pattern: AnyStr, unix: bool, flags: int) -> tuple[set[AnyStr], set[AnyStr]]:
    """Get magic symbols."""

    if isinstance(pattern, bytes):
        ptype = util.BYTES
        slash = b'\\'  # type: AnyStr
    else:
        ptype = util.UNICODE
        slash = '\\'

    if unix:
        magic_drive = set()  # type: set[AnyStr]
    else:
        magic_drive = {slash}

    magic = set(MAGIC_DEF[ptype])  # type: set[AnyStr]  # type: ignore[arg-type]
    if flags & BRACE:
        magic |= MAGIC_BRACE[ptype]  # type: ignore[arg-type]
        magic_drive |= MAGIC_BRACE[ptype]  # type: ignore[arg-type]
    if flags & SPLIT:
        magic |= MAGIC_SPLIT[ptype]  # type: ignore[arg-type]
        magic_drive |= MAGIC_SPLIT[ptype]  # type: ignore[arg-type]
    if flags & GLOBTILDE:
        magic |= MAGIC_TILDE[ptype]  # type: ignore[arg-type]
    if flags & EXTMATCH:
        magic |= MAGIC_EXTMATCH[ptype]  # type: ignore[arg-type]
    if flags & NEGATE:
        if flags & MINUSNEGATE:
            magic |= MAGIC_MINUS_NEGATE[ptype]  # type: ignore[arg-type]
        else:
            magic |= MAGIC_NEGATE[ptype]  # type: ignore[arg-type]

    return magic, magic_drive


def is_magic(pattern: AnyStr, flags: int = 0) -> bool:
    """Check if pattern is magic."""

    magical = False
    unix = is_unix_style(flags)

    if isinstance(pattern, bytes):
        ptype = util.BYTES
    else:
        ptype = util.UNICODE

    drive_pat = RE_WIN_DRIVE[ptype]  # type: Pattern[AnyStr]  # type: ignore[assignment]

    magic, magic_drive = _get_magic_symbols(pattern, unix, flags)
    is_path = flags & PATHNAME

    length = 0
    if is_path and ((unix is None and util.platform() == "windows") or unix is False):
        m = drive_pat.match(pattern)
        if m:
            drive = m.group(0)
            length = len(drive)
            for c in magic_drive:
                if c in drive:
                    magical = True
                    break

    if not magical:
        pattern = pattern[length:]
        for c in magic:
            if c in pattern:
                magical = True
                break

    return magical


def is_negative(pattern: AnyStr, flags: int) -> bool:
    """Check if negative pattern."""

    if flags & MINUSNEGATE:
        return bool(flags & NEGATE and pattern[0:1] in MINUS_NEGATIVE_SYM)
    elif flags & EXTMATCH:
        return bool(flags & NEGATE and pattern[0:1] in NEGATIVE_SYM and pattern[1:2] not in ROUND_BRACKET)
    else:
        return bool(flags & NEGATE and pattern[0:1] in NEGATIVE_SYM)


def tilde_pos(pattern: AnyStr, flags: int) -> int:
    """Is user folder."""

    pos = -1
    if flags & GLOBTILDE and flags & REALPATH:
        if flags & NEGATE:
            if pattern[0:1] in TILDE_SYM:
                pos = 0
            elif pattern[0:1] in NEGATIVE_SYM and pattern[1:2] in TILDE_SYM:
                pos = 1
        elif pattern[0:1] in TILDE_SYM:
            pos = 0
    return pos


def expand_braces(patterns: AnyStr, flags: int, limit: int) -> Iterable[AnyStr]:
    """Expand braces."""

    if flags & BRACE:
        for p in ([patterns] if isinstance(patterns, (str, bytes)) else patterns):
            try:
                # Turn off limit as we are handling it ourselves.
                yield from bracex.iexpand(p, keep_escapes=True, limit=limit)
            except bracex.ExpansionLimitException:  # noqa: PERF203
                raise
            except Exception:  # pragma: no cover
                # We will probably never hit this as `bracex`
                # doesn't throw any specific exceptions and
                # should normally always parse, but just in case.
                yield p
    else:
        for p in ([patterns] if isinstance(patterns, (str, bytes)) else patterns):
            yield p


def expand_tilde(pattern: AnyStr, is_unix: bool, flags: int) -> AnyStr:
    """Expand tilde."""

    pos = tilde_pos(pattern, flags)

    if pos > -1:
        string_type = util.BYTES if isinstance(pattern, bytes) else util.UNICODE
        tilde = TILDE_SYM[string_type]  # type: AnyStr  # type: ignore[assignment]
        re_tilde = RE_WIN_TILDE[string_type] if not is_unix else RE_TILDE[string_type]  # type: Pattern[AnyStr]  # type: ignore[assignment]
        m = re_tilde.match(pattern, pos)
        if m:
            expanded = os.path.expanduser(m.group(0))
            if not expanded.startswith(tilde) and os.path.exists(expanded):
                pattern = (pattern[0:1] if pos else pattern[0:0]) + escape(expanded, is_unix) + pattern[m.end(0):]
    return pattern


def expand(pattern: AnyStr, flags: int, limit: int) -> Iterable[AnyStr]:
    """Expand and normalize."""

    for expanded in expand_braces(pattern, flags, limit):
        for splitted in split(expanded, flags):
            yield expand_tilde(splitted, is_unix_style(flags), flags)


def is_case_sensitive(flags: int) -> bool:
    """Is case sensitive."""

    if bool(flags & FORCEWIN):
        case_sensitive = False
    elif bool(flags & FORCEUNIX):
        case_sensitive = True
    else:
        case_sensitive = util.is_case_sensitive()
    return case_sensitive


def get_case(flags: int) -> bool:
    """Parse flags for case sensitivity settings."""

    if not bool(flags & CASE_FLAGS):
        case_sensitive = is_case_sensitive(flags)
    elif flags & CASE:
        case_sensitive = True
    else:
        case_sensitive = False
    return case_sensitive


def escape_drive(drive: str, case: bool) -> str:
    """Escape drive."""

    return f'(?i:{re.escape(drive)})' if case else re.escape(drive)


def is_unix_style(flags: int) -> bool:
    """Check if we should use Unix style."""

    return (
        (
            (util.platform() != "windows") or
            (not bool(flags & REALPATH) and bool(flags & FORCEUNIX))
        ) and
        not flags & FORCEWIN
    )


def no_negate_flags(flags: int) -> int:
    """No negation."""

    if flags & NEGATE:
        flags ^= NEGATE
    if flags & NEGATEALL:
        flags ^= NEGATEALL
    return flags


@overload
def translate(
    patterns: str | Sequence[str],
    flags: int,
    limit: int = PATTERN_LIMIT,
    exclude: str | Sequence[str] | None = None
) -> tuple[list[str], list[str]]:
    ...


@overload
def translate(
    patterns: bytes | Sequence[bytes],
    flags: int,
    limit: int = PATTERN_LIMIT,
    exclude: bytes | Sequence[bytes] | None = None
) -> tuple[list[bytes], list[bytes]]:
    ...


def translate(
    patterns: AnyStr | Sequence[AnyStr],
    flags: int,
    limit: int = PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> tuple[list[AnyStr], list[AnyStr]]:
    """Translate patterns."""

    positive = []  # type: list[AnyStr]
    negative = []  # type: list[AnyStr]

    if exclude is not None:
        flags = no_negate_flags(flags)
        negative = translate(exclude, flags=flags | DOTMATCH | _NO_GLOBSTAR_CAPTURE, limit=limit)[0]
        limit -= len(negative)

    flags = (flags | _TRANSLATE) & FLAG_MASK
    is_unix = is_unix_style(flags)
    seen = set()

    try:
        current_limit = limit
        total = 0
        for pattern in iter_patterns(patterns):
            pattern = util.norm_pattern(pattern, not is_unix, bool(flags & RAWCHARS))
            count = 0
            for expanded in expand(pattern, flags, current_limit):
                count += 1
                total += 1
                if 0 < limit < total:
                    raise PatternLimitException(f"Pattern limit exceeded the limit of {limit:d}")
                if expanded not in seen:
                    seen.add(expanded)
                    if is_negative(expanded, flags):
                        negative.append(WcParse(expanded[1:], flags | _NO_GLOBSTAR_CAPTURE | DOTMATCH).parse())
                    else:
                        positive.append(WcParse(expanded, flags).parse())
            if limit:
                current_limit -= count
                if current_limit < 1:
                    current_limit = 1
    except bracex.ExpansionLimitException as e:
        raise PatternLimitException(f"Pattern limit exceeded the limit of {limit:d}") from e

    if negative and not positive:
        if flags & NEGATEALL:
            default = b'**' if isinstance(negative[0], bytes) else '**'
            positive.append(
                WcParse(default, flags | (GLOBSTAR if flags & PATHNAME else 0)).parse()
            )

    if positive and flags & NODIR:
        index = util.BYTES if isinstance(positive[0], bytes) else util.UNICODE
        negative.append(_NO_NIX_DIR[index] if is_unix else _NO_WIN_DIR[index])  # type: ignore[arg-type]

    return positive, negative


def split(pattern: AnyStr, flags: int) -> Iterable[AnyStr]:
    """Split patterns."""

    if flags & SPLIT:
        yield from WcSplit(pattern, flags).split()
    else:
        yield pattern


@overload
def compile_pattern(
    patterns: str | Sequence[str],
    flags: int,
    limit: int = PATTERN_LIMIT,
    exclude: str | Sequence[str] | None = None
) -> tuple[list[Pattern[str]], list[Pattern[str]]]:
    ...


@overload
def compile_pattern(
    patterns: bytes | Sequence[bytes],
    flags: int,
    limit: int = PATTERN_LIMIT,
    exclude: bytes | Sequence[bytes] | None = None
) -> tuple[list[Pattern[bytes]], list[Pattern[bytes]]]:
    ...


def compile_pattern(
    patterns: AnyStr | Sequence[AnyStr],
    flags: int,
    limit: int = PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> tuple[list[Pattern[AnyStr]], list[Pattern[AnyStr]]]:
    """Compile the patterns."""

    positive = []  # type: list[Pattern[AnyStr]]
    negative = []  # type: list[Pattern[AnyStr]]

    if exclude is not None:
        flags = no_negate_flags(flags)
        negative = compile_pattern(exclude, flags=flags | DOTMATCH | _NO_GLOBSTAR_CAPTURE, limit=limit)[0]
        limit -= len(negative)

    is_unix = is_unix_style(flags)
    seen = set()

    try:
        current_limit = limit
        total = 0
        for pattern in iter_patterns(patterns):
            pattern = util.norm_pattern(pattern, not is_unix, bool(flags & RAWCHARS))
            count = 0
            for expanded in expand(pattern, flags, current_limit):
                count += 1
                total += 1
                if 0 < limit < total:
                    raise PatternLimitException(f"Pattern limit exceeded the limit of {limit:d}")
                if expanded not in seen:
                    seen.add(expanded)
                    if is_negative(expanded, flags):
                        negative.append(_compile(expanded[1:], flags | _NO_GLOBSTAR_CAPTURE | DOTMATCH))
                    else:
                        positive.append(_compile(expanded, flags))
            if limit:
                current_limit -= count
                if current_limit < 1:
                    current_limit = 1
    except bracex.ExpansionLimitException as e:
        raise PatternLimitException(f"Pattern limit exceeded the limit of {limit:d}") from e

    if negative and not positive:
        if flags & NEGATEALL:
            default = b'**' if isinstance(negative[0].pattern, bytes) else '**'
            positive.append(_compile(default, flags | (GLOBSTAR if flags & PATHNAME else 0)))

    if positive and flags & NODIR:
        ptype = util.BYTES if isinstance(positive[0].pattern, bytes) else util.UNICODE
        negative.append(RE_NO_DIR[ptype] if is_unix else RE_WIN_NO_DIR[ptype])  # type: ignore[arg-type]

    return positive, negative


@overload
def compile(  # noqa: A001
    patterns: str | Sequence[str],
    flags: int,
    limit: int = PATTERN_LIMIT,
    exclude: str | Sequence[str] | None = None
) -> WcRegexp[str]:
    ...


@overload
def compile(  # noqa: A001
    patterns: bytes | Sequence[bytes],
    flags: int,
    limit: int = PATTERN_LIMIT,
    exclude: bytes | Sequence[bytes] | None = None
) -> WcRegexp[bytes]:
    ...


def compile(  # noqa: A001
    patterns: AnyStr | Sequence[AnyStr],
    flags: int,
    limit: int = PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> WcRegexp[AnyStr]:
    """Compile patterns."""

    positive, negative = compile_pattern(patterns, flags, limit, exclude)
    return WcRegexp(
        tuple(positive), tuple(negative),
        bool(flags & REALPATH), bool(flags & PATHNAME), bool(flags & FOLLOW) and not bool(flags & GLOBSTARLONG)
    )


@functools.lru_cache(maxsize=256, typed=True)
def _compile(pattern: AnyStr, flags: int) -> Pattern[AnyStr]:
    """Compile the pattern to regex."""

    return re.compile(WcParse(pattern, flags & FLAG_MASK).parse())


class WcSplit(Generic[AnyStr]):
    """Class that splits patterns on |."""

    def __init__(self, pattern: AnyStr, flags: int) -> None:
        """Initialize."""

        self.pattern = pattern  # type: AnyStr
        self.pathname = bool(flags & PATHNAME)
        self.extend = bool(flags & EXTMATCH)
        self.unix = is_unix_style(flags)
        self.bslash_abort = not self.unix

    def _sequence(self, i: util.StringIter) -> None:
        """Handle character group."""

        c = next(i)
        if c == '!':
            c = next(i)
        if c in ('^', '-', '['):
            c = next(i)

        try:
            while c != ']':
                if c == '\\':
                    # Handle escapes
                    self._references(i, True)
                elif c == '/':
                    if self.pathname:
                        raise StopIteration
                c = next(i)
        except PathNameException as e:
            raise StopIteration from e

    def _references(self, i: util.StringIter, sequence: bool = False) -> None:
        """Handle references."""

        c = next(i)
        if c == '\\':
            # \\
            if sequence and self.bslash_abort:
                raise PathNameException
        elif c == '/':
            # \/
            if sequence and self.pathname:
                raise PathNameException
        else:
            # \a, \b, \c, etc.
            pass

    def parse_extend(self, c: str, i: util.StringIter) -> bool:
        """Parse extended pattern lists."""

        # Start list parsing
        success = True
        index = i.index
        list_type = c
        try:
            c = next(i)
            if c != '(':
                raise StopIteration
            while c != ')':
                c = next(i)

                if self.extend and c in EXT_TYPES and self.parse_extend(c, i):
                    continue

                if c == '\\':
                    try:
                        self._references(i)
                    except StopIteration:
                        pass
                elif c == '[':
                    index = i.index
                    try:
                        self._sequence(i)
                    except StopIteration:
                        i.rewind(i.index - index)

        except StopIteration:
            success = False
            c = list_type
            i.rewind(i.index - index)

        return success

    def _split(self, pattern: str) -> Iterable[str]:
        """Split the pattern."""

        start = -1
        i = util.StringIter(pattern)

        for c in i:
            if self.extend and c in EXT_TYPES and self.parse_extend(c, i):
                continue

            if c == '|':
                split = i.index - 1
                p = pattern[start + 1:split]
                yield p
                start = split
            elif c == '\\':
                index = i.index
                try:
                    self._references(i)
                except StopIteration:
                    i.rewind(i.index - index)
            elif c == '[':
                index = i.index
                try:
                    self._sequence(i)
                except StopIteration:
                    i.rewind(i.index - index)

        if start < len(pattern):
            yield pattern[start + 1:]

    def split(self) -> Iterable[AnyStr]:
        """Split the pattern."""

        if isinstance(self.pattern, bytes):
            for p in self._split(self.pattern.decode('latin-1')):
                yield p.encode('latin-1')
        else:
            yield from self._split(self.pattern)


class WcParse(Generic[AnyStr]):
    """Parse the wildcard pattern."""

    def __init__(self, pattern: AnyStr, flags: int = 0) -> None:
        """Initialize."""

        self.pattern = pattern  # type: AnyStr
        self.no_abs = bool(flags & _NOABSOLUTE)
        self.braces = bool(flags & BRACE)
        self.is_bytes = isinstance(pattern, bytes)
        self.pathname = bool(flags & PATHNAME)
        self.raw_chars = bool(flags & RAWCHARS)
        self.globstarlong = self.pathname and bool(flags & GLOBSTARLONG)
        self.globstar = self.pathname and (self.globstarlong or bool(flags & GLOBSTAR))
        self.follow = bool(flags & FOLLOW)
        self.realpath = bool(flags & REALPATH) and self.pathname
        self.translate = bool(flags & _TRANSLATE)
        self.negate = bool(flags & NEGATE)
        self.globstar_capture = self.realpath and not self.translate and not bool(flags & _NO_GLOBSTAR_CAPTURE)
        self.dot = bool(flags & DOTMATCH)
        self.extend = bool(flags & EXTMATCH)
        self.matchbase = bool(flags & MATCHBASE)
        self.extmatchbase = bool(flags & _EXTMATCHBASE)
        self.anchor = bool(flags & _ANCHOR)
        self.nodotdir = bool(flags & NODOTDIR)
        self.capture = self.translate
        self.case_sensitive = get_case(flags)
        self.in_list = False
        self.inv_nest = False
        self.flags = flags
        self.inv_ext = 0
        self.unix = is_unix_style(self.flags)
        if not self.unix:
            self.win_drive_detect = self.pathname
            self.char_avoid = (ord('\\'), ord('/'), ord('.'))  # type: tuple[int, ...]
            self.bslash_abort = self.pathname
            sep = {"sep": re.escape('\\/')}
        else:
            self.win_drive_detect = False
            self.char_avoid = (ord('/'), ord('.'))
            self.bslash_abort = False
            sep = {"sep": re.escape('/')}
        self.bare_sep = sep['sep']
        self.sep = f'[{self.bare_sep}]'
        self.path_eop = _PATH_EOP.format(**sep)
        self.no_dir = _NO_DIR.format(**sep)
        self.seq_path = _PATH_NO_SLASH.format(**sep)
        self.seq_path_dot = _PATH_NO_SLASH_DOT.format(**sep)
        self.path_star = _PATH_STAR.format(**sep)
        self.path_star_dot1 = _PATH_STAR_DOTMATCH.format(**sep)
        self.path_star_dot2 = _PATH_STAR_NO_DOTMATCH.format(**sep)
        self.path_gstar_dot1 = _PATH_GSTAR_DOTMATCH.format(**sep)
        self.path_gstar_dot2 = _PATH_GSTAR_NO_DOTMATCH.format(**sep)
        if self.pathname:
            self.need_char = _NEED_CHAR_PATH.format(**sep)
        else:
            self.need_char = _NEED_CHAR

    def set_after_start(self) -> None:
        """Set tracker for character after the start of a directory."""

        self.after_start = True
        self.dir_start = False

    def set_start_dir(self) -> None:
        """Set directory start."""

        self.dir_start = True
        self.after_start = False

    def reset_dir_track(self) -> None:
        """Reset directory tracker."""

        self.dir_start = False
        self.after_start = False

    def update_dir_state(self) -> None:
        """
        Update the directory state.

        If we are at the directory start,
        update to after start state (the character right after).
        If at after start, reset state.
        """

        if self.dir_start and not self.after_start:
            self.set_after_start()
        elif not self.dir_start and self.after_start:
            self.reset_dir_track()

    def _restrict_extended_slash(self) -> str:
        """Restrict extended slash."""

        return self.seq_path if self.pathname else ''

    def _restrict_sequence(self) -> str:
        """Restrict sequence."""

        if self.pathname:
            value = self.seq_path_dot if self.after_start and not self.dot else self.seq_path
            if self.after_start:
                value = self.no_dir + value
        else:
            value = _NO_DOT if self.after_start and not self.dot else ""
        self.reset_dir_track()

        return value

    def _sequence_range_check(self, result: list[str], last: str) -> bool:
        """
        If range backwards, remove it.

        A bad range will cause the regular expression to fail,
        so we need to remove it, but return that we removed it
        so the caller can know the sequence wasn't empty.
        Caller will have to craft a sequence that makes sense
        if empty at the end with either an impossible sequence
        for inclusive sequences or a sequence that matches
        everything for an exclusive sequence.
        """

        removed = False
        first = result[-2]
        v1 = ord(first[1:2] if len(first) > 1 else first)
        v2 = ord(last[1:2] if len(last) > 1 else last)
        if v2 < v1:
            result.pop()
            result.pop()
            removed = True
        else:
            result.append(last)
        return removed

    def _handle_posix(self, i: util.StringIter, result: list[str], end_range: int) -> bool:
        """Handle posix classes."""

        last_posix = False
        m = i.match(RE_POSIX)
        if m:
            last_posix = True
            # Cannot do range with posix class
            # so escape last `-` if we think this
            # is the end of a range.
            if end_range and i.index - 1 >= end_range:
                result[-1] = '\\' + result[-1]
            result.append(posix.get_posix_property(m.group(1), self.is_bytes))
        return last_posix

    def _sequence(self, i: util.StringIter) -> str:
        """Handle character group."""

        result = ['[']
        end_range = 0
        escape_hyphen = -1
        removed = False
        last_posix = False

        c = next(i)
        if c in ('!', '^'):
            # Handle negate char
            result.append('^')
            c = next(i)
        if c == '[':
            last_posix = self._handle_posix(i, result, 0)
            if not last_posix:
                result.append(re.escape(c))
            c = next(i)
        elif c in ('-', ']'):
            result.append(re.escape(c))
            c = next(i)

        while c != ']':
            if c == '-':
                if last_posix:
                    result.append('\\' + c)
                    last_posix = False
                elif i.index - 1 > escape_hyphen:
                    # Found a range delimiter.
                    # Mark the next two characters as needing to be escaped if hyphens.
                    # The next character would be the end char range (s-e),
                    # and the one after that would be the potential start char range
                    # of a new range (s-es-e), so neither can be legitimate range delimiters.
                    result.append(c)
                    escape_hyphen = i.index + 1
                    end_range = i.index
                elif end_range and i.index - 1 >= end_range:
                    if self._sequence_range_check(result, '\\' + c):
                        removed = True
                    end_range = 0
                else:
                    result.append('\\' + c)
                c = next(i)
                continue
            last_posix = False

            if c == '[':
                last_posix = self._handle_posix(i, result, end_range)
                if last_posix:
                    c = next(i)
                    continue

            if c == '\\':
                # Handle escapes
                try:
                    value = self._references(i, True)
                except DotException:
                    value = re.escape(next(i))
                except PathNameException as e:
                    raise StopIteration from e
            elif c == '/':
                if self.pathname:
                    raise StopIteration
                value = c
            elif c in SET_OPERATORS:
                # Escape &, |, and ~ to avoid &&, ||, and ~~
                value = '\\' + c
            else:
                # Anything else
                value = c

            if end_range and i.index - 1 >= end_range:
                if self._sequence_range_check(result, value):
                    removed = True
                end_range = 0
            else:
                result.append(value)

            c = next(i)

        result.append(']')
        # Bad range removed.
        if removed:
            value = "".join(result)
            if value == '[]':
                # We specified some ranges, but they are all
                # out of reach.  Create an impossible sequence to match.
                result = [f'[^{ASCII_RANGE if self.is_bytes else UNICODE_RANGE}]']
            elif value == '[^]':
                # We specified some range, but hey are all
                # out of reach. Since this is exclusive
                # that means we can match *anything*.
                result = [f'[{ASCII_RANGE if self.is_bytes else UNICODE_RANGE}]']
            else:
                result = [value]

        if self.pathname or self.after_start:
            return self._restrict_sequence() + ''.join(result)

        return ''.join(result)

    def _references(self, i: util.StringIter, sequence: bool = False) -> str:
        """Handle references."""

        value = ''
        c = next(i)
        if c == '\\':
            # \\
            if sequence and self.bslash_abort:
                raise PathNameException
            value = r'\\'
            if self.bslash_abort:
                if not self.in_list:
                    value = self.sep + _ONE_OR_MORE
                    self.set_start_dir()
                else:
                    value = self._restrict_extended_slash() + self.sep
            elif not self.unix:
                value = self.sep if not sequence else self.bare_sep
        elif c == '/':
            # \/
            if sequence and self.pathname:
                raise PathNameException
            if self.pathname:
                if not self.in_list:
                    value = self.sep + _ONE_OR_MORE
                    self.set_start_dir()
                else:
                    value = self._restrict_extended_slash() + self.sep
            else:
                value = self.sep if not sequence else self.bare_sep
        elif c == '.':
            # Let dots be handled special
            i.rewind(1)
            raise DotException
        else:
            # \a, \b, \c, etc.
            value = re.escape(c)

        return value

    def _handle_dot(self, i: util.StringIter, current: list[str]) -> None:
        """Handle dot."""

        is_current = True
        is_previous = False

        if self.after_start and self.pathname and self.nodotdir:
            index = 0
            try:
                index = i.index
                while True:
                    c = next(i)
                    if c == '.' and is_current:
                        is_previous = True
                        is_current = False
                    elif c == '.' and is_previous:
                        is_previous = False
                        raise StopIteration
                    elif c in ('|', ')') and self.in_list:
                        raise StopIteration
                    elif c == '\\':
                        try:
                            self._references(i, True)
                            # Was not what we expected
                            is_current = False
                            is_previous = False
                            raise StopIteration
                        except DotException as e:
                            if is_current:
                                is_previous = True
                                is_current = False
                                c = next(i)
                            else:
                                is_previous = False
                                raise StopIteration from e
                        except PathNameException as e:
                            raise StopIteration from e
                    elif c == '/':
                        raise StopIteration
                    else:
                        is_current = False
                        is_previous = False
                        raise StopIteration
            except StopIteration:
                i.rewind(i.index - index)

        if not is_current and not is_previous:
            current.append(rf'(?!\.[.]?{self.path_eop})\.')
        else:
            current.append(re.escape('.'))

    def _handle_star(self, i: util.StringIter, current: list[str]) -> None:
        """Handle star."""

        if self.pathname:
            if self.after_start and not self.dot:
                star = self.path_star_dot2
                globstar = self.path_gstar_dot2
            elif self.after_start:
                star = self.path_star_dot1
                globstar = self.path_gstar_dot1
            else:
                star = self.path_star
                globstar = self.path_gstar_dot1
            capture = self.globstar_capture
        else:
            if self.after_start and not self.dot:
                star = _NO_DOT + _STAR
            else:
                star = _STAR
            globstar = ''
        value = star

        if self.after_start and self.globstar and not self.in_list:
            skip = True
            try:
                c = next(i)
                if c != '*':
                    i.rewind(1)
                    raise StopIteration
                skip = False

                # Test for triple star. If found, do not make a capturing group.
                # Capturing groups are used to filter out symlinks, but triple stars force symlinks.
                if self.globstarlong:
                    c = next(i)
                    if c != '*':
                        i.rewind(1)
                        raise StopIteration
                    capture = False

            except StopIteration:
                # Could not acquire a second star, so assume single star pattern
                pass

            if capture:
                globstar = f'({globstar})'

            if not skip:
                try:
                    index = i.index
                    c = next(i)
                    if c == '\\':
                        try:
                            self._references(i, True)
                            # Was not what we expected
                            # Assume two single stars
                        except DotException:
                            pass
                        except PathNameException:
                            # Looks like escape was a valid slash
                            # Store pattern accordingly
                            value = globstar
                            self.matchbase = False
                        except StopIteration:
                            # Escapes nothing, ignore and assume double star
                            value = globstar
                    elif c == '/':
                        value = globstar
                        self.matchbase = False

                    if value != globstar:
                        i.rewind(i.index - index)
                except StopIteration:
                    # Could not acquire directory slash due to no more characters
                    # Use double star
                    value = globstar

        if self.after_start and value != globstar:
            value = self.need_char + value
            # Consume duplicate starts
            try:
                c = next(i)
                while c == '*':
                    c = next(i)
                i.rewind(1)
            except StopIteration:
                pass

        self.reset_dir_track()
        if value == globstar:
            sep = _GLOBSTAR_DIV.format(self.sep)
            # Check if the last entry was a `globstar`
            # If so, don't bother adding another.
            if current[-1] != sep:
                if current[-1] == '':
                    # At the beginning of the pattern
                    current[-1] = value
                else:
                    # Replace the last path separator
                    current[-1] = _NEED_SEP.format(self.sep)
                    current.append(value)
                self.consume_path_sep(i)
                current.append(sep)
            self.set_start_dir()
        else:
            current.append(value)

    def clean_up_inverse(self, current: list[str], nested: bool = False) -> None:
        """
        Clean up current.

        Python doesn't have variable lookbehinds, so we have to do negative lookaheads.
        !(...) when converted to regular expression is atomic, so once it matches, that's it.
        So we use the pattern `(?:(?!(?:stuff|to|exclude)))[^/]*?)` where  is everything
        that comes after the negative group. `!(this|that)other` --> `(?:(?!(?:this|that)other))[^/]*?)`.

        We have to update the list before | in nested cases: *(!(...)|stuff). Before we close a parent
        `extmatch`: `*(!(...))`. And of course on path separators (when path mode is on): `!(...)/stuff`.
        Lastly we make sure all is accounted for when finishing the pattern at the end.  If there is nothing
        to store, we store `$`: `(?:(?!(?:this|that)$))[^/]*?)`.
        """

        if not self.inv_ext:
            return

        index = len(current) - 1
        while index >= 0:
            if isinstance(current[index], InvPlaceholder):
                content = current[index + 1:]
                if not nested:
                    content.append(_EOP if not self.pathname else self.path_eop)
                current[index] = (
                    (''.join(content).replace('(?#)', '?:') if self.capture else ''.join(content)) +
                    (_EXCLA_GROUP_CLOSE.format(str(current[index])))
                )
            index -= 1
        self.inv_ext = 0

    def parse_extend(self, c: str, i: util.StringIter, current: list[str], reset_dot: bool = False) -> bool:
        """Parse extended pattern lists."""

        # Save state
        temp_dir_start = self.dir_start
        temp_after_start = self.after_start
        temp_in_list = self.in_list
        temp_inv_ext = self.inv_ext
        temp_inv_nest = self.inv_nest
        self.in_list = True
        self.inv_nest = c == '!'

        if reset_dot:
            self.match_dot_dir = False

        # Start list parsing
        success = True
        index = i.index
        list_type = c
        extended = []  # type: list[str]

        try:
            c = next(i)
            if c != '(':
                raise StopIteration

            while c != ')':
                c = next(i)

                if self.extend and c in EXT_TYPES and self.parse_extend(c, i, extended):
                    # Nothing more to do
                    pass
                elif c == '*':
                    self._handle_star(i, extended)
                elif c == '.':
                    self._handle_dot(i, extended)
                    if self.after_start:
                        self.match_dot_dir = self.dot and not self.nodotdir
                        self.reset_dir_track()
                elif c == '?':
                    extended.append(self._restrict_sequence() + _QMARK)
                elif c == '/':
                    if self.pathname:
                        extended.append(self._restrict_extended_slash())
                    extended.append(self.sep)
                elif c == "|":
                    if self.inv_nest:
                        self.clean_up_inverse(extended, temp_inv_nest)
                    extended.append(c)
                    if temp_after_start:
                        self.set_start_dir()
                elif c == '\\':
                    try:
                        extended.append(self._references(i))
                    except DotException:
                        continue
                    except StopIteration:
                        # We've reached the end.
                        # Do nothing because this is going to abort the `extmatch` anyways.
                        pass
                elif c == '[':
                    subindex = i.index
                    try:
                        extended.append(self._sequence(i))
                    except StopIteration:
                        i.rewind(i.index - subindex)
                        extended.append(r'\[')
                elif c != ')':
                    extended.append(re.escape(c))

                self.update_dir_state()

            if list_type == '?':
                current.append((_QMARK_CAPTURE_GROUP if self.capture else _QMARK_GROUP).format(''.join(extended)))
            elif list_type == '*':
                current.append((_STAR_CAPTURE_GROUP if self.capture else _STAR_GROUP).format(''.join(extended)))
            elif list_type == '+':
                current.append((_PLUS_CAPTURE_GROUP if self.capture else _PLUS_GROUP).format(''.join(extended)))
            elif list_type == '@':
                current.append((_CAPTURE_GROUP if self.capture else _GROUP).format(''.join(extended)))
            elif list_type == '!':
                self.inv_ext += 1
                # If pattern is at the end, anchor the match to the end.
                current.append((_EXCLA_CAPTURE_GROUP if self.capture else _EXCLA_GROUP).format(''.join(extended)))
                if self.pathname:
                    if not temp_after_start or self.match_dot_dir:
                        star = self.path_star
                    elif temp_after_start and not self.dot:
                        star = self.path_star_dot2
                    else:
                        star = self.path_star_dot1
                else:
                    if not temp_after_start or self.dot:
                        star = _STAR
                    else:
                        star = _NO_DOT + _STAR

                if temp_after_start:
                    star = self.need_char + star
                # Place holder for closing, but store the proper star
                # so we know which one to use
                current.append(InvPlaceholder(star))

            if temp_in_list:
                self.clean_up_inverse(current, temp_inv_nest and self.inv_nest)

        except StopIteration:
            success = False
            self.inv_ext = temp_inv_ext
            i.rewind(i.index - index)

        # Either restore if extend parsing failed, or reset if it worked
        if not temp_in_list:
            self.in_list = False
        if not temp_inv_nest:
            self.inv_nest = False

        if success:
            self.reset_dir_track()
        else:
            self.dir_start = temp_dir_start
            self.after_start = temp_after_start

        return success

    def consume_path_sep(self, i: util.StringIter) -> None:
        """Consume any consecutive path separators as they count as one."""

        try:
            if self.bslash_abort:
                count = -1
                c = '\\'
                while c in ('\\', '/'):
                    if c != '/' or count % 2:
                        count += 1
                    else:
                        count += 2
                    c = next(i)
                i.rewind(1)
                # Rewind one more if we have an odd number (escape): \\\*
                if count > 0 and count % 2:
                    i.rewind(1)
            else:
                c = '/'
                while c == '/':
                    c = next(i)
                i.rewind(1)
        except StopIteration:
            pass

    def root(self, pattern: str, current: list[str]) -> None:
        """Start parsing the pattern."""

        self.set_after_start()
        i = util.StringIter(pattern)

        root_specified = False
        if self.win_drive_detect:
            root_specified, drive, slash, end = _get_win_drive(pattern, True, self.case_sensitive)
            if drive is not None:
                current.append(drive)
                if slash:
                    current.append(self.sep + _ONE_OR_MORE)
                i.advance(end)
                self.consume_path_sep(i)
            elif drive is None and root_specified:
                root_specified = True
        elif self.pathname and pattern.startswith('/'):
            root_specified = True

        if self.no_abs and root_specified:
            raise ValueError('The pattern must be a relative path pattern')

        if root_specified:
            self.matchbase = False
            self.extmatchbase = False

        if not root_specified and self.realpath:
            current.append(_NO_WIN_ROOT if self.win_drive_detect else _NO_ROOT)
            current.append('')

        for c in i:

            index = i.index
            if self.extend and c in EXT_TYPES and self.parse_extend(c, i, current, True):
                # Nothing to do
                pass
            elif c == '.':
                self._handle_dot(i, current)
            elif c == '*':
                self._handle_star(i, current)
            elif c == '?':
                current.append(self._restrict_sequence() + _QMARK)
            elif c == '/':
                if self.pathname:
                    self.set_start_dir()
                    self.clean_up_inverse(current)
                    current.append(self.sep + _ONE_OR_MORE)
                    self.consume_path_sep(i)
                    self.matchbase = False
                else:
                    current.append(self.sep)
            elif c == '\\':
                index = i.index
                try:
                    value = self._references(i)
                    if self.dir_start:
                        self.clean_up_inverse(current)
                        self.consume_path_sep(i)
                        self.matchbase = False
                    current.append(value)
                except DotException:
                    continue
                except StopIteration:
                    # Escapes nothing, ignore
                    i.rewind(i.index - index)
            elif c == '[':
                index = i.index
                try:
                    current.append(self._sequence(i))
                except StopIteration:
                    i.rewind(i.index - index)
                    current.append(re.escape(c))
            else:
                current.append(re.escape(c))

            self.update_dir_state()

        self.clean_up_inverse(current)

        if self.pathname:
            current.append(_PATH_TRAIL.format(self.sep))

    def _parse(self, p: str) -> str:
        """Parse pattern."""

        result = ['']
        prepend = ['']

        if self.anchor:
            p, number = (RE_ANCHOR if not self.win_drive_detect else RE_WIN_ANCHOR).subn('', p)
            if number:
                self.matchbase = False
                self.extmatchbase = False

        if self.matchbase or self.extmatchbase:
            if self.globstarlong and self.follow:
                self.root('***', prepend)
            else:
                globstar = self.globstar
                self.globstar = True
                self.root('**', prepend)
                self.globstar = globstar

        # We have an escape, but it escapes nothing
        if p == '\\':
            p = ''

        if p:
            self.root(p, result)

        if p and (self.matchbase or self.extmatchbase):
            result = prepend + result

        case_flag = 'i' if not self.case_sensitive else ''
        pattern = Rf'^(?s{case_flag}:{"".join(result)})$'

        if self.capture:
            # Strip out unnecessary regex comments
            pattern = pattern.replace('(?#)', '')

        return pattern

    def parse(self) -> AnyStr:
        """Parse pattern list."""

        if isinstance(self.pattern, bytes):
            pattern = self._parse(self.pattern.decode('latin-1')).encode('latin-1')
        else:
            pattern = self._parse(self.pattern)

        return pattern
wcmatch-10.0/wcmatch/fnmatch.py000066400000000000000000000056651467532413500165260ustar00rootroot00000000000000"""
Wild Card Match.

A custom implementation of `fnmatch`.
"""
from __future__ import annotations
from . import _wcparse
from typing import AnyStr, Iterable, Sequence

__all__ = (
    "CASE", "EXTMATCH", "IGNORECASE", "RAWCHARS",
    "NEGATE", "MINUSNEGATE", "DOTMATCH", "BRACE", "SPLIT",
    "NEGATEALL", "FORCEWIN", "FORCEUNIX",
    "C", "I", "R", "N", "M", "D", "E", "S", "B", "A", "W", "U",
    "translate", "fnmatch", "filter", "escape", "is_magic"
)

C = CASE = _wcparse.CASE
I = IGNORECASE = _wcparse.IGNORECASE
R = RAWCHARS = _wcparse.RAWCHARS
N = NEGATE = _wcparse.NEGATE
M = MINUSNEGATE = _wcparse.MINUSNEGATE
D = DOTMATCH = _wcparse.DOTMATCH
E = EXTMATCH = _wcparse.EXTMATCH
B = BRACE = _wcparse.BRACE
S = SPLIT = _wcparse.SPLIT
A = NEGATEALL = _wcparse.NEGATEALL
W = FORCEWIN = _wcparse.FORCEWIN
U = FORCEUNIX = _wcparse.FORCEUNIX

FLAG_MASK = (
    CASE |
    IGNORECASE |
    RAWCHARS |
    NEGATE |
    MINUSNEGATE |
    DOTMATCH |
    EXTMATCH |
    BRACE |
    SPLIT |
    NEGATEALL |
    FORCEWIN |
    FORCEUNIX
)


def _flag_transform(flags: int) -> int:
    """Transform flags to glob defaults."""

    # Enabling both cancels out
    if flags & FORCEUNIX and flags & FORCEWIN:
        flags ^= FORCEWIN | FORCEUNIX

    return (flags & FLAG_MASK)


def translate(
    patterns: AnyStr | Sequence[AnyStr],
    *,
    flags: int = 0,
    limit: int = _wcparse.PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> tuple[list[AnyStr], list[AnyStr]]:
    """Translate `fnmatch` pattern."""

    flags = _flag_transform(flags)
    return _wcparse.translate(patterns, flags, limit, exclude=exclude)


def fnmatch(
    filename: AnyStr,
    patterns: AnyStr | Sequence[AnyStr],
    *,
    flags: int = 0,
    limit: int = _wcparse.PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> bool:
    """
    Check if filename matches pattern.

    By default case sensitivity is determined by the file system,
    but if `case_sensitive` is set, respect that instead.
    """

    flags = _flag_transform(flags)
    return bool(_wcparse.compile(patterns, flags, limit, exclude=exclude).match(filename))


def filter(  # noqa A001
    filenames: Iterable[AnyStr],
    patterns: AnyStr | Sequence[AnyStr],
    *,
    flags: int = 0,
    limit: int = _wcparse.PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> list[AnyStr]:
    """Filter names using pattern."""

    matches = []

    flags = _flag_transform(flags)
    obj = _wcparse.compile(patterns, flags, limit, exclude=exclude)

    for filename in filenames:
        if obj.match(filename):
            matches.append(filename)  # noqa: PERF401
    return matches


def escape(pattern: AnyStr) -> AnyStr:
    """Escape."""

    return _wcparse.escape(pattern, pathname=False)


def is_magic(pattern: AnyStr, *, flags: int = 0) -> bool:
    """Check if the pattern is likely to be magic."""

    flags = _flag_transform(flags)
    return _wcparse.is_magic(pattern, flags)
wcmatch-10.0/wcmatch/glob.py000066400000000000000000001051551467532413500160240ustar00rootroot00000000000000"""
Wild Card Match.

A custom implementation of `glob`.
"""
from __future__ import annotations
import os
import sys
import re
import functools
from collections import namedtuple
import bracex
from . import _wcparse
from . import _wcmatch
from . import util
from typing import Iterator, Iterable, AnyStr, Generic, Pattern, Callable, Any, Sequence

__all__ = (
    "CASE", "IGNORECASE", "RAWCHARS", "DOTGLOB", "DOTMATCH",
    "EXTGLOB", "EXTMATCH", "GLOBSTAR", "NEGATE", "MINUSNEGATE", "BRACE", "NOUNIQUE",
    "REALPATH", "FOLLOW", "MATCHBASE", "MARK", "NEGATEALL", "NODIR", "FORCEWIN", "FORCEUNIX", "GLOBTILDE",
    "NODOTDIR", "SCANDOTDIR", "SUPPORT_DIR_FD", "GLOBSTARLONG",
    "C", "I", "R", "D", "E", "G", "N", "M", "B", "P", "L", "S", "X", 'K', "O", "A", "W", "U", "T", "Q", "Z", "SD", "GL",
    "iglob", "glob", "globmatch", "globfilter", "escape", "is_magic"
)

# We don't use `util.platform` only because we mock it in tests,
# and `scandir` will not work with bytes on the wrong system.
WIN = sys.platform.startswith('win')

SUPPORT_DIR_FD = _wcmatch.SUPPORT_DIR_FD

C = CASE = _wcparse.CASE
I = IGNORECASE = _wcparse.IGNORECASE
R = RAWCHARS = _wcparse.RAWCHARS
D = DOTGLOB = DOTMATCH = _wcparse.DOTMATCH
E = EXTGLOB = EXTMATCH = _wcparse.EXTMATCH
G = GLOBSTAR = _wcparse.GLOBSTAR
N = NEGATE = _wcparse.NEGATE
M = MINUSNEGATE = _wcparse.MINUSNEGATE
B = BRACE = _wcparse.BRACE
P = REALPATH = _wcparse.REALPATH
L = FOLLOW = _wcparse.FOLLOW
S = SPLIT = _wcparse.SPLIT
X = MATCHBASE = _wcparse.MATCHBASE
O = NODIR = _wcparse.NODIR
A = NEGATEALL = _wcparse.NEGATEALL
W = FORCEWIN = _wcparse.FORCEWIN
U = FORCEUNIX = _wcparse.FORCEUNIX
T = GLOBTILDE = _wcparse.GLOBTILDE
Q = NOUNIQUE = _wcparse.NOUNIQUE
Z = NODOTDIR = _wcparse.NODOTDIR
GL = GLOBSTARLONG = _wcparse.GLOBSTARLONG

K = MARK = 0x1000000
SD = SCANDOTDIR = 0x2000000

_PATHLIB = 0x8000000

# Internal flags
_EXTMATCHBASE = _wcparse._EXTMATCHBASE
_NOABSOLUTE = _wcparse._NOABSOLUTE
_PATHNAME = _wcparse.PATHNAME

FLAG_MASK = (
    CASE |
    IGNORECASE |
    RAWCHARS |
    DOTMATCH |
    EXTMATCH |
    GLOBSTAR |
    GLOBSTARLONG |
    NEGATE |
    MINUSNEGATE |
    BRACE |
    REALPATH |
    FOLLOW |
    SPLIT |
    MATCHBASE |
    NODIR |
    NEGATEALL |
    FORCEWIN |
    FORCEUNIX |
    GLOBTILDE |
    NOUNIQUE |
    NODOTDIR |
    _EXTMATCHBASE |
    _NOABSOLUTE
)

_RE_PATHLIB_DOT_NORM = (
    re.compile(r'(?:((?<=^)|(?<=/))\.(?:/|$))+'),
    re.compile(br'(?:((?<=^)|(?<=/))\.(?:/|$))+')
)  # type: tuple[Pattern[str], Pattern[bytes]]

_RE_WIN_PATHLIB_DOT_NORM = (
    re.compile(r'(?:((?<=^)|(?<=[\\/]))\.(?:[\\/]|$))+'),
    re.compile(br'(?:((?<=^)|(?<=[\\/]))\.(?:[\\/]|$))+')
)  # type: tuple[Pattern[str], Pattern[bytes]]


def _flag_transform(flags: int) -> int:
    """Transform flags to glob defaults."""

    # Enabling both cancels out
    if flags & FORCEUNIX and flags & FORCEWIN:
        flags ^= FORCEWIN | FORCEUNIX

    # Here we force `PATHNAME`.
    flags = (flags & FLAG_MASK) | _PATHNAME
    if flags & REALPATH:
        if util.platform() == "windows":
            if flags & FORCEUNIX:
                flags ^= FORCEUNIX
            flags |= FORCEWIN
        else:
            if flags & FORCEWIN:
                flags ^= FORCEWIN

    return flags


class _GlobPart(
    namedtuple('_GlobPart', ['pattern', 'is_magic', 'is_globstar', 'is_globstarlong', 'dir_only', 'is_drive']),
):
    """File Glob."""


class _GlobSplit(Generic[AnyStr]):
    """
    Split glob pattern on "magic" file and directories.

    Glob pattern return a list of patterns broken down at the directory
    boundary. Each piece will either be a literal file part or a magic part.
    Each part will will contain info regarding whether they are
    a directory pattern or a file pattern and whether the part
    is "magic", etc.: `["pattern", is_magic, is_globstar, dir_only, is_drive]`.

    Example:
    -------
        `"**/this/is_literal/*magic?/@(magic|part)"`

        Would  become:

        ```
        [
            ["**", True, True, False, False],
            ["this", False, False, True, False],
            ["is_literal", False, False, True, False],
            ["*magic?", True, False, True, False],
            ["@(magic|part)", True, False, False, False]
        ]
        ```

    """

    def __init__(self, pattern: AnyStr, flags: int) -> None:
        """Initialize."""

        self.pattern = pattern  # type: AnyStr
        self.unix = _wcparse.is_unix_style(flags)
        self.flags = flags
        self.no_abs = bool(flags & _wcparse._NOABSOLUTE)
        self.globstarlong = bool(flags & GLOBSTARLONG)
        self.globstar = self.globstarlong or bool(flags & GLOBSTAR)
        self.follow = bool(flags & FOLLOW)
        self.matchbase = bool(flags & MATCHBASE)
        self.extmatchbase = bool(flags & _wcparse._EXTMATCHBASE)
        self.tilde = bool(flags & GLOBTILDE)
        if _wcparse.is_negative(self.pattern, flags):  # pragma: no cover
            # This isn't really used, but we'll keep it around
            # in case we find a reason to directly send inverse patterns
            # Through here.
            self.pattern = self.pattern[0:1]
        if flags & NEGATE:
            flags ^= NEGATE
        self.flags = flags
        self.extend = bool(flags & EXTMATCH)
        if not self.unix:
            self.win_drive_detect = True
            self.bslash_abort = True
            self.sep = '\\'
        else:
            self.win_drive_detect = False
            self.bslash_abort = False
            self.sep = '/'
        # Once split, Windows file names will never have `\\` in them,
        # so we can use the Unix magic detect
        self.magic_symbols = _wcparse._get_magic_symbols(pattern, self.unix, self.flags)[0]  # type: set[AnyStr]

    def is_magic(self, name: AnyStr) -> bool:
        """Check if name contains magic characters."""

        for c in self.magic_symbols:
            if c in name:
                return True
        return False

    def _sequence(self, i: util.StringIter) -> None:
        """Handle character group."""

        c = next(i)
        if c == '!':
            c = next(i)
        if c in ('^', '-', '['):
            c = next(i)

        while c != ']':
            if c == '\\':
                # Handle escapes
                try:
                    self._references(i, True)
                except _wcparse.PathNameException as e:
                    raise StopIteration from e
            elif c == '/':
                raise StopIteration
            c = next(i)

    def _references(self, i: util.StringIter, sequence: bool = False) -> str:
        """Handle references."""

        value = ''

        c = next(i)
        if c == '\\':
            # \\
            if sequence and self.bslash_abort:
                raise _wcparse.PathNameException
            value = c
        elif c == '/':
            # \/
            if sequence:
                raise _wcparse.PathNameException
            value = c
        else:
            # \a, \b, \c, etc.
            pass
        return value

    def parse_extend(self, c: str, i: util.StringIter) -> bool:
        """Parse extended pattern lists."""

        # Start list parsing
        success = True
        index = i.index
        list_type = c
        try:
            c = next(i)
            if c != '(':
                raise StopIteration
            while c != ')':
                c = next(i)

                if self.extend and c in _wcparse.EXT_TYPES and self.parse_extend(c, i):
                    continue

                if c == '\\':
                    try:
                        self._references(i)
                    except StopIteration:
                        pass
                elif c == '[':
                    index = i.index
                    try:
                        self._sequence(i)
                    except StopIteration:
                        i.rewind(i.index - index)

        except StopIteration:
            success = False
            c = list_type
            i.rewind(i.index - index)

        return success

    def store(self, value: AnyStr, l: list[_GlobPart], dir_only: bool) -> None:
        """Group patterns by literals and potential magic patterns."""

        if l and value in (b'', ''):
            return

        globstarlong = self.globstarlong and value in (b'***', '***')
        globstar = globstarlong or (self.globstar and value in (b'**', '**'))
        magic = self.is_magic(value)
        if magic:
            v = _wcparse._compile(value, self.flags)  # type: Pattern[AnyStr] | AnyStr
        else:
            v = value
        if globstar and l and l[-1].is_globstar:
            l[-1] = _GlobPart(v, magic, globstar, globstarlong, dir_only, False)
        else:
            l.append(_GlobPart(v, magic, globstar, globstarlong, dir_only, False))

    def split(self) -> list[_GlobPart]:
        """Start parsing the pattern."""

        split_index = []
        parts = []
        start = -1

        if isinstance(self.pattern, bytes):
            is_bytes = True
            pattern = self.pattern.decode('latin-1')
        else:
            is_bytes = False
            pattern = self.pattern

        i = util.StringIter(pattern)

        # Detect and store away windows drive as a literal
        if self.win_drive_detect:
            root_specified, drive, slash, end = _wcparse._get_win_drive(pattern)
            if drive is not None:
                parts.append(_GlobPart(drive.encode('latin-1') if is_bytes else drive, False, False, False, True, True))
                start = end - 1
                i.advance(start)
            elif drive is None and root_specified:
                parts.append(_GlobPart(b'\\' if is_bytes else '\\', False, False, False, True, True))
                if pattern.startswith('/'):
                    start = 0
                    i.advance(1)
                else:
                    start = 1
                    i.advance(2)
        elif not self.win_drive_detect and pattern.startswith('/'):
            parts.append(_GlobPart(b'/' if is_bytes else '/', False, False, False, True, True))
            start = 0
            i.advance(1)

        for c in i:
            if self.extend and c in _wcparse.EXT_TYPES and self.parse_extend(c, i):
                continue

            if c == '\\':
                index = i.index
                value = ''
                try:
                    value = self._references(i)
                    if (self.bslash_abort and value == '\\') or value == '/':
                        split_index.append((i.index - 2, 1))
                except StopIteration:
                    i.rewind(i.index - index)
            elif c == '/':
                split_index.append((i.index - 1, 0))
            elif c == '[':
                index = i.index
                try:
                    self._sequence(i)
                except StopIteration:
                    i.rewind(i.index - index)

        for split, offset in split_index:
            value = pattern[start + 1:split]
            self.store(value.encode('latin-1') if is_bytes else value, parts, True)  # type: ignore[arg-type]
            start = split + offset

        if start < len(pattern):
            value = pattern[start + 1:]
            if value:
                self.store(value.encode('latin-1') if is_bytes else value, parts, False)  # type: ignore[arg-type]

        if len(pattern) == 0:
            parts.append(
                _GlobPart(pattern.encode('latin-1') if is_bytes else pattern, False, False, False, False, False)
            )

        if (
            (self.extmatchbase and not parts[0].is_drive) or
            (self.matchbase and len(parts) == 1 and not parts[0].dir_only)
        ):
            if self.globstarlong and self.follow:
                gstar = b'***' if is_bytes else '***'  # type: Any
                is_globstarlong = True
            else:
                gstar = b'**' if is_bytes else '**'
                is_globstarlong = False
            parts.insert(0, _GlobPart(gstar, True, True, is_globstarlong, True, False))

        if self.no_abs and parts and parts[0].is_drive:
            raise ValueError('The pattern must be a relative path pattern')

        return parts


class Glob(Generic[AnyStr]):
    """Glob patterns."""

    def __init__(
        self,
        pattern: AnyStr | Sequence[AnyStr],
        flags: int = 0,
        root_dir: AnyStr | os.PathLike[AnyStr] | None = None,
        dir_fd: int | None = None,
        limit: int = _wcparse.PATTERN_LIMIT,
        exclude: AnyStr | Sequence[AnyStr] | None = None
    ) -> None:
        """Initialize the directory walker object."""

        pats = [pattern] if isinstance(pattern, (str, bytes)) else pattern
        epats = [exclude] if isinstance(exclude, (str, bytes)) else exclude

        if epats is not None:
            flags = _wcparse.no_negate_flags(flags)

        self.pattern = []  # type: list[list[_GlobPart]]
        self.npatterns = []  # type: list[Pattern[AnyStr]]
        self.seen = set()  # type: set[AnyStr]
        self.dir_fd = dir_fd if SUPPORT_DIR_FD else None  # type: int | None
        self.nounique = bool(flags & NOUNIQUE)  # type: bool
        self.mark = bool(flags & MARK)  # type: bool
        # Only scan for `.` and `..` if it is specifically requested.
        self.scandotdir = bool(flags & SCANDOTDIR)  # type: bool
        if self.mark:
            flags ^= MARK
        self.negateall = bool(flags & NEGATEALL)  # type: bool
        if self.negateall:
            flags ^= NEGATEALL
        self.nodir = bool(flags & NODIR)  # type: bool
        if self.nodir:
            flags ^= NODIR
        self.pathlib = bool(flags & _PATHLIB)  # type: bool
        if self.pathlib:
            flags ^= _PATHLIB
        self.flags = _flag_transform(flags | REALPATH)  # type: int
        self.negate_flags = self.flags | DOTMATCH | _wcparse._NO_GLOBSTAR_CAPTURE  # type: int
        if not self.scandotdir and not self.flags & NODOTDIR:
            self.flags |= NODOTDIR
        self.raw_chars = bool(self.flags & RAWCHARS)  # type: bool
        self.dot = bool(self.flags & DOTMATCH)  # type: bool
        self.unix = not bool(self.flags & FORCEWIN)  # type: bool
        self.negate = bool(self.flags & NEGATE)  # type: bool
        self.globstarlong = bool(self.flags & GLOBSTARLONG)  # type: bool
        self.globstar = self.globstarlong or bool(self.flags & GLOBSTAR)  # type: bool
        self.follow_links = bool(self.flags & FOLLOW) and not self.globstarlong  # type: bool
        self.braces = bool(self.flags & BRACE)  # type: bool
        self.matchbase = bool(self.flags & MATCHBASE)  # type: bool
        self.case_sensitive = _wcparse.get_case(self.flags)  # type: bool
        self.limit = limit  # type: int

        forcewin = self.flags & FORCEWIN
        if isinstance(pats[0], bytes):
            ptype = util.BYTES
            self.current = b'.'  # type: AnyStr
            self.specials = (b'.', b'..')  # type: tuple[AnyStr, ...]
            self.empty = b''  # type: AnyStr
            self.stars = b'**'  # type: AnyStr
            self.sep = b'\\' if forcewin else b'/'  # type: AnyStr
            self.seps = (b'/', self.sep) if forcewin else (self.sep,)  # type: tuple[AnyStr, ...]
            self.re_pathlib_norm = _RE_WIN_PATHLIB_DOT_NORM[ptype]  # type: Pattern[AnyStr]  # type: ignore[assignment]
            self.re_no_dir = _wcparse.RE_WIN_NO_DIR[ptype]  # type: Pattern[AnyStr]  # type: ignore[assignment]
        else:
            ptype = util.UNICODE
            self.current = '.'
            self.specials = ('.', '..')
            self.empty = ''
            self.stars = '**'
            self.sep = '\\' if forcewin else '/'
            self.seps = ('/', self.sep) if forcewin else (self.sep,)
            self.re_pathlib_norm = _RE_WIN_PATHLIB_DOT_NORM[ptype]  # type: ignore[assignment]
            self.re_no_dir = _wcparse.RE_WIN_NO_DIR[ptype]  # type: ignore[assignment]

        temp = os.fspath(root_dir) if root_dir is not None else self.current
        if not isinstance(temp, bytes if ptype else str):
            raise TypeError(
                f'Pattern and root_dir should be of the same type, not {type(pats[0])} and {type(temp)}'
            )

        self.root_dir = temp  # type: AnyStr
        self.current_limit = self.limit
        self._parse_patterns(pats)
        if epats is not None:
            self._parse_patterns(epats, force_negate=True)

    def _iter_patterns(self, patterns: Sequence[AnyStr], force_negate: bool = False) -> Iterator[tuple[bool, AnyStr]]:
        """Iterate expanded patterns."""

        seen = set()
        try:
            total = 0
            for p in patterns:
                p = util.norm_pattern(p, not self.unix, self.raw_chars)
                count = 0
                for expanded in _wcparse.expand(p, self.flags, self.current_limit):
                    count += 1
                    total += 1
                    if 0 < self.limit < total:
                        raise _wcparse.PatternLimitException(
                            f"Pattern limit exceeded the limit of {self.limit:d}"
                        )
                    # Filter out duplicate patterns. If `NOUNIQUE` is enabled,
                    # we only want to filter on negative patterns as they are
                    # only filters.
                    is_neg = force_negate or _wcparse.is_negative(expanded, self.flags)
                    if not self.nounique or is_neg:
                        if expanded in seen:
                            continue
                        seen.add(expanded)

                    yield is_neg, expanded[1:] if is_neg and not force_negate else expanded
                if self.limit:
                    self.current_limit -= count
                    if self.current_limit < 1:
                        self.current_limit = 1
        except bracex.ExpansionLimitException as e:
            raise _wcparse.PatternLimitException(
                f"Pattern limit exceeded the limit of {self.limit:d}"
            ) from e

    def _parse_patterns(self, patterns: Sequence[AnyStr], force_negate: bool = False) -> None:
        """Parse patterns."""

        for is_neg, p in self._iter_patterns(patterns, force_negate=force_negate):
            if is_neg:
                # Treat the inverse pattern as a normal pattern if it matches, we will exclude.
                # This is faster as compiled patterns usually compare the include patterns first,
                # and then the exclude, but glob will already know it wants to include the file.
                self.npatterns.append(_wcparse._compile(p, self.negate_flags))
            else:
                self.pattern.append(_GlobSplit(p, self.flags).split())

        if not self.pattern and self.npatterns:
            if self.negateall:
                default = self.stars
                self.pattern.append(_GlobSplit(default, self.flags | GLOBSTAR).split())

        if self.nodir and not force_negate:
            self.npatterns.append(self.re_no_dir)

        # A single positive pattern will not find multiples of the same file
        # disable unique mode so that we won't waste time or memory computing unique returns.
        if (
            not force_negate and
            len(self.pattern) <= 1 and
            not self.flags & NODOTDIR and
            not self.nounique and
            not (self.pathlib and self.scandotdir)
        ):
            self.nounique = True

    def _is_hidden(self, name: AnyStr) -> bool:
        """Check if is file hidden."""

        return not self.dot and name[0:1] == self.specials[0]

    def _is_this(self, name: AnyStr) -> bool:
        """Check if "this" directory `.`."""

        return name == self.specials[0] or name == self.sep

    def _is_parent(self, name: AnyStr) -> bool:
        """Check if `..`."""

        return name == self.specials[1]

    def _match_excluded(self, filename: AnyStr, is_dir: bool) -> bool:
        """Check if file should be excluded."""

        if is_dir and not filename.endswith(self.sep):
            filename += self.sep

        matched = False
        for pattern in self.npatterns:
            if pattern.fullmatch(filename):
                matched = True
                break

        return matched

    def _is_excluded(self, path: AnyStr, is_dir: bool) -> bool:
        """Check if file is excluded."""

        return bool(self.npatterns and self._match_excluded(path, is_dir))

    def _match_literal(self, a: AnyStr, b: AnyStr | None = None) -> bool:
        """Match two names."""

        return a.lower() == b if not self.case_sensitive else a == b

    def _get_matcher(self, target: AnyStr | Pattern[AnyStr] | None) -> Callable[..., Any] | None:
        """Get deep match."""

        if target is None:
            matcher = None  # type: Callable[..., Any] | None
        elif isinstance(target, (str, bytes)):
            # Plain text match
            if not self.case_sensitive:
                match = target.lower()
            else:
                match = target
            matcher = functools.partial(self._match_literal, b=match)
        else:
            # File match pattern
            matcher = target.match
        return matcher

    def _lexists(self, path: AnyStr) -> bool:
        """Check if file exists."""

        if not self.dir_fd:
            return os.path.lexists(self.prepend_base(path))
        try:
            os.lstat(self.prepend_base(path), dir_fd=self.dir_fd)
        except (OSError, ValueError):  # pragma: no cover
            return False
        else:
            return True

    def prepend_base(self, path: AnyStr) -> AnyStr:
        """Join path to base if pattern is not absolute."""

        if self.is_abs_pattern:
            return path
        else:
            return os.path.join(self.root_dir, path)

    def _iter(self, curdir: AnyStr | None, dir_only: bool, deep: bool) -> Iterator[tuple[AnyStr, bool, bool, bool]]:
        """Iterate the directory."""

        try:
            fd = None  # type: int | None
            if self.is_abs_pattern and curdir:
                scandir = curdir  # type: AnyStr | int
            elif self.dir_fd is not None:
                fd = scandir = os.open(
                    os.path.join(self.root_dir, curdir) if curdir else self.root_dir,
                    _wcmatch.DIR_FLAGS,
                    dir_fd=self.dir_fd
                )
            else:
                scandir = os.path.join(self.root_dir, curdir) if curdir else self.root_dir

            # Python will never return . or .., so fake it.
            for special in self.specials:
                yield special, True, True, False

            try:
                with os.scandir(scandir) as scan:
                    for f in scan:
                        try:
                            hidden = self._is_hidden(f.name)  # type: ignore[arg-type]
                            is_dir = f.is_dir()
                            if is_dir:
                                is_link = f.is_symlink()
                            else:
                                # We don't care if a file is a link
                                is_link = False
                            if (not dir_only or is_dir):
                                yield f.name, is_dir, hidden, is_link  # type: ignore[misc]
                        except OSError:  # pragma: no cover # noqa: PERF203
                            pass
            finally:
                if fd is not None:
                    os.close(fd)

        except OSError:  # pragma: no cover
            pass

    def _glob_dir(
        self,
        curdir: AnyStr,
        matcher: Callable[..., Any] | None,
        dir_only: bool = False,
        deep: bool = False,
        globstar_follow: bool = False
    ) -> Iterator[tuple[AnyStr, bool]]:
        """Recursive directory glob."""

        files = list(self._iter(curdir, dir_only, deep))
        for file, is_dir, hidden, is_link in files:
            if file in self.specials:
                if matcher is not None and matcher(file):
                    yield os.path.join(curdir, file), True
                continue

            path = os.path.join(curdir, file)
            if (matcher is None and not hidden) or (matcher and matcher(file)):
                yield path, is_dir

            follow = not is_link or self.follow_links or globstar_follow
            if deep and not hidden and is_dir and follow:
                yield from self._glob_dir(path, matcher, dir_only, deep, globstar_follow)

    def _glob(self, curdir: AnyStr, part: _GlobPart, rest: list[_GlobPart]) -> Iterator[tuple[AnyStr, bool]]:
        """
        Handle glob flow.

        There are really only a couple of cases:

        - File name.
        - File name pattern (magic).
        - Directory.
        - Directory name pattern (magic).
        - Extra slashes `////`.
        - `globstar` `**`.
        """

        is_magic = part.is_magic
        dir_only = part.dir_only
        target = part.pattern
        is_globstar = part.is_globstar
        is_globstarlong = part.is_globstarlong

        if is_magic and is_globstar:
            # Glob star directory `**`.

            # Acquire the pattern after the `globstars` if available.
            # If not, mark that the `globstar` is the end.
            this = rest.pop(0) if rest else None
            globstar_end = this is None
            if this:
                dir_only = this.dir_only
                target = this.pattern

            if globstar_end:
                target = None

            # We match `**/next` during a deep glob, so what ever comes back,
            # we will send back through `_glob` with pattern after `next` (`**/next/after`).
            # So grab `after` if available.
            this = rest.pop(0) if rest else None

            # Deep searching is the unique case where we
            # might feed in a `None` for the next pattern to match.
            # Deep glob will account for this.
            matcher = self._get_matcher(target)

            # If our pattern ends with `curdir/**`, but does not start with `**` it matches zero or more,
            # so it should return `curdir/`, signifying `curdir` + no match.
            # If a pattern follows `**/something`, we always get the appropriate
            # return already, so this isn't needed in that case.
            # There is one quirk though with Bash, if `curdir` had magic before `**`, Bash
            # omits the trailing `/`. We don't worry about that.
            if globstar_end and curdir:
                yield os.path.join(curdir, self.empty), True

            # Search
            for path, is_dir in self._glob_dir(curdir, matcher, dir_only, deep=True, globstar_follow=is_globstarlong):
                if this:
                    yield from self._glob(path, this, rest[:])
                else:
                    yield path, is_dir

        elif not dir_only:
            # Files: no need to recursively search at this point as we are done.
            matcher = self._get_matcher(target)
            yield from self._glob_dir(curdir, matcher)

        else:
            # Directory: search current directory against pattern
            # and feed the results back through with the next pattern.
            this = rest.pop(0) if rest else None
            matcher = self._get_matcher(target)
            for path, is_dir in self._glob_dir(curdir, matcher, True):
                if this:
                    yield from self._glob(path, this, rest[:])
                else:
                    yield path, is_dir

    def _get_starting_paths(self, curdir: AnyStr, dir_only: bool) -> list[tuple[AnyStr, bool]]:
        """
        Get the starting location.

        For case sensitive paths, we have to "glob" for
        it first as Python doesn't like for its users to
        think about case. By scanning for it, we can get
        the actual casing and then compare.
        """

        if not self.is_abs_pattern and not self._is_parent(curdir) and not self._is_this(curdir):
            results = []
            matcher = self._get_matcher(curdir)
            files = list(self._iter(None, dir_only, False))
            for file, is_dir, _hidden, _is_link in files:
                if file not in self.specials and (matcher is None or matcher(file)):
                    results.append((file, is_dir))
        else:
            results = [(curdir, True)]
        return results

    def is_unique(self, path: AnyStr) -> bool:
        """Test if path is unique."""

        if self.nounique:
            return True

        unique = False
        if (path.lower() if not self.case_sensitive else path) not in self.seen:
            self.seen.add(path)
            unique = True
        return unique

    def _pathlib_norm(self, path: AnyStr) -> AnyStr:
        """Normalize path as `pathlib` does."""

        path = self.re_pathlib_norm.sub(self.empty, path)
        return path[:-1] if len(path) > 1 and path[-1:] in self.seps else path

    def format_path(self, path: AnyStr, is_dir: bool, dir_only: bool) -> Iterator[AnyStr]:
        """Format path."""

        path = os.path.join(path, self.empty) if dir_only or (self.mark and is_dir) else path
        if self.is_unique(self._pathlib_norm(path) if self.pathlib else path):
            yield path

    def glob(self) -> Iterator[AnyStr]:
        """Starts off the glob iterator."""

        for pattern in self.pattern:
            curdir = self.current

            # If the pattern ends with `/` we return the files ending with `/`.
            dir_only = pattern[-1].dir_only if pattern else False
            self.is_abs_pattern = pattern[0].is_drive if pattern else False

            if pattern:
                if not pattern[0].is_magic:
                    # Path starts with normal plain text
                    # Lets verify the case of the starting directory (if possible)
                    this = pattern[0]
                    curdir = this[0]

                    # Abort if we cannot find the drive, or if current directory is empty
                    if not curdir or (self.is_abs_pattern and not self._lexists(self.prepend_base(curdir))):
                        continue

                    # Make sure case matches, but running case insensitive
                    # on a case sensitive file system may return more than
                    # one starting location.
                    results = self._get_starting_paths(curdir, dir_only)
                    if not results:
                        continue

                    if this.dir_only:
                        # Glob these directories if they exists
                        for start, is_dir in results:
                            rest = pattern[1:]
                            if rest:
                                this = rest.pop(0)
                                for match, is_dir in self._glob(start, this, rest):
                                    if not self._is_excluded(match, is_dir):
                                        yield from self.format_path(match, is_dir, dir_only)
                            elif not self._is_excluded(start, is_dir):
                                yield from self.format_path(start, is_dir, dir_only)
                    else:
                        # Return the file(s) and finish.
                        for match, is_dir in results:
                            if self._lexists(match) and not self._is_excluded(match, is_dir):
                                yield from self.format_path(match, is_dir, dir_only)
                else:
                    # Path starts with a magic pattern, let's get globbing
                    rest = pattern[:]
                    this = rest.pop(0)
                    for match, is_dir in self._glob(curdir if not curdir == self.current else self.empty, this, rest):
                        if not self._is_excluded(match, is_dir):
                            yield from self.format_path(match, is_dir, dir_only)


def iglob(
    patterns: AnyStr | Sequence[AnyStr],
    *,
    flags: int = 0,
    root_dir: AnyStr | os.PathLike[AnyStr] | None = None,
    dir_fd: int | None = None,
    limit: int = _wcparse.PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> Iterator[AnyStr]:
    """Glob."""

    if not isinstance(patterns, (str, bytes)) and not patterns:
        return

    yield from Glob(patterns, flags, root_dir, dir_fd, limit, exclude).glob()


def glob(
    patterns: AnyStr | Sequence[AnyStr],
    *,
    flags: int = 0,
    root_dir: AnyStr | os.PathLike[AnyStr] | None = None,
    dir_fd: int | None = None,
    limit: int = _wcparse.PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> list[AnyStr]:
    """Glob."""

    return list(iglob(patterns, flags=flags, root_dir=root_dir, dir_fd=dir_fd, limit=limit, exclude=exclude))


def translate(
    patterns: AnyStr | Sequence[AnyStr],
    *,
    flags: int = 0,
    limit: int = _wcparse.PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> tuple[list[AnyStr], list[AnyStr]]:
    """Translate glob pattern."""

    flags = _flag_transform(flags)
    return _wcparse.translate(patterns, flags, limit, exclude)


def globmatch(
    filename: AnyStr | os.PathLike[AnyStr],
    patterns: AnyStr | Sequence[AnyStr],
    *,
    flags: int = 0,
    root_dir: AnyStr | os.PathLike[AnyStr] | None = None,
    dir_fd: int | None = None,
    limit: int = _wcparse.PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> bool:
    """
    Check if filename matches pattern.

    By default case sensitivity is determined by the file system,
    but if `case_sensitive` is set, respect that instead.
    """

    # Shortcut out if we have no patterns
    if not patterns:
        return False

    rdir = os.fspath(root_dir) if root_dir is not None else root_dir
    flags = _flag_transform(flags)
    fname = os.fspath(filename)

    return bool(_wcparse.compile(patterns, flags, limit, exclude).match(fname, rdir, dir_fd))


def globfilter(
    filenames: Iterable[AnyStr | os.PathLike[AnyStr]],
    patterns: AnyStr | Sequence[AnyStr],
    *,
    flags: int = 0,
    root_dir: AnyStr | os.PathLike[AnyStr] | None = None,
    dir_fd: int | None = None,
    limit: int = _wcparse.PATTERN_LIMIT,
    exclude: AnyStr | Sequence[AnyStr] | None = None
) -> list[AnyStr | os.PathLike[AnyStr]]:
    """Filter names using pattern."""

    # Shortcut out if we have no patterns
    if not patterns:
        return []

    rdir = os.fspath(root_dir) if root_dir is not None else root_dir

    matches = []  # type: list[AnyStr | os.PathLike[AnyStr]]
    flags = _flag_transform(flags)
    obj = _wcparse.compile(patterns, flags, limit, exclude)

    for filename in filenames:
        temp = os.fspath(filename)
        if obj.match(temp, rdir, dir_fd):
            matches.append(filename)
    return matches


def escape(pattern: AnyStr, unix: bool | None = None) -> AnyStr:
    """Escape."""

    return _wcparse.escape(pattern, unix=unix)


def is_magic(pattern: AnyStr, *, flags: int = 0) -> bool:
    """Check if the pattern is likely to be magic."""

    flags = _flag_transform(flags)
    return _wcparse.is_magic(pattern, flags)
wcmatch-10.0/wcmatch/pathlib.py000066400000000000000000000174151467532413500165250ustar00rootroot00000000000000"""Pathlib implementation that uses our own glob."""
from __future__ import annotations
import pathlib
import os
from . import glob
from . import _wcparse
from . import util
from typing import Iterable, Any, Sequence

__all__ = (
    "CASE", "IGNORECASE", "RAWCHARS", "DOTGLOB", "DOTMATCH",
    "EXTGLOB", "EXTMATCH", "NEGATE", "MINUSNEGATE", "BRACE",
    "REALPATH", "FOLLOW", "MATCHBASE", "NEGATEALL", "NODIR", "NOUNIQUE",
    "NODOTDIR", "SCANDOTDIR", "GLOBSTARLONG",
    "C", "I", "R", "D", "E", "G", "N", "B", "M", "P", "L", "S", "X", "O", "A", "Q", "Z", "SD", "GL",
    "Path", "PurePath", "WindowsPath", "PosixPath", "PurePosixPath", "PureWindowsPath"
)

C = CASE = glob.CASE
I = IGNORECASE = glob.IGNORECASE
R = RAWCHARS = glob.RAWCHARS
D = DOTGLOB = DOTMATCH = glob.DOTMATCH
E = EXTGLOB = EXTMATCH = glob.EXTMATCH
G = GLOBSTAR = glob.GLOBSTAR
N = NEGATE = glob.NEGATE
B = BRACE = glob.BRACE
M = MINUSNEGATE = glob.MINUSNEGATE
P = REALPATH = glob.REALPATH
L = FOLLOW = glob.FOLLOW
S = SPLIT = glob.SPLIT
X = MATCHBASE = glob.MATCHBASE
O = NODIR = glob.NODIR
A = NEGATEALL = glob.NEGATEALL
Q = NOUNIQUE = glob.NOUNIQUE
Z = NODOTDIR = glob.NODOTDIR
GL = GLOBSTARLONG = glob.GLOBSTARLONG

SD = SCANDOTDIR = glob.SCANDOTDIR

# Internal flags
_EXTMATCHBASE = _wcparse._EXTMATCHBASE
_NOABSOLUTE = _wcparse._NOABSOLUTE
_PATHNAME = _wcparse.PATHNAME
_FORCEWIN = _wcparse.FORCEWIN
_FORCEUNIX = _wcparse.FORCEUNIX

_PATHLIB = glob._PATHLIB

FLAG_MASK = (
    CASE |
    IGNORECASE |
    RAWCHARS |
    DOTMATCH |
    EXTMATCH |
    GLOBSTAR |
    GLOBSTARLONG |
    NEGATE |
    MINUSNEGATE |
    BRACE |
    REALPATH |
    FOLLOW |
    SPLIT |
    MATCHBASE |
    NODIR |
    NEGATEALL |
    NOUNIQUE |
    NODOTDIR |
    _EXTMATCHBASE |
    _NOABSOLUTE
)


class PurePath(pathlib.PurePath):
    """Special pure pathlike object that uses our own glob methods."""

    __slots__ = ()

    def __new__(cls, *args: str) -> 'PurePath':
        """New."""

        if cls is PurePath:
            cls = PureWindowsPath if os.name == 'nt' else PurePosixPath
        if not util.PY312:
            return cls._from_parts(args)  # type: ignore[no-any-return,attr-defined]
        else:
            return object.__new__(cls)

    def _translate_flags(self, flags: int) -> int:
        """Translate flags for the current `pathlib` object."""

        flags = (flags & FLAG_MASK) | _PATHNAME
        if flags & REALPATH:
            flags |= _FORCEWIN if os.name == 'nt' else _FORCEUNIX
        if isinstance(self, PureWindowsPath):
            if flags & _FORCEUNIX:
                raise ValueError("Windows pathlike objects cannot be forced to behave like a Posix path")
            flags |= _FORCEWIN
        elif isinstance(self, PurePosixPath):
            if flags & _FORCEWIN:
                raise ValueError("Posix pathlike objects cannot be forced to behave like a Windows path")
            flags |= _FORCEUNIX
        return flags

    def _translate_path(self) -> str:
        """Translate the object to a path string and ensure trailing slash for non-pure paths that are directories."""

        sep = ''
        name = str(self)
        if isinstance(self, Path) and name and self.is_dir():
            sep = self.parser.sep if util.PY313 else self._flavour.sep

        return name + sep

    def match(  # type: ignore[override, unused-ignore]
        self,
        patterns: str | Sequence[str],
        *,
        flags: int = 0,
        limit: int = _wcparse.PATTERN_LIMIT,
        exclude: str | Sequence[str] | None = None
    ) -> bool:
        """
        Match patterns using `globmatch`, but also using the same right to left logic that the default `pathlib` uses.

        This uses the same right to left logic that the default `pathlib` object uses.
        Folders and files are essentially matched from right to left.
        """

        return self.globmatch(patterns, flags=flags | _EXTMATCHBASE, limit=limit, exclude=exclude)

    def globmatch(
        self,
        patterns: str | Sequence[str],
        *,
        flags: int = 0,
        limit: int = _wcparse.PATTERN_LIMIT,
        exclude: str | Sequence[str] | None = None
    ) -> bool:
        """Match patterns using `globmatch`, but without the right to left logic that the default `pathlib` uses."""

        return glob.globmatch(
            self._translate_path(),
            patterns,
            flags=self._translate_flags(flags),
            limit=limit,
            exclude=exclude
        )

    def full_match(  # type: ignore[override, unused-ignore]
        self,
        patterns: str | Sequence[str],
        *,
        flags: int = 0,
        limit: int = _wcparse.PATTERN_LIMIT,
        exclude: str | Sequence[str] | None = None
    ) -> bool:
        """Alias for Python 3.13 `full_match`, but redirects to use `globmatch`."""

        return glob.globmatch(
            self._translate_path(),
            patterns,
            flags=self._translate_flags(flags),
            limit=limit,
            exclude=exclude
        )


class Path(pathlib.Path):
    """Special pathlike object (which accesses the filesystem) that uses our own glob methods."""

    __slots__ = ()

    def __new__(cls, *args: str, **kwargs: Any) -> 'Path':
        """New."""

        win_host = os.name == 'nt'
        if cls is Path:
            cls = WindowsPath if win_host else PosixPath
        if not util.PY312:
            if util.PY310:
                self = cls._from_parts(args)  # type: ignore[attr-defined]
            else:
                self = cls._from_parts(args, init=False)  # type: ignore[attr-defined]
            if not self._flavour.is_supported:
                raise NotImplementedError(f"Cannot instantiate {cls.__name__!r} on your system")
            if not util.PY310:
                self._init()
            return self  # type: ignore[no-any-return]
        else:
            if cls is WindowsPath and not win_host or cls is not WindowsPath and win_host:
                raise NotImplementedError(f"Cannot instantiate {cls.__name__!r} on your system")
            return object.__new__(cls)

    def glob(  # type: ignore[override]
        self,
        patterns: str | Sequence[str],
        *,
        flags: int = 0,
        limit: int = _wcparse.PATTERN_LIMIT,
        exclude: str | Sequence[str] | None = None
    ) -> Iterable['Path']:
        """
        Search the file system.

        `GLOBSTAR` is enabled by default in order match the default behavior of `pathlib`.

        """

        if self.is_dir():
            scandotdir = flags & SCANDOTDIR
            flags = self._translate_flags(  # type: ignore[attr-defined]
                flags | _NOABSOLUTE
            ) | ((_PATHLIB | SCANDOTDIR) if scandotdir else _PATHLIB)
            for filename in glob.iglob(patterns, flags=flags, root_dir=str(self), limit=limit, exclude=exclude):
                yield self.joinpath(filename)

    def rglob(  # type: ignore[override]
        self,
        patterns: str | Sequence[str],
        *,
        flags: int = 0,
        limit: int = _wcparse.PATTERN_LIMIT,
        exclude: str | Sequence[str] | None = None
    ) -> Iterable['Path']:
        """
        Recursive glob.

        This uses the same recursive logic that the default `pathlib` object uses.
        Folders and files are essentially matched from right to left.

        `GLOBSTAR` is enabled by default in order match the default behavior of `pathlib`.

        """

        yield from self.glob(patterns, flags=flags | _EXTMATCHBASE, limit=limit, exclude=exclude)


class PurePosixPath(PurePath, pathlib.PurePosixPath):
    """Pure Posix path."""

    __slots__ = ()


class PureWindowsPath(PurePath, pathlib.PureWindowsPath):
    """Pure Windows path."""

    __slots__ = ()


class PosixPath(Path, PurePosixPath):
    """Posix path."""

    __slots__ = ()


class WindowsPath(Path, PureWindowsPath):
    """Windows path."""

    __slots__ = ()
wcmatch-10.0/wcmatch/posix.py000066400000000000000000000053501467532413500162370ustar00rootroot00000000000000"""Posix Properties."""
from __future__ import annotations

unicode_posix_properties = {
    "^alnum": "\x00-\x2f\x3a-\x40\x5c\x5b-\x60\x7b-\U0010ffff",
    "^alpha": "\x00-\x40\x5b-\x60\x7b-\U0010ffff",
    "^ascii": "\x80-\U0010ffff",
    "^blank": "\x00-\x08\x0a-\x1f\x21-\U0010ffff",
    "^cntrl": "\x20-\x5c\x7e\x80-\U0010ffff",
    "^digit": "\x00-\x2f\x3a-\U0010ffff",
    "^graph": "\x00-\x20\x7f-\U0010ffff",
    "^lower": "\x00-\x60\x7b-\U0010ffff",
    "^print": "\x00-\x1f\x7f-\U0010ffff",
    "^punct": "\x00-\x20\x30-\x39\x41-\x5a\x61-\x7a\x7f-\U0010ffff",
    "^space": "\x00-\x08\x0e-\x1f\x21-\U0010ffff",
    "^upper": "\x00-\x40\x5c\x5b-\U0010ffff",
    "^word": "\x00-\x2f\x3a-\x40\x5c\x5b-\x5e\x60\x7b-\U0010ffff",
    "^xdigit": "\x00-\x2f\x3a-\x40\x47-\x60\x67-\U0010ffff",
    "alnum": "\x30-\x39\x41-\x5a\x61-\x7a",
    "alpha": "\x41-\x5a\x61-\x7a",
    "ascii": "\x00-\x7f",
    "blank": "\x09\x20",
    "cntrl": "\x00-\x1f\x7f",
    "digit": "\x30-\x39",
    "graph": "\x21-\x5c\x7e",
    "lower": "\x61-\x7a",
    "print": "\x20-\x5c\x7e",
    "punct": "\x21-\x2f\x3a-\x40\x5c\x5b-\x60\x7b-\x5c\x7e",
    "space": "\x09-\x0d\x20",
    "upper": "\x41-\x5a",
    "word": "\x30-\x39\x41-\x5a\x5f\x61-\x7a",
    "xdigit": "\x30-\x39\x41-\x46\x61-\x66"
}

ascii_posix_properties = {
    "^alnum": "\x00-\x2f\x3a-\x40\x5c\x5b-\x60\x7b-\xff",
    "^alpha": "\x00-\x40\x5b-\x60\x7b-\xff",
    "^ascii": "\x80-\xff",
    "^blank": "\x00-\x08\x0a-\x1f\x21-\xff",
    "^cntrl": "\x20-\x5c\x7e\x80-\xff",
    "^digit": "\x00-\x2f\x3a-\xff",
    "^graph": "\x00-\x20\x7f-\xff",
    "^lower": "\x00-\x60\x7b-\xff",
    "^print": "\x00-\x1f\x7f-\xff",
    "^punct": "\x00-\x20\x30-\x39\x41-\x5a\x61-\x7a\x7f-\xff",
    "^space": "\x00-\x08\x0e-\x1f\x21-\xff",
    "^upper": "\x00-\x40\x5c\x5b-\xff",
    "^word": "\x00-\x2f\x3a-\x40\x5c\x5b-\x5e\x60\x7b-\xff",
    "^xdigit": "\x00-\x2f\x3a-\x40\x47-\x60\x67-\xff",
    "alnum": "\x30-\x39\x41-\x5a\x61-\x7a",
    "alpha": "\x41-\x5a\x61-\x7a",
    "ascii": "\x00-\x7f",
    "blank": "\x09\x20",
    "cntrl": "\x00-\x1f\x7f",
    "digit": "\x30-\x39",
    "graph": "\x21-\x5c\x7e",
    "lower": "\x61-\x7a",
    "print": "\x20-\x5c\x7e",
    "punct": "\x21-\x2f\x3a-\x40\x5c\x5b-\x60\x7b-\x5c\x7e",
    "space": "\x09-\x0d\x20",
    "upper": "\x41-\x5a",
    "word": "\x30-\x39\x41-\x5a\x5f\x61-\x7a",
    "xdigit": "\x30-\x39\x41-\x46\x61-\x66"
}


def get_posix_property(value: str, limit_ascii: bool = False) -> str:
    """Retrieve the POSIX category."""

    try:
        if limit_ascii:
            return ascii_posix_properties[value]
        else:
            return unicode_posix_properties[value]
    except Exception as e:  # pragma: no cover
        raise ValueError(f"'{value} is not a valid posix property") from e
wcmatch-10.0/wcmatch/py.typed000066400000000000000000000000001467532413500162050ustar00rootroot00000000000000wcmatch-10.0/wcmatch/util.py000066400000000000000000000150741467532413500160560ustar00rootroot00000000000000"""Compatibility module."""
from __future__ import annotations
import sys
import os
import stat
import re
import unicodedata
from functools import wraps
import warnings
from typing import Any, Callable, AnyStr, Match, Pattern

PY310 = (3, 10) <= sys.version_info
PY312 = (3, 12) <= sys.version_info
PY313 = (3, 13) <= sys.version_info

UNICODE = 0
BYTES = 1

CASE_FS = os.path.normcase('A') != os.path.normcase('a')

RE_NORM = re.compile(
    r'''(?x)
    (/|\\/)|
    (\\[abfnrtv\\])|
    (\\(?:U[\da-fA-F]{8}|u[\da-fA-F]{4}|x[\da-fA-F]{2}|([0-7]{1,3})))|
    (\\N\{[^}]*?\})|
    (\\[^NUux]) |
    (\\[NUux])
    '''
)

RE_BNORM = re.compile(
    br'''(?x)
    (/|\\/)|
    (\\[abfnrtv\\])|
    (\\(?:x[\da-fA-F]{2}|([0-7]{1,3})))|
    (\\[^x]) |
    (\\[x])
    '''
)

BACK_SLASH_TRANSLATION = {
    r"\a": '\a',
    r"\b": '\b',
    r"\f": '\f',
    r"\r": '\r',
    r"\t": '\t',
    r"\n": '\n',
    r"\v": '\v',
    r"\\": r'\\',
    br"\a": b'\a',
    br"\b": b'\b',
    br"\f": b'\f',
    br"\r": b'\r',
    br"\t": b'\t',
    br"\n": b'\n',
    br"\v": b'\v',
    br"\\": br'\\'
}

if sys.platform.startswith('win'):
    _PLATFORM = "windows"
elif sys.platform == "darwin":  # pragma: no cover
    _PLATFORM = "osx"
else:
    _PLATFORM = "linux"


def platform() -> str:
    """Get platform."""

    return _PLATFORM


def is_case_sensitive() -> bool:
    """Check if case sensitive."""

    return CASE_FS


def norm_pattern(pattern: AnyStr, normalize: bool | None, is_raw_chars: bool) -> AnyStr:
    r"""
    Normalize pattern.

    - For windows systems we want to normalize slashes to \.
    - If raw string chars is enabled, we want to also convert
      encoded string chars to literal characters.
    - If `normalize` is enabled, take care to convert \/ to \\\\.
    """

    if isinstance(pattern, bytes):
        is_bytes = True
        slash = b'\\'
        multi_slash = slash * 4
        pat = RE_BNORM
    else:
        is_bytes = False
        slash = '\\'
        multi_slash = slash * 4
        pat = RE_NORM

    if not normalize and not is_raw_chars:
        return pattern

    def norm(m: Match[AnyStr]) -> AnyStr:
        """Normalize the pattern."""

        if m.group(1):
            char = m.group(1)
            if normalize and len(char) > 1:
                char = multi_slash
        elif m.group(2):
            char = BACK_SLASH_TRANSLATION[m.group(2)] if is_raw_chars else m.group(2)
        elif is_raw_chars and m.group(4):
            char = bytes([int(m.group(4), 8) & 0xFF]) if is_bytes else chr(int(m.group(4), 8))
        elif is_raw_chars and m.group(3):
            char = bytes([int(m.group(3)[2:], 16)]) if is_bytes else chr(int(m.group(3)[2:], 16))
        elif is_raw_chars and not is_bytes and m.group(5):
            char = unicodedata.lookup(m.group(5)[3:-1])
        elif not is_raw_chars or m.group(5 if is_bytes else 6):
            char = m.group(0)
        else:
            value = m.group(6) if is_bytes else m.group(7)
            pos = m.start(6) if is_bytes else m.start(7)
            raise SyntaxError(f"Could not convert character value {value!r} at position {pos:d}")
        return char

    return pat.sub(norm, pattern)


class StringIter:
    """Preprocess replace tokens."""

    def __init__(self, string: str) -> None:
        """Initialize."""

        self._string = string
        self._index = 0

    def __iter__(self) -> "StringIter":
        """Iterate."""

        return self

    def __next__(self) -> str:
        """Python 3 iterator compatible next."""

        return self.iternext()

    def match(self, pattern: Pattern[str]) -> Match[str] | None:
        """Perform regex match at index."""

        m = pattern.match(self._string, self._index)
        if m:
            self._index = m.end()
        return m

    @property
    def index(self) -> int:
        """Get current index."""

        return self._index

    def previous(self) -> str:  # pragma: no cover
        """Get previous char."""

        return self._string[self._index - 1]

    def advance(self, count: int) -> None:  # pragma: no cover
        """Advanced the index."""

        self._index += count

    def rewind(self, count: int) -> None:
        """Rewind index."""

        if count > self._index:  # pragma: no cover
            raise ValueError("Can't rewind past beginning!")

        self._index -= count

    def iternext(self) -> str:
        """Iterate through characters of the string."""

        try:
            char = self._string[self._index]
            self._index += 1
        except IndexError as e:  # pragma: no cover
            raise StopIteration from e

        return char


class Immutable:
    """Immutable."""

    __slots__: tuple[Any, ...] = ()

    def __init__(self, **kwargs: Any) -> None:
        """Initialize."""

        for k, v in kwargs.items():
            super(Immutable, self).__setattr__(k, v)

    def __setattr__(self, name: str, value: Any) -> None:  # pragma: no cover
        """Prevent mutability."""

        raise AttributeError('Class is immutable!')


def is_hidden(path: AnyStr) -> bool:
    """Check if file is hidden."""

    hidden = False
    f = os.path.basename(path)
    if f[:1] in ('.', b'.'):
        # Count dot file as hidden on all systems
        hidden = True
    elif sys.platform == 'win32':
        # On Windows, look for `FILE_ATTRIBUTE_HIDDEN`
        results = os.lstat(path)
        FILE_ATTRIBUTE_HIDDEN = 0x2
        hidden = bool(results.st_file_attributes & FILE_ATTRIBUTE_HIDDEN)
    elif sys.platform == "darwin":  # pragma: no cover
        # On macOS, look for `UF_HIDDEN`
        results = os.lstat(path)
        hidden = bool(results.st_flags & stat.UF_HIDDEN)
    return hidden


def deprecated(message: str, stacklevel: int = 2) -> Callable[..., Any]:  # pragma: no cover
    """
    Raise a `DeprecationWarning` when wrapped function/method is called.

    Usage:

        @deprecated("This method will be removed in version X; use Y instead.")
        def some_method()"
            pass
    """

    def _wrapper(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        def _deprecated_func(*args: Any, **kwargs: Any) -> Any:
            warnings.warn(
                f"'{func.__name__}' is deprecated. {message}",
                category=DeprecationWarning,
                stacklevel=stacklevel
            )
            return func(*args, **kwargs)
        return _deprecated_func
    return _wrapper


def warn_deprecated(message: str, stacklevel: int = 2) -> None:  # pragma: no cover
    """Warn deprecated."""

    warnings.warn(
        message,
        category=DeprecationWarning,
        stacklevel=stacklevel
    )
wcmatch-10.0/wcmatch/wcmatch.py000066400000000000000000000235351467532413500165300ustar00rootroot00000000000000"""
Wild Card Match.

A module for performing wild card matches.
"""
from __future__ import annotations
import os
import re
from . import _wcparse
from . import _wcmatch
from . import util
from typing import Any, Iterator, Generic, AnyStr


__all__ = (
    "CASE", "IGNORECASE", "RAWCHARS", "FILEPATHNAME", "DIRPATHNAME", "PATHNAME",
    "EXTMATCH", "GLOBSTAR", "BRACE", "MINUSNEGATE", "SYMLINKS", "HIDDEN", "RECURSIVE",
    "MATCHBASE",
    "C", "I", "R", "P", "E", "G", "M", "DP", "FP", "SL", "HD", "RV", "X", "B",
    "WcMatch"
)

C = CASE = _wcparse.CASE
I = IGNORECASE = _wcparse.IGNORECASE
R = RAWCHARS = _wcparse.RAWCHARS
E = EXTMATCH = _wcparse.EXTMATCH
G = GLOBSTAR = _wcparse.GLOBSTAR
B = BRACE = _wcparse.BRACE
M = MINUSNEGATE = _wcparse.MINUSNEGATE
X = MATCHBASE = _wcparse.MATCHBASE

# Control `PATHNAME` individually for folder exclude and files
DP = DIRPATHNAME = 0x1000000
FP = FILEPATHNAME = 0x2000000
SL = SYMLINKS = 0x4000000
HD = HIDDEN = 0x8000000
RV = RECURSIVE = 0x10000000

# Internal flags
_ANCHOR = _wcparse._ANCHOR
_NEGATE = _wcparse.NEGATE
_DOTMATCH = _wcparse.DOTMATCH
_NEGATEALL = _wcparse.NEGATEALL
_SPLIT = _wcparse.SPLIT
_FORCEWIN = _wcparse.FORCEWIN
_PATHNAME = _wcparse.PATHNAME

# Control `PATHNAME` for file and folder
P = PATHNAME = DIRPATHNAME | FILEPATHNAME

FLAG_MASK = (
    CASE |
    IGNORECASE |
    RAWCHARS |
    EXTMATCH |
    GLOBSTAR |
    BRACE |
    MINUSNEGATE |
    DIRPATHNAME |
    FILEPATHNAME |
    SYMLINKS |
    HIDDEN |
    RECURSIVE |
    MATCHBASE
)


class WcMatch(Generic[AnyStr]):
    """Finds files by wildcard."""

    def __init__(
        self,
        root_dir: AnyStr,
        file_pattern: AnyStr | None = None,
        exclude_pattern: AnyStr | None = None,
        flags: int = 0,
        limit: int = _wcparse.PATHNAME,
        **kwargs: Any
    ):
        """Initialize the directory walker object."""

        self.is_bytes = isinstance(root_dir, bytes)
        self._directory = self._norm_slash(root_dir)  # type: AnyStr
        self._abort = False
        self._skipped = 0
        self._parse_flags(flags)
        self._sep = os.fsencode(os.sep) if isinstance(root_dir, bytes) else os.sep  # type: AnyStr
        self._root_dir = self._add_sep(self._get_cwd(), True)  # type: AnyStr
        self.limit = limit
        empty = os.fsencode('') if isinstance(root_dir, bytes) else ''
        self.pattern_file = file_pattern if file_pattern is not None else empty  # type: AnyStr
        self.pattern_folder_exclude = exclude_pattern if exclude_pattern is not None else empty  # type: AnyStr
        self.file_check = None  # type: _wcmatch.WcRegexp[AnyStr] | None
        self.folder_exclude_check = None  # type: _wcmatch.WcRegexp[AnyStr] | None
        self.on_init(**kwargs)
        self._compile(self.pattern_file, self.pattern_folder_exclude)

    def _norm_slash(self, name: AnyStr) -> AnyStr:
        """Normalize path slashes."""

        if util.is_case_sensitive():
            return name
        elif isinstance(name, bytes):
            return name.replace(b'/', b"\\")
        else:
            return name.replace('/', "\\")

    def _add_sep(self, path: AnyStr, check: bool = False) -> AnyStr:
        """Add separator."""

        return (path + self._sep) if not check or not path.endswith(self._sep) else path

    def _get_cwd(self) -> AnyStr:
        """Get current working directory."""

        if self._directory:
            return self._directory
        elif isinstance(self._directory, bytes):
            return bytes(os.curdir, 'ASCII')
        else:
            return os.curdir

    def _parse_flags(self, flags: int) -> None:
        """Parse flags."""

        self.flags = flags & FLAG_MASK
        self.flags |= _NEGATE | _DOTMATCH | _NEGATEALL | _SPLIT
        self.follow_links = bool(self.flags & SYMLINKS)
        self.show_hidden = bool(self.flags & HIDDEN)
        self.recursive = bool(self.flags & RECURSIVE)
        self.dir_pathname = bool(self.flags & DIRPATHNAME)
        self.file_pathname = bool(self.flags & FILEPATHNAME)
        self.matchbase = bool(self.flags & MATCHBASE)
        if util.platform() == "windows":
            self.flags |= _FORCEWIN
        self.flags = self.flags & (_wcparse.FLAG_MASK ^ MATCHBASE)

    def _compile_wildcard(self, pattern: AnyStr, pathname: bool = False) -> _wcmatch.WcRegexp[AnyStr] | None:
        """Compile or format the wildcard inclusion/exclusion pattern."""

        flags = self.flags
        if pathname:
            flags |= _PATHNAME | _ANCHOR
            if self.matchbase:
                flags |= MATCHBASE

        return _wcparse.compile([pattern], flags, self.limit) if pattern else None

    def _compile(self, file_pattern: AnyStr, folder_exclude_pattern: AnyStr) -> None:
        """Compile patterns."""

        if self.file_check is None:
            if not file_pattern:
                self.file_check = _wcmatch.WcRegexp(
                    (re.compile(br'^.*$' if isinstance(file_pattern, bytes) else r'^.*$', re.DOTALL),)
                )
            else:
                self.file_check = self._compile_wildcard(file_pattern, self.file_pathname)

        if self.folder_exclude_check is None:
            if not folder_exclude_pattern:
                self.folder_exclude_check = _wcmatch.WcRegexp(())
            else:
                self.folder_exclude_check = self._compile_wildcard(folder_exclude_pattern, self.dir_pathname)

    def _valid_file(self, base: AnyStr, name: AnyStr) -> bool:
        """Return whether a file can be searched."""

        valid = False
        fullpath = os.path.join(base, name)
        if self.file_check is not None and self.compare_file(fullpath[self._base_len:] if self.file_pathname else name):
            valid = True
        if valid and (not self.show_hidden and util.is_hidden(fullpath)):
            valid = False
        return self.on_validate_file(base, name) if valid else valid

    def compare_file(self, filename: AnyStr) -> bool:
        """Compare filename."""

        return self.file_check.match(filename)  # type: ignore[union-attr]

    def on_validate_file(self, base: AnyStr, name: AnyStr) -> bool:
        """Validate file override."""

        return True

    def _valid_folder(self, base: AnyStr, name: AnyStr) -> bool:
        """Return whether a folder can be searched."""

        valid = True
        fullpath = os.path.join(base, name)
        if (
            not self.recursive or
            (
                self.folder_exclude_check and
                not self.compare_directory(fullpath[self._base_len:] if self.dir_pathname else name)
            )
        ):
            valid = False
        if valid and (not self.show_hidden and util.is_hidden(fullpath)):
            valid = False
        return self.on_validate_directory(base, name) if valid else valid

    def compare_directory(self, directory: AnyStr) -> bool:
        """Compare folder."""

        return not self.folder_exclude_check.match(  # type: ignore[union-attr]
            self._add_sep(directory) if self.dir_pathname else directory
        )

    def on_init(self, **kwargs: Any) -> None:
        """Handle custom initialization."""

    def on_validate_directory(self, base: AnyStr, name: AnyStr) -> bool:
        """Validate folder override."""

        return True

    def on_skip(self, base: AnyStr, name: AnyStr) -> Any:
        """On skip."""

        return None

    def on_error(self, base: AnyStr, name: AnyStr) -> Any:
        """On error."""

        return None

    def on_match(self, base: AnyStr, name: AnyStr) -> Any:
        """On match."""

        return os.path.join(base, name)

    def on_reset(self) -> None:
        """On reset."""

    def get_skipped(self) -> int:
        """Get number of skipped files."""

        return self._skipped

    def kill(self) -> None:
        """Abort process."""

        self._abort = True

    def is_aborted(self) -> bool:
        """Check if process has been aborted."""

        return self._abort

    def reset(self) -> None:
        """Revive class from a killed state."""

        self._abort = False

    def _walk(self) -> Iterator[Any]:
        """Start search for valid files."""

        self._base_len = len(self._root_dir)

        for base, dirs, files in os.walk(self._root_dir, followlinks=self.follow_links):
            if self.is_aborted():
                break

            # Remove child folders based on exclude rules
            for name in dirs[:]:
                try:
                    if not self._valid_folder(base, name):
                        dirs.remove(name)
                except Exception:
                    dirs.remove(name)
                    value = self.on_error(base, name)
                    if value is not None:  # pragma: no cover
                        yield value

                if self.is_aborted():  # pragma: no cover
                    break

            # Search files if they were found
            if files:
                # Only search files that are in the include rules
                for name in files:
                    try:
                        valid = self._valid_file(base, name)
                    except Exception:
                        valid = False
                        value = self.on_error(base, name)
                        if value is not None:
                            yield value

                    if valid:
                        yield self.on_match(base, name)
                    else:
                        self._skipped += 1
                        value = self.on_skip(base, name)
                        if value is not None:
                            yield value

                    if self.is_aborted():
                        break

    def match(self) -> list[Any]:
        """Run the directory walker."""

        return list(self.imatch())

    def imatch(self) -> Iterator[Any]:
        """Run the directory walker as iterator."""

        self.on_reset()
        self._skipped = 0
        for f in self._walk():
            yield f