Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-wcmatch for openSUSE:Factory checked in at 2025-07-09 17:29:39 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-wcmatch (Old) and /work/SRC/openSUSE:Factory/.python-wcmatch.new.7373 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-wcmatch" Wed Jul 9 17:29:39 2025 rev:8 rq:1291495 version:10.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-wcmatch/python-wcmatch.changes 2024-11-20 17:44:17.975456285 +0100 +++ /work/SRC/openSUSE:Factory/.python-wcmatch.new.7373/python-wcmatch.changes 2025-07-09 17:30:23.552516452 +0200 @@ -1,0 +2,9 @@ +Wed Jul 2 12:16:23 UTC 2025 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to 10.1 + * NEW: Drop support for Python 3.8 which is "end of life". + * NEW: Add support for Python 3.14. + * NEW: Add wcmatch.glob.compile(pattern) and wcmatch.fnmatch.compile(pattern) + to allow for precompiled matcher objects that can be reused. + +------------------------------------------------------------------- Old: ---- wcmatch-10.0.tar.gz New: ---- wcmatch-10.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-wcmatch.spec ++++++ --- /var/tmp/diff_new_pack.duekD9/_old 2025-07-09 17:30:25.320590064 +0200 +++ /var/tmp/diff_new_pack.duekD9/_new 2025-07-09 17:30:25.344591063 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-wcmatch # -# Copyright (c) 2024 SUSE LLC +# Copyright (c) 2025 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %{?sle15_python_module_pythons} %{?python_enable_dependency_generator} Name: python-wcmatch -Version: 10.0 +Version: 10.1 Release: 0 Summary: Wildcard/glob file name matcher License: MIT ++++++ wcmatch-10.0.tar.gz -> wcmatch-10.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/.pyspelling.yml new/wcmatch-10.1/.pyspelling.yml --- old/wcmatch-10.0/.pyspelling.yml 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/.pyspelling.yml 2020-02-02 01:00:00.000000000 +0100 @@ -1,3 +1,5 @@ +jobs: 8 + matrix: - name: mkdocs sources: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/LICENSE.md new/wcmatch-10.1/LICENSE.md --- old/wcmatch-10.0/LICENSE.md 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/LICENSE.md 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 - 2024 Isaac Muse +Copyright (c) 2018 - 2025 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/PKG-INFO new/wcmatch-10.1/PKG-INFO --- old/wcmatch-10.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/PKG-INFO 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ -Metadata-Version: 2.3 +Metadata-Version: 2.4 Name: wcmatch -Version: 10.0 +Version: 10.1 Summary: Wildcard/glob file name matcher. Project-URL: Homepage, https://github.com/facelessuser/wcmatch Author-email: Isaac Muse <isaac.m...@gmail.com> @@ -13,15 +13,15 @@ Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Typing :: Typed -Requires-Python: >=3.8 +Requires-Python: >=3.9 Requires-Dist: bracex>=2.1.1 Description-Content-Type: text/markdown @@ -31,7 +31,7 @@ [![PyPI Version][pypi-image]][pypi-link] [![PyPI Downloads][pypi-down]][pypi-link] [![PyPI - Python Version][python-image]][pypi-link] -![License][license-image-mit] +[![License][license-image-mit]][license-link] # Wildcard Match ## Overview @@ -88,7 +88,7 @@ MIT -[github-ci-image]: https://github.com/facelessuser/wcmatch/workflows/build/badge.svg?branch=main&event=push +[github-ci-image]: https://github.com/facelessuser/wcmatch/workflows/build/badge.svg [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 @@ -97,5 +97,6 @@ [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 +[license-link]: https://github.com/facelessuser/wcmatch/blob/main/LICENSE.md [donate-image]: https://img.shields.io/badge/Donate-PayPal-3fabd1?logo=paypal [donate-link]: https://www.paypal.me/facelessuser diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/README.md new/wcmatch-10.1/README.md --- old/wcmatch-10.0/README.md 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/README.md 2020-02-02 01:00:00.000000000 +0100 @@ -4,7 +4,7 @@ [![PyPI Version][pypi-image]][pypi-link] [![PyPI Downloads][pypi-down]][pypi-link] [![PyPI - Python Version][python-image]][pypi-link] -![License][license-image-mit] +[![License][license-image-mit]][license-link] # Wildcard Match ## Overview @@ -61,7 +61,7 @@ MIT -[github-ci-image]: https://github.com/facelessuser/wcmatch/workflows/build/badge.svg?branch=main&event=push +[github-ci-image]: https://github.com/facelessuser/wcmatch/workflows/build/badge.svg [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 @@ -70,5 +70,6 @@ [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 +[license-link]: https://github.com/facelessuser/wcmatch/blob/main/LICENSE.md [donate-image]: https://img.shields.io/badge/Donate-PayPal-3fabd1?logo=paypal [donate-link]: https://www.paypal.me/facelessuser diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/docs/src/markdown/about/changelog.md new/wcmatch-10.1/docs/src/markdown/about/changelog.md --- old/wcmatch-10.0/docs/src/markdown/about/changelog.md 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/docs/src/markdown/about/changelog.md 2020-02-02 01:00:00.000000000 +0100 @@ -1,9 +1,16 @@ # Changelog +## 10.1 + +- **NEW**: Drop support for Python 3.8 which is "end of life". +- **NEW**: Add support for Python 3.14. +- **NEW**: Add `wcmatch.glob.compile(pattern)` and `wcmatch.fnmatch.compile(pattern)` to allow for precompiled matcher + objects that can be reused. + ## 10.0 - **NEW**: Added `GLOBSTARLONG` which adds support for the Zsh style `***` which acts like `**` with `GLOBSTAR` but - but traverses symlinks. + 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 @@ -80,7 +87,7 @@ - **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`. + `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. @@ -124,7 +131,7 @@ ## 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 issue where an extended pattern can't follow right behind an inverse extended pattern. - **FIX**: Fix issues related to nested inverse glob patterns. ## 7.0 @@ -280,8 +287,8 @@ - **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 `MATCHBASE` that causes 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/docs/src/markdown/about/release.md new/wcmatch-10.1/docs/src/markdown/about/release.md --- old/wcmatch-10.0/docs/src/markdown/about/release.md 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/docs/src/markdown/about/release.md 2020-02-02 01:00:00.000000000 +0100 @@ -13,7 +13,7 @@ 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 +```py # Excplicitly named def on_init(self, key1=value, key2=value): @@ -23,7 +23,7 @@ Lastly, only pass your custom variables in as keyword arguments: -```py3 +```py CustomWcmatch('.', '*.md|*.txt', flags=wcmatch.RECURSIVE, custom_key=value) ``` @@ -101,7 +101,7 @@ [] ``` -If we want to modify the pattern matcher, and not just the the directory scanner, we can use the flag +If we want to modify the pattern matcher, and not just the directory scanner, we can use the flag [`NODITDIR`](../glob.md#nodotdir). ```pycon3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/docs/src/markdown/fnmatch.md new/wcmatch-10.1/docs/src/markdown/fnmatch.md --- old/wcmatch-10.0/docs/src/markdown/fnmatch.md 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/docs/src/markdown/fnmatch.md 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ # `wcmatch.fnmatch` -```py3 +```py from wcmatch import fnmatch ``` @@ -13,7 +13,7 @@ /// 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'`. +character `#!py r'\?'`. If you want to represent a literal backslash, you must use two: `#!py r'some\\path'`. /// Pattern | Meaning @@ -51,11 +51,30 @@ The imposed pattern limit and corresponding `limit` option was introduced in 6.0. /// +## Precompiling + +While patterns are often cached, auto expanding patterns, such as `'file{a, b, c}'` will have each individual +permutation cached (up to the cache limit), but not the entire pattern. This is to prevent the cache from exploding with +really large patterns such as `{1..100}`. Essentially, individual patterns are cached, but not the expansion of a +pattern into many patterns. + +If it is planned to reuse a pattern and the performance hit of recompiling is not desired, you can precompile a matcher +object via [`fnmatch.compile`](#compile) which returns a [`WcMatcher`](#wcmatcher) object. + +```py +>>> import wcmatch.fnmatch as fnmatch +>>> m = fnmatch.compile('*.md') +>>> m.match('README.md') +True +>>> m.filter(['test.txt', 'file.md', 'README.md']) +['file.md', 'README.md'] +``` + ## API #### `fnmatch.fnmatch` {: #fnmatch} -```py3 +```py def fnmatch(filename, patterns, *, flags=0, limit=1000, exclude=None) ``` @@ -128,7 +147,7 @@ #### `fnmatch.filter` {: #filter} -```py3 +```py def filter(filenames, patterns, *, flags=0, limit=1000, exclude=None): ``` @@ -151,9 +170,60 @@ `exclude` parameter was added. /// +#### `fnmatch.compile` {: #compile} + +```py +def compile(patterns, *, flags=0, limit=1000, exclude=None): +``` + +The `compile` function 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 a [`WcMatcher`](#wcmatcher) object which can match or filter file paths depending on +which method is called. + +```pycon3 +>>> import wcmatch.fnmatch as fnmatch +>>> m = fnmatch.compile('*.md') +>>> m.match('README.md') +True +>>> m.filter(['test.txt', 'file.md', 'README.md']) +['file.md', 'README.md'] +``` + +#### `fnmatch.WcMatcher` {: #wcmatcher} + +The `WcMatcher` class is returned when a pattern is precompiled with [`compile`](#compile). It has two methods: `match` +and `filter`. + +```py +def match(self, filename): +``` + +This `match` method allows for matching against a precompiled pattern. + +```pycon3 +>>> import wcmatch.fnmatch as fnmatch +>>> m = fnmatch.compile('*.md') +>>> m.match('README.md') +True +``` + +```py +def filter(self, filenames): +``` + +The `filter` method allows for filtering paths against a precompiled pattern. + +```pycon3 +>>> import wcmatch.fnmatch as fnmatch +>>> m = fnmatch.compile('*.md') +>>> m.filter(['test.txt', 'file.md', 'README.md']) +['file.md', 'README.md'] +``` + #### `fnmatch.translate` {: #translate} -```py3 +```py def translate(patterns, *, flags=0, limit=1000, exclude=None): ``` @@ -173,8 +243,8 @@ 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)+)'`. +While in regex patterns like `#!py r'(a)+'` would capture only the last character, even though multiple where matched, +we wrap the entire group to be captured: `#!py '+(a)'` --> `#!py r'((a)+)'`. ```pycon3 >>> from wcmatch import fnmatch @@ -199,7 +269,7 @@ #### `fnmatch.escape` {: #escape} -```py3 +```py def escape(pattern): ``` @@ -220,7 +290,7 @@ ### `fnmatch.is_magic` {: #is_magic} -```py3 +```py def is_magic(pattern, *, flags=0): """Check if the pattern is likely to be magic.""" ``` @@ -267,8 +337,8 @@ #### `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}'`. +`RAWCHARS` causes string character syntax to be parsed in raw strings: `#!py r'\u0040'` --> `#!py r'@'`. This will +handle standard string escapes and Unicode including `#!py r'\N{CHAR NAME}'`. #### `fnmatch.NEGATE, fnmatch.N` {: #negate} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/docs/src/markdown/glob.md new/wcmatch-10.1/docs/src/markdown/glob.md --- old/wcmatch-10.0/docs/src/markdown/glob.md 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/docs/src/markdown/glob.md 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ # `wcmatch.glob` -```py3 +```py from wcmatch import glob ``` @@ -13,7 +13,7 @@ /// 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'`. +character `#!py r'\?'`. If you want to represent a literal backslash, you must use two: `#!py r'some\\path'`. /// Pattern | Meaning @@ -184,11 +184,30 @@ The imposed pattern limit and corresponding `limit` option was introduced in 6.0. /// +## Precompiling + +While patterns are often cached, auto expanding patterns, such as `'file{a, b, c}'` will have each individual +permutation cached (up to the cache limit), but not the entire pattern. This is to prevent the cache from exploding with +really large patterns such as `{1..100}`. Essentially, individual patterns are cached, but not the expansion of a +pattern into many patterns. + +If it is planned to reuse a pattern and the performance hit of recompiling is not desired, you can precompile a matcher +object via [`glob.compile`](#compile) which returns a [`WcMatcher`](#wcmatcher) object. + +```py +>>> import wcmatch.glob as glob +>>> m = glob.compile('**/*.py', flags=glob.GLOBSTAR) +>>> m.match('wcmatch/__init__.py') +True +>>> m.filter(['wcmatch/__init__.py', 'wcmatch/glob.py', 'README.md']) +['wcmatch/__init__.py', 'wcmatch/glob.py'] +``` + ## API #### `glob.glob` {: #glob} -```py3 +```py def glob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, exclude=None): ``` @@ -286,7 +305,7 @@ ``` 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 +`#!py 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 @@ -309,10 +328,10 @@ /// 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. +your platform referencing the attribute `#!py glob.SUPPORT_DIR_FD` which will be `#!py 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`. +Additionally, the `#!py os.O_DIRECTORY` may not be defined on some systems. You can likely just use +`#!py os.O_RDONLY`. /// /// new | New 5.1 @@ -333,7 +352,7 @@ #### `glob.iglob` {: #iglob} -```py3 +```py def iglob(patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, exclude=None): ``` @@ -363,7 +382,7 @@ #### `glob.globmatch` {: #globmatch} -```py3 +```py def globmatch(filename, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, exclude=None): ``` @@ -482,10 +501,10 @@ /// 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. +your platform referencing the attribute `#!py glob.SUPPORT_DIR_FD` which will be `#!py 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`. +Additionally, the `#!py os.O_DIRECTORY` may not be defined on some systems. You can likely just use +`#!py os.O_RDONLY`. /// /// new | New 5.1 @@ -507,7 +526,7 @@ #### `glob.globfilter` {: #globfilter} -```py3 +```py def globfilter(filenames, patterns, *, flags=0, root_dir=None, dir_fd=None, limit=1000, method=None): ``` @@ -551,7 +570,7 @@ #### `glob.translate` {: #translate} -```py3 +```py def translate(patterns, *, flags=0, limit=1000, exclude=None): ``` @@ -571,8 +590,8 @@ 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)+)'`. +While in regex patterns like `#!py r'(a)+'` would capture only the last character, even though multiple where matched, +we wrap the entire group to be captured: `#!py '+(a)'` --> `#!py r'((a)+)'`. ```pycon3 >>> from wcmatch import glob @@ -595,9 +614,60 @@ `exclude` parameter was added. /// +#### `glob.compile` {: #compile} + +```py +def compile(patterns, *, flags=0, limit=1000, exclude=None): +``` + +The `compile` function 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 a [`WcMatcher`](#wcmatcher) object which can match or filter file paths depending on +which method is called. + +```pycon3 +>>> import wcmatch.glob as glob +>>> m = glob.compile('**/*.py', flags=glob.GLOBSTAR) +>>> m.match('wcmatch/__init__.py') +True +>>> m.filter(['wcmatch/__init__.py', 'wcmatch/glob.py', 'README.md']) +['wcmatch/__init__.py', 'wcmatch/glob.py'] +``` + +#### `glob.WcMatcher` {: #wcmatcher} + +The `WcMatcher` class is returned when a pattern is precompiled with [`compile`](#compile). It has two methods: `match` +and `filter`. + +```py +def match(self, filename, *, root_dir=None, dir_fd=None): +``` + +This `match` method allows for matching against a precompiled pattern. + +```pycon3 +>>> import wcmatch.glob as glob +>>> m = glob.compile('**/*.py', flags=glob.GLOBSTAR) +>>> m.match('wcmatch/__init__.py') +True +``` + +```py +def filter(self, filenames, *, root_dir=None, dir_fd=None): +``` + +The `filter` method allows for filtering paths against a precompiled pattern. + +```pycon3 +>>> import wcmatch.glob as glob +>>> m = glob.compile('**/*.py', flags=glob.GLOBSTAR) +>>> m.filter(['wcmatch/__init__.py', 'wcmatch/glob.py', 'README.md']) +['wcmatch/__init__.py', 'wcmatch/glob.py'] +``` + #### `glob.escape` {: #escape} -```py3 +```py def escape(pattern, unix=None): ``` @@ -615,7 +685,7 @@ `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 '\\\\'`). +(represented in a raw string as `#!py r'\\'` or a normal string as `#!py '\\\\'`). ```pycon3 >>> from wmcatch import glob @@ -663,7 +733,7 @@ ### `glob.is_magic` {: #is_magic} -```py3 +```py def is_magic(pattern, *, flags=0): """Check if the pattern is likely to be magic.""" ``` @@ -729,8 +799,8 @@ #### `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}'`. +`RAWCHARS` causes string character syntax to be parsed in raw strings: `#!py r'\u0040'` --> `#!py r'@'`. This will +handle standard string escapes and Unicode including `#!py r'\N{CHAR NAME}'`. #### `glob.NEGATE, glob.N` {: #negate} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/docs/src/markdown/pathlib.md new/wcmatch-10.1/docs/src/markdown/pathlib.md --- old/wcmatch-10.0/docs/src/markdown/pathlib.md 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/docs/src/markdown/pathlib.md 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ # `wcmatch.pathlib` -```py3 +```py from wcmatch import pathlib ``` @@ -212,7 +212,7 @@ #### `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 +have the ease that `pathlib` offers when working with a path and be able to manipulate or gain access 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). @@ -227,7 +227,7 @@ #### `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 +have the ease that `pathlib` offers when working with a path and be able to manipulate or gain access 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). @@ -243,7 +243,7 @@ #### `PurePath.match` {: #match} -```py3 +```py def match(self, patterns, *, flags=0, limit=1000, exclude=None): ``` @@ -282,7 +282,7 @@ #### `PurePath.globmatch` {: #globmatch} -```py3 +```py def globmatch(self, patterns, *, flags=0, limit=1000, exclude=None): ``` @@ -321,7 +321,7 @@ /// new | new 10.0 /// -```py3 +```py def full_match(self, patterns, *, flags=0, limit=1000, exclude=None): ``` @@ -331,7 +331,7 @@ #### `Path.glob` {: #glob} -```py3 +```py def glob(self, patterns, *, flags=0, limit=1000, exclude=None): ``` @@ -366,7 +366,7 @@ #### `Path.rglob` {: #rglob} -```py3 +```py def rglob(self, patterns, *, flags=0, path_limit=1000, exclude=None): ``` @@ -414,8 +414,8 @@ #### `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}'`. +`RAWCHARS` causes string character syntax to be parsed in raw strings: `#!py r'\u0040'` --> `#!py r'@'`. This will +handle standard string escapes and Unicode including `#!py r'\N{CHAR NAME}'`. #### `pathlib.NEGATE, pathlib.N` {: #negate} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/docs/src/markdown/wcmatch.md new/wcmatch-10.1/docs/src/markdown/wcmatch.md --- old/wcmatch-10.0/docs/src/markdown/wcmatch.md 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/docs/src/markdown/wcmatch.md 2020-02-02 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ # `wcmatch.wcmatch` -```py3 +```py from wcmatch import wcmatch ``` @@ -16,7 +16,7 @@ ## `wcmatch.WcMatch` {: #wcmatch} -```py3 +```py class WcMatch: """Finds files by wildcard.""" @@ -32,10 +32,10 @@ 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). +`file_pattern` | `#!py ''` | 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` | `#!py ''` | Zero or more folder exclude patterns separated by `|`. You can define exceptions by starting a pattern with `!` (or `-` if [`MINUSNEGATE`](#minusnegate) is set). +`flags` | `#!py 0` | Flags to alter behavior of folder and file matching. See [Flags](#flags) for more info. +`limit` | `#!py 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 @@ -210,7 +210,7 @@ #### `WcMatch.on_init` {: #on_init} -```py3 +```py def on_init(self, **kwargs): """Handle custom init.""" ``` @@ -225,7 +225,7 @@ #### `WcMatch.on_validate_directory` {: #on_validate_directory} -```py3 +```py def on_validate_directory(self, base, name): """Validate folder override.""" @@ -237,7 +237,7 @@ #### `WcMatch.on_validate_file` {: #on_validate_file} -```py3 +```py def on_validate_file(self, base, name): """Validate file override.""" @@ -249,7 +249,7 @@ #### `WcMatch.on_skip` {: #on_skip} -```py3 +```py def on_skip(self, base, name): """On skip.""" @@ -263,7 +263,7 @@ #### `WcMatch.on_error` {: #on_error} -```py3 +```py def on_error(self, base, name): """On error.""" @@ -276,7 +276,7 @@ #### `WcMatch.on_match` {: #on_match} -```py3 +```py def on_match(self, base, name): """On match.""" @@ -290,7 +290,7 @@ #### `WcMatch.on_reset` {: #on_reset} -```py3 +```py def on_reset(self): """On reset.""" pass @@ -325,8 +325,8 @@ #### `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}'`). +`RAWCHARS` causes string character syntax to be parsed in raw strings: `#!py r'\u0040'` --> `#!py r'@'`. This will +handle standard string escapes and Unicode (including `#!py r'\N{CHAR NAME}'`). #### `wcmatch.EXTMATCH, wcmatch.E` {: #extmatch} @@ -355,9 +355,9 @@ 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: +2. Splitting patterns with `|` is built into [`WcMatch`](#wcmatch_1). `BRACE` 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) @@ -409,11 +409,10 @@ #### `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 [`FILEPATHNAME`](#filepathname) or [`DIRPATHNAME`](#dirpathname) is enabled, `MATCHBASE` will ensure 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: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/hatch_build.py new/wcmatch-10.1/hatch_build.py --- old/wcmatch-10.0/hatch_build.py 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/hatch_build.py 2020-02-02 01:00:00.000000000 +0100 @@ -28,12 +28,12 @@ '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', + 'Programming Language :: Python :: 3.14', 'Topic :: Software Development :: Libraries :: Python Modules', 'Typing :: Typed' ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/mkdocs.yml new/wcmatch-10.1/mkdocs.yml --- old/wcmatch-10.0/mkdocs.yml 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/mkdocs.yml 2020-02-02 01:00:00.000000000 +0100 @@ -4,7 +4,7 @@ edit_uri: tree/main/docs/src/markdown site_description: A wildcard file name matching library copyright: | - Copyright © 2014 - 2024 <a href="https://github.com/facelessuser" target="_blank" rel="noopener">Isaac Muse</a> + Copyright © 2014 - 2025 <a href="https://github.com/facelessuser" target="_blank" rel="noopener">Isaac Muse</a> docs_dir: docs/src/markdown theme: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/pyproject.toml new/wcmatch-10.1/pyproject.toml --- old/wcmatch-10.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/pyproject.toml 2020-02-02 01:00:00.000000000 +0100 @@ -9,7 +9,7 @@ description = "Wildcard/glob file name matcher." readme = "README.md" license = "MIT" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ { name = "Isaac Muse", email = "isaac.m...@gmail.com" }, ] @@ -102,6 +102,8 @@ "RUF012", "RUF005", "PGH004", + "RUF022", + "RUF023", "RUF100" ] @@ -111,7 +113,7 @@ isolated_build = true skipsdist=true envlist= - py38,py39,py310,py311,py312,py313, + py39,py310,py311,py312,py313,py314, lint [testenv] @@ -137,7 +139,7 @@ -r requirements/docs.txt commands= {envpython} -m mkdocs build --clean --verbose --strict - {envbindir}/pyspelling + {envbindir}/pyspelling -j 8 [pytest] addopts=-p no:warnings diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/tests/test_fnmatch.py new/wcmatch-10.1/tests/test_fnmatch.py --- old/wcmatch-10.0/tests/test_fnmatch.py 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/tests/test_fnmatch.py 2020-02-02 01:00:00.000000000 +0100 @@ -5,6 +5,7 @@ import sys import os import pytest +import copy import wcmatch.fnmatch as fnmatch from unittest import mock from wcmatch import util @@ -776,3 +777,47 @@ """Test `list` matching.""" self.assertTrue(fnmatch.fnmatch('a', ['a'])) + + +class TestPrecompile(unittest.TestCase): + """Test precompiled match objects.""" + + def test_precompiled_match(self): + """Test precompiled matching.""" + + m = fnmatch.compile('*file') + self.assertTrue(m.match('testfile')) + + def test_precompiled_match_empty(self): + """Test precompiled matching with empty input.""" + + m = fnmatch.compile('*file') + self.assertFalse(m.match('')) + + def test_precompiled_filter(self): + """Test precompiled filtering.""" + + m = fnmatch.compile('*file') + self.assertEqual(m.filter(['testfile', 'test_2_file', 'nope']), ['testfile', 'test_2_file']) + + def test_precompiled_filter_empty(self): + """Test precompiled filtering with empty input.""" + + m = fnmatch.compile('*file') + self.assertEqual(m.filter([]), []) + + def test_hash(self): + """Test hashing.""" + + m1 = fnmatch.compile('test', flags=fnmatch.C) + m2 = fnmatch.compile('test', flags=fnmatch.C) + m3 = fnmatch.compile('test', flags=fnmatch.I) + m4 = fnmatch.compile(b'test', flags=fnmatch.C) + + self.assertTrue(m1 == m2) + self.assertTrue(m1 != m3) + self.assertTrue(m1 != m4) + + m5 = copy.copy(m1) + self.assertTrue(m1 == m5) + self.assertTrue(m5 in {m1}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/tests/test_glob.py new/wcmatch-10.1/tests/test_glob.py --- old/wcmatch-10.0/tests/test_glob.py 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/tests/test_glob.py 2020-02-02 01:00:00.000000000 +0100 @@ -531,17 +531,13 @@ Options(default_negate='sym3/EF'), [ ('**', 'EF'), - [ - ] if not can_symlink() else [ - ], + [], glob.N | glob.L ], [ ('**', 'EF'), - [ - ] if not can_symlink() else [ - ], + [], glob.N ], @@ -953,9 +949,7 @@ Options(just_negative=True, default_negate='**'), [ ('a*', '**'), - [ - ] if not can_symlink() else [ - ], + [], glob.N ], Options(just_negative=False, cwd_temp=False, absolute=False), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/tests/test_globmatch.py new/wcmatch-10.1/tests/test_globmatch.py --- old/wcmatch-10.0/tests/test_globmatch.py 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/tests/test_globmatch.py 2020-02-02 01:00:00.000000000 +0100 @@ -2,6 +2,7 @@ """Tests for `globmatch`.""" import unittest import pytest +import copy import re import os import sys @@ -9,6 +10,7 @@ import wcmatch._wcparse as _wcparse import wcmatch.util as util import shutil +import pathlib # Below is general helper stuff that Python uses in `unittests`. As these # not meant for users, and could change without notice, include them @@ -1921,3 +1923,56 @@ """Test exclusion with filter.""" self.assertEqual(glob.globfilter(['path/name', 'path/test'], '*/*', exclude='path/test'), ['path/name']) + + +class TestPrecompile(unittest.TestCase): + """Test precompiled match objects.""" + + def test_precompiled_match(self): + """Test precompiled matching.""" + + m = glob.compile('**/file', flags=glob.GLOBSTAR) + self.assertTrue(m.match('some/path/file')) + + def test_precompiled_filter(self): + """Test precompiled filtering.""" + + pattern = '**/file' + m = glob.compile(pattern, flags=glob.GLOBSTAR) + self.assertEqual( + m.filter(['test/file', 'file', 'nope']), + glob.globfilter(['test/file', 'file', 'nope'], pattern, flags=glob.GLOBSTAR) + ) + + def test_precompiled_match_pathlib(self): + """Test precompiled matching.""" + + m = glob.compile('**/file', flags=glob.GLOBSTAR) + self.assertTrue(m.match(pathlib.PurePath('some/path/file'))) + + def test_precompiled_filter_pathlib(self): + """Test precompiled filtering.""" + + pattern = '**/file' + m = glob.compile(pattern, flags=glob.GLOBSTAR) + paths = [pathlib.PurePath('test/file'), pathlib.PurePath('file'), pathlib.PurePath('nope')] + self.assertEqual( + m.filter(paths), + glob.globfilter(paths, pattern, flags=glob.GLOBSTAR) + ) + + def test_hash(self): + """Test hashing.""" + + m1 = glob.compile('test', flags=glob.C) + m2 = glob.compile('test', flags=glob.C) + m3 = glob.compile('test', flags=glob.I) + m4 = glob.compile(b'test', flags=glob.C) + + self.assertTrue(m1 == m2) + self.assertTrue(m1 != m3) + self.assertTrue(m1 != m4) + + m5 = copy.copy(m1) + self.assertTrue(m1 == m5) + self.assertTrue(m5 in {m1}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/wcmatch/__meta__.py new/wcmatch-10.1/wcmatch/__meta__.py --- old/wcmatch-10.0/wcmatch/__meta__.py 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/wcmatch/__meta__.py 2020-02-02 01:00:00.000000000 +0100 @@ -193,5 +193,5 @@ return Version(major, minor, micro, release, pre, post, dev) -__version_info__ = Version(10, 0, 0, "final") +__version_info__ = Version(10, 1, 0, "final") __version__ = __version_info__._get_canonical() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/wcmatch/_wcmatch.py new/wcmatch-10.1/wcmatch/_wcmatch.py --- old/wcmatch-10.0/wcmatch/_wcmatch.py 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/wcmatch/_wcmatch.py 2020-02-02 01:00:00.000000000 +0100 @@ -5,7 +5,7 @@ import stat import copyreg from . import util -from typing import Pattern, AnyStr, Generic, Any +from typing import Pattern, AnyStr, Generic, Iterable, Any # `O_DIRECTORY` may not always be defined DIR_FLAGS = os.O_RDONLY | getattr(os, 'O_DIRECTORY', 0) @@ -320,24 +320,100 @@ self._follow != other._follow ) - def match(self, filename: AnyStr, root_dir: AnyStr | None = None, dir_fd: int | None = None) -> bool: - """Match filename.""" + def match( + self, + filename: AnyStr | os.PathLike[AnyStr], + root_dir: AnyStr | os.PathLike[AnyStr] | None = None, + dir_fd: int | None = None + ) -> bool: + """Filter filenames.""" + + if not filename: + return False return _Match( - filename, + os.fspath(filename), self._include, self._exclude, self._real, self._path, self._follow ).match( - root_dir=root_dir, + root_dir=os.fspath(root_dir) if root_dir is not None else None, dir_fd=dir_fd ) + def filter( + self, + filenames: Iterable[AnyStr | os.PathLike[AnyStr]], + root_dir: AnyStr | os.PathLike[AnyStr] | None = None, + dir_fd: int | None = None + ) -> list[AnyStr | os.PathLike[AnyStr]]: + """Filter filenames.""" + + if not filenames: + return [] + + rdir = os.fspath(root_dir) if root_dir is not None else None + matches = [ + filename + for filename in filenames + if _Match( + os.fspath(filename), + self._include, + self._exclude, + self._real, + self._path, + self._follow + ).match( + root_dir=rdir, + dir_fd=dir_fd + ) + ] + return matches + + +class WcMatcher(util.Immutable, Generic[AnyStr]): + """Pre-compiled matcher object.""" + + _matcher: WcRegexp[AnyStr] + _hash: int -def _pickle(p): # type: ignore[no-untyped-def] - return WcRegexp, (p._include, p._exclude, p._real, p._path, p._follow) + __slots__ = ('_matcher', '_hash') + + def __init__(self, matcher: WcRegexp[AnyStr]) -> None: + """Initialize.""" + + super().__init__( + _matcher=matcher, + _hash=hash( + ( + type(self), + type(matcher), matcher, + ) + ) + ) + + def __hash__(self) -> int: + """Hash.""" + + return self._hash + + def __eq__(self, other: Any) -> bool: + """Equal.""" + + return ( + isinstance(other, WcMatcher) and + self._matcher == other._matcher + ) + + def __ne__(self, other: Any) -> bool: + """Equal.""" + + return ( + not isinstance(other, WcMatcher) or + self._matcher != other._matcher + ) -copyreg.pickle(WcRegexp, _pickle) +copyreg.pickle(WcRegexp, lambda p: (WcRegexp, (p._include, p._exclude, p._real, p._path, p._follow))) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/wcmatch/fnmatch.py new/wcmatch-10.1/wcmatch/fnmatch.py --- old/wcmatch-10.0/wcmatch/fnmatch.py 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/wcmatch/fnmatch.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,10 +1,13 @@ +# noqa: A005 """ Wild Card Match. A custom implementation of `fnmatch`. """ from __future__ import annotations +from . import _wcmatch from . import _wcparse +import copyreg from typing import AnyStr, Iterable, Sequence __all__ = ( @@ -12,7 +15,8 @@ "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" + "translate", "fnmatch", "filter", "escape", "is_magic", "compile", + "WcMatcher" ) C = CASE = _wcparse.CASE @@ -44,6 +48,34 @@ ) +class WcMatcher(_wcmatch.WcMatcher[AnyStr]): + """Pre-compiled matcher object.""" + + def match(self, filename: AnyStr) -> bool: + """Match filename.""" + + return self._matcher.match(filename) + + def filter(self, filenames: Iterable[AnyStr]) -> list[AnyStr]: + """Match filename.""" + + return self._matcher.filter(filenames) # type: ignore[return-value] + + +copyreg.pickle(WcMatcher, lambda p: (WcMatcher, (p._matcher,))) + + +def compile( # noqa: A001 + patterns: AnyStr | Sequence[AnyStr], + flags: int = 0, + limit: int = _wcparse.PATTERN_LIMIT, + exclude: AnyStr | Sequence[AnyStr] | None = None +) -> WcMatcher[AnyStr]: + """Pre-compile a matcher object.""" + + return WcMatcher(_wcparse.compile(patterns, _flag_transform(flags), limit, exclude=exclude)) + + def _flag_transform(flags: int) -> int: """Transform flags to glob defaults.""" @@ -63,8 +95,7 @@ ) -> tuple[list[AnyStr], list[AnyStr]]: """Translate `fnmatch` pattern.""" - flags = _flag_transform(flags) - return _wcparse.translate(patterns, flags, limit, exclude=exclude) + return _wcparse.translate(patterns, _flag_transform(flags), limit, exclude=exclude) def fnmatch( @@ -82,8 +113,7 @@ but if `case_sensitive` is set, respect that instead. """ - flags = _flag_transform(flags) - return bool(_wcparse.compile(patterns, flags, limit, exclude=exclude).match(filename)) + return _wcparse.compile(patterns, _flag_transform(flags), limit, exclude=exclude).match(filename) def filter( # noqa A001 @@ -96,15 +126,7 @@ ) -> 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 + return _wcparse.compile(patterns, _flag_transform(flags), limit, exclude=exclude).filter(filenames) # type: ignore[return-value] def escape(pattern: AnyStr) -> AnyStr: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/wcmatch/glob.py new/wcmatch-10.1/wcmatch/glob.py --- old/wcmatch-10.0/wcmatch/glob.py 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/wcmatch/glob.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,3 +1,4 @@ +# noqa: A005 """ Wild Card Match. @@ -10,6 +11,7 @@ import functools from collections import namedtuple import bracex +import copyreg from . import _wcparse from . import _wcmatch from . import util @@ -21,7 +23,8 @@ "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" + "iglob", "glob", "globmatch", "globfilter", "escape", "is_magic", "compile", + "Glob", "WcMatcher" ) # We don't use `util.platform` only because we mock it in tests, @@ -132,9 +135,9 @@ 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]`. + Each part 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: ------- @@ -388,6 +391,7 @@ def __init__( self, pattern: AnyStr | Sequence[AnyStr], + *, flags: int = 0, root_dir: AnyStr | os.PathLike[AnyStr] | None = None, dir_fd: int | None = None, @@ -403,6 +407,10 @@ flags = _wcparse.no_negate_flags(flags) self.pattern = [] # type: list[list[_GlobPart]] + + if not isinstance(pattern, (str, bytes)) and not pattern: + return + 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 @@ -597,15 +605,15 @@ """Check if file exists.""" if not self.dir_fd: - return os.path.lexists(self.prepend_base(path)) + return os.path.lexists(self._prepend_base(path)) try: - os.lstat(self.prepend_base(path), dir_fd=self.dir_fd) + 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: + def _prepend_base(self, path: AnyStr) -> AnyStr: """Join path to base if pattern is not absolute.""" if self.is_abs_pattern: @@ -777,7 +785,7 @@ results = [(curdir, True)] return results - def is_unique(self, path: AnyStr) -> bool: + def _is_unique(self, path: AnyStr) -> bool: """Test if path is unique.""" if self.nounique: @@ -795,11 +803,11 @@ 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]: + 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): + if self._is_unique(self._pathlib_norm(path) if self.pathlib else path): yield path def glob(self) -> Iterator[AnyStr]: @@ -820,7 +828,7 @@ 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))): + 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 @@ -838,21 +846,21 @@ 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) + 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) + 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) + 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) + yield from self._format_path(match, is_dir, dir_only) def iglob( @@ -866,10 +874,14 @@ ) -> 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() + yield from Glob( + patterns, + flags=flags, + root_dir=root_dir, + dir_fd=dir_fd, + limit=limit, + exclude=exclude + ).glob() def glob( @@ -886,6 +898,47 @@ return list(iglob(patterns, flags=flags, root_dir=root_dir, dir_fd=dir_fd, limit=limit, exclude=exclude)) +class WcMatcher(_wcmatch.WcMatcher[AnyStr]): + """Pre-compiled matcher object.""" + + def match( + self, + filename: AnyStr | os.PathLike[AnyStr], + *, + root_dir: AnyStr | os.PathLike[AnyStr] | None = None, + dir_fd: int | None = None + ) -> bool: + """Match filename.""" + + return self._matcher.match(filename, root_dir, dir_fd) + + def filter( + self, + filenames: Iterable[AnyStr | os.PathLike[AnyStr]], + *, + root_dir: AnyStr | os.PathLike[AnyStr] | None = None, + dir_fd: int | None = None + ) -> list[AnyStr | os.PathLike[AnyStr]]: + """Match filename.""" + + return self._matcher.filter(filenames, root_dir, dir_fd) + + +copyreg.pickle(WcMatcher, lambda p: (WcMatcher, (p._matcher,))) + + +def compile( # noqa: A001 + patterns: AnyStr | Sequence[AnyStr], + *, + flags: int = 0, + limit: int = _wcparse.PATTERN_LIMIT, + exclude: AnyStr | Sequence[AnyStr] | None = None +) -> WcMatcher[AnyStr]: + """Pre-compile a matcher object.""" + + return WcMatcher(_wcparse.compile(patterns, _flag_transform(flags), limit, exclude=exclude)) + + def translate( patterns: AnyStr | Sequence[AnyStr], *, @@ -895,8 +948,7 @@ ) -> tuple[list[AnyStr], list[AnyStr]]: """Translate glob pattern.""" - flags = _flag_transform(flags) - return _wcparse.translate(patterns, flags, limit, exclude) + return _wcparse.translate(patterns, _flag_transform(flags), limit, exclude) def globmatch( @@ -916,15 +968,7 @@ 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)) + return _wcparse.compile(patterns, _flag_transform(flags), limit, exclude).match(filename, root_dir, dir_fd) def globfilter( @@ -939,21 +983,7 @@ ) -> 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 + return _wcparse.compile(patterns, _flag_transform(flags), limit, exclude).filter(filenames, root_dir, dir_fd) def escape(pattern: AnyStr, unix: bool | None = None) -> AnyStr: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/wcmatch/pathlib.py new/wcmatch-10.1/wcmatch/pathlib.py --- old/wcmatch-10.0/wcmatch/pathlib.py 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/wcmatch/pathlib.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,3 +1,4 @@ +# noqa: A005 """Pathlib implementation that uses our own glob.""" from __future__ import annotations import pathlib @@ -187,7 +188,7 @@ 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: + 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) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/wcmatch-10.0/wcmatch/posix.py new/wcmatch-10.1/wcmatch/posix.py --- old/wcmatch-10.0/wcmatch/posix.py 2020-02-02 01:00:00.000000000 +0100 +++ new/wcmatch-10.1/wcmatch/posix.py 2020-02-02 01:00:00.000000000 +0100 @@ -1,3 +1,4 @@ +# noqa: A005 """Posix Properties.""" from __future__ import annotations