Skip to content

Commit 08c64f5

Browse files
committed
Allow precompiled matcher objects to be hashed and pickled
1 parent 73e0f45 commit 08c64f5

6 files changed

Lines changed: 94 additions & 22 deletions

File tree

.pyspelling.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
jobs: 8
2+
13
matrix:
24
- name: mkdocs
35
sources:

tests/test_fnmatch.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import os
77
import pytest
8+
import copy
89
import wcmatch.fnmatch as fnmatch
910
from unittest import mock
1011
from wcmatch import util
@@ -804,3 +805,19 @@ def test_precompiled_filter_empty(self):
804805

805806
m = fnmatch.compile('*file')
806807
self.assertEqual(m.filter([]), [])
808+
809+
def test_hash(self):
810+
"""Test hashing."""
811+
812+
m1 = fnmatch.compile('test', flags=fnmatch.C)
813+
m2 = fnmatch.compile('test', flags=fnmatch.C)
814+
m3 = fnmatch.compile('test', flags=fnmatch.I)
815+
m4 = fnmatch.compile(b'test', flags=fnmatch.C)
816+
817+
self.assertTrue(m1 == m2)
818+
self.assertTrue(m1 != m3)
819+
self.assertTrue(m1 != m4)
820+
821+
m5 = copy.copy(m1)
822+
self.assertTrue(m1 == m5)
823+
self.assertTrue(m5 in {m1})

tests/test_globmatch.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""Tests for `globmatch`."""
33
import unittest
44
import pytest
5+
import copy
56
import re
67
import os
78
import sys
@@ -1959,3 +1960,19 @@ def test_precompiled_filter_pathlib(self):
19591960
m.filter(paths),
19601961
glob.globfilter(paths, pattern, flags=glob.GLOBSTAR)
19611962
)
1963+
1964+
def test_hash(self):
1965+
"""Test hashing."""
1966+
1967+
m1 = glob.compile('test', flags=glob.C)
1968+
m2 = glob.compile('test', flags=glob.C)
1969+
m3 = glob.compile('test', flags=glob.I)
1970+
m4 = glob.compile(b'test', flags=glob.C)
1971+
1972+
self.assertTrue(m1 == m2)
1973+
self.assertTrue(m1 != m3)
1974+
self.assertTrue(m1 != m4)
1975+
1976+
m5 = copy.copy(m1)
1977+
self.assertTrue(m1 == m5)
1978+
self.assertTrue(m5 in {m1})

wcmatch/_wcmatch.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,47 @@ def filter(
373373
return matches
374374

375375

376-
def _pickle(p): # type: ignore[no-untyped-def]
377-
return WcRegexp, (p._include, p._exclude, p._real, p._path, p._follow)
376+
class WcMatcher(util.Immutable, Generic[AnyStr]):
377+
"""Pre-compiled matcher object."""
378+
379+
_matcher: WcRegexp[AnyStr]
380+
_hash: int
381+
382+
__slots__ = ('_matcher', '_hash')
383+
384+
def __init__(self, matcher: WcRegexp[AnyStr]) -> None:
385+
"""Initialize."""
386+
387+
super().__init__(
388+
_matcher=matcher,
389+
_hash=hash(
390+
(
391+
type(self),
392+
type(matcher), matcher,
393+
)
394+
)
395+
)
396+
397+
def __hash__(self) -> int:
398+
"""Hash."""
399+
400+
return self._hash
401+
402+
def __eq__(self, other: Any) -> bool:
403+
"""Equal."""
404+
405+
return (
406+
isinstance(other, WcMatcher) and
407+
self._matcher == other._matcher
408+
)
409+
410+
def __ne__(self, other: Any) -> bool:
411+
"""Equal."""
412+
413+
return (
414+
not isinstance(other, WcMatcher) or
415+
self._matcher != other._matcher
416+
)
378417

379418

380-
copyreg.pickle(WcRegexp, _pickle)
419+
copyreg.pickle(WcRegexp, lambda p: (WcRegexp, (p._include, p._exclude, p._real, p._path, p._follow)))

wcmatch/fnmatch.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
A custom implementation of `fnmatch`.
66
"""
77
from __future__ import annotations
8-
from wcmatch._wcmatch import WcRegexp
8+
from . import _wcmatch
99
from . import _wcparse
10-
from typing import AnyStr, Iterable, Sequence, Generic
10+
import copyreg
11+
from typing import AnyStr, Iterable, Sequence
1112

1213
__all__ = (
1314
"CASE", "EXTMATCH", "IGNORECASE", "RAWCHARS",
@@ -47,23 +48,21 @@
4748
)
4849

4950

50-
class WcMatcher(Generic[AnyStr]):
51+
class WcMatcher(_wcmatch.WcMatcher[AnyStr]):
5152
"""Pre-compiled matcher object."""
5253

53-
def __init__(self, matcher: WcRegexp[AnyStr]) -> None:
54-
"""Initialize."""
55-
56-
self.matcher = matcher # type: WcRegexp[AnyStr]
57-
5854
def match(self, filename: AnyStr) -> bool:
5955
"""Match filename."""
6056

61-
return self.matcher.match(filename)
57+
return self._matcher.match(filename)
6258

6359
def filter(self, filenames: Iterable[AnyStr]) -> list[AnyStr]:
6460
"""Match filename."""
6561

66-
return self.matcher.filter(filenames) # type: ignore[return-value]
62+
return self._matcher.filter(filenames) # type: ignore[return-value]
63+
64+
65+
copyreg.pickle(WcMatcher, lambda p: (WcMatcher, (p._matcher,)))
6766

6867

6968
def compile( # noqa: A001

wcmatch/glob.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
import functools
1212
from collections import namedtuple
1313
import bracex
14+
import copyreg
1415
from . import _wcparse
1516
from . import _wcmatch
1617
from . import util
17-
from ._wcmatch import WcRegexp
1818
from typing import Iterator, Iterable, AnyStr, Generic, Pattern, Callable, Any, Sequence
1919

2020
__all__ = (
@@ -898,14 +898,9 @@ def glob(
898898
return list(iglob(patterns, flags=flags, root_dir=root_dir, dir_fd=dir_fd, limit=limit, exclude=exclude))
899899

900900

901-
class WcMatcher(Generic[AnyStr]):
901+
class WcMatcher(_wcmatch.WcMatcher[AnyStr]):
902902
"""Pre-compiled matcher object."""
903903

904-
def __init__(self, matcher: WcRegexp[AnyStr]) -> None:
905-
"""Initialize."""
906-
907-
self.matcher = matcher # type: WcRegexp[AnyStr]
908-
909904
def match(
910905
self,
911906
filename: AnyStr | os.PathLike[AnyStr],
@@ -915,7 +910,7 @@ def match(
915910
) -> bool:
916911
"""Match filename."""
917912

918-
return self.matcher.match(filename, root_dir, dir_fd)
913+
return self._matcher.match(filename, root_dir, dir_fd)
919914

920915
def filter(
921916
self,
@@ -926,7 +921,10 @@ def filter(
926921
) -> list[AnyStr | os.PathLike[AnyStr]]:
927922
"""Match filename."""
928923

929-
return self.matcher.filter(filenames, root_dir, dir_fd)
924+
return self._matcher.filter(filenames, root_dir, dir_fd)
925+
926+
927+
copyreg.pickle(WcMatcher, lambda p: (WcMatcher, (p._matcher,)))
930928

931929

932930
def compile( # noqa: A001

0 commit comments

Comments
 (0)