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 &copy; 2014 - 2024 <a href="https://github.com/facelessuser"; 
target="_blank" rel="noopener">Isaac Muse</a>
+  Copyright &copy; 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
 

Reply via email to