upload existing project
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -174,3 +174,4 @@ cython_debug/
|
|||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
|
.idea
|
||||||
|
|||||||
64
README.md
64
README.md
@@ -1,2 +1,64 @@
|
|||||||
# kpmatch
|
# kpmatch - Path patterns
|
||||||
|
|
||||||
|
## Pattern Syntax
|
||||||
|
|
||||||
|
Path patterns should always use forward slashes `/` between segments.
|
||||||
|
The actual separator used when matching paths depends on the system.
|
||||||
|
|
||||||
|
A path segment containing two asterisks `/**/` matches zero, one or more
|
||||||
|
segments.
|
||||||
|
|
||||||
|
A single asterisk `*` matches zero, one or more characters except a path
|
||||||
|
separator.
|
||||||
|
|
||||||
|
A question mark `?` matches exactly one character unless it's a path
|
||||||
|
separator.
|
||||||
|
|
||||||
|
Square brackets `[ ]` can match one of the characters between the brackets.
|
||||||
|
If the first character is an exclamation point `[! ]`, it will match one
|
||||||
|
character that does NOT appear between the brackets.
|
||||||
|
|
||||||
|
Curly braces `{ , }` can match one of the sub-expressions separated by commas.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Function `kpmatch.kpmatch(path: str | PathLike[str], pattern: str) -> bool`
|
||||||
|
|
||||||
|
### Method `kpmatch.Pattern.match(path: str | PathLike[str]) -> bool`
|
||||||
|
|
||||||
|
Tests if a path matches a pattern.
|
||||||
|
|
||||||
|
<dl>
|
||||||
|
<dt>Parameters</dt>
|
||||||
|
<dd>
|
||||||
|
<strong>path</strong> — A relative or absolute file path.<br/>
|
||||||
|
<strong>pattern</strong> — A path pattern (see above for the syntax of patterns).
|
||||||
|
</dd>
|
||||||
|
<dt>Returns</dt>
|
||||||
|
<dd>True if the path matches the pattern.</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
```
|
||||||
|
function kpmatch.specificity(pattern: str) -> float
|
||||||
|
property kpmatch.Pattern.specificity: bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Computes the specificity of a pattern. The higher the number, the more
|
||||||
|
specific it is.
|
||||||
|
:param pattern: The path pattern to evaluate.
|
||||||
|
:return: A positive integer.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def compile(pattern: str) -> Pattern
|
||||||
|
```
|
||||||
|
|
||||||
|
Compiles a pattern into a Pattern object that can be reused multiple times.
|
||||||
|
:param pattern: The path pattern to compile.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def is_valid_pattern(pattern: str) -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifies the syntax of a pattern.
|
||||||
|
:param pattern: The path pattern to check.
|
||||||
|
:return: True if the pattern is well-formed.
|
||||||
|
|||||||
96
kpmatch/__init__.py
Normal file
96
kpmatch/__init__.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"""
|
||||||
|
kpmatch - Path glob matching functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
from os import PathLike
|
||||||
|
|
||||||
|
from kpmatch.matchers import PathMatcher
|
||||||
|
from kpmatch.parse import _parse_pattern, PatternParsingError
|
||||||
|
|
||||||
|
|
||||||
|
class Pattern:
|
||||||
|
"""
|
||||||
|
Represents a path pattern. Use kpmatch.compile(...) to create instances of
|
||||||
|
this class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__first_segment: "PathMatcher"
|
||||||
|
|
||||||
|
def __init__(self, first_segment: "PathMatcher"):
|
||||||
|
self.__first_segment = first_segment
|
||||||
|
|
||||||
|
def match(self, path: str):
|
||||||
|
normalised_path = os.path.normcase(path)
|
||||||
|
return self.__first_segment.match(normalised_path, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def specificity(self) -> int:
|
||||||
|
"""
|
||||||
|
The specificity of this pattern. The higher the number, the more
|
||||||
|
specific it is.
|
||||||
|
:return: A positive integer.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# if bit 1 is set, there is a fixed segment
|
||||||
|
# if bit 2 is set, there is fixed-length wildcard (either ?, [] or {})
|
||||||
|
# if bit 3 is set, there is * wildcard
|
||||||
|
# if bit 3 is set, there is ** wildcard
|
||||||
|
mask = self.__first_segment.generate_wildness_mask()
|
||||||
|
spec = 0
|
||||||
|
for n in (1, 2, 4, 8):
|
||||||
|
if (mask & n) == 0:
|
||||||
|
# if there is no segment of wildness n...
|
||||||
|
if spec > 0:
|
||||||
|
# ... add n so that it is always more specific than any
|
||||||
|
# pattern with such a segment
|
||||||
|
spec += n
|
||||||
|
# if spec is 0, we stay at 0 until we find
|
||||||
|
# the most specific segment
|
||||||
|
else:
|
||||||
|
# if there is a segment of wildness n,
|
||||||
|
# add 1 so that it is a little bit more specific
|
||||||
|
spec += 1
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
def compile(pattern: str) -> Pattern:
|
||||||
|
"""
|
||||||
|
Compiles a pattern into a Pattern object that can be reused multiple times.
|
||||||
|
:param pattern: The path pattern to compile.
|
||||||
|
"""
|
||||||
|
normalised_pattern = os.path.normcase(pattern)
|
||||||
|
return Pattern(_parse_pattern(normalised_pattern))
|
||||||
|
|
||||||
|
|
||||||
|
def kpmatch(path: str | PathLike[str], pattern: str) -> bool:
|
||||||
|
"""
|
||||||
|
Tests if a path matches a pattern.
|
||||||
|
:param path: A relative or absolute file path.
|
||||||
|
:param pattern: A path pattern (see the README for the syntax of patterns).
|
||||||
|
:return: True if the path matches the pattern.
|
||||||
|
"""
|
||||||
|
return compile(pattern).match(path)
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_pattern(pattern: str) -> bool:
|
||||||
|
"""
|
||||||
|
Verifies the syntax of a pattern.
|
||||||
|
:param pattern: The path pattern to check.
|
||||||
|
:return: True if the pattern is well-formed.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
compile(pattern)
|
||||||
|
return True
|
||||||
|
except PatternParsingError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def specificity(pattern: str) -> float:
|
||||||
|
"""
|
||||||
|
Computes the specificity of a pattern. The higher the number, the more
|
||||||
|
specific it is.
|
||||||
|
:param pattern: The path pattern to evaluate.
|
||||||
|
:return: A positive integer.
|
||||||
|
"""
|
||||||
|
return compile(pattern).specificity
|
||||||
251
kpmatch/matchers.py
Normal file
251
kpmatch/matchers.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import os.path
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class PathMatcher(ABC):
|
||||||
|
__next: Optional["PathMatcher"]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.__next = None
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return isinstance(other, PathMatcher) and other.__next == self.__next
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__}({self._repr_attrs()})" + (
|
||||||
|
" & " + repr(self.__next) if self.__next is not None else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
def _repr_attrs(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def set_next(self, matcher: "PathMatcher"):
|
||||||
|
self.__next = matcher
|
||||||
|
|
||||||
|
def append(self, matcher: "PathMatcher") -> "PathMatcher":
|
||||||
|
if self.__next is None:
|
||||||
|
self.set_next(matcher)
|
||||||
|
else:
|
||||||
|
self.__next.append(matcher)
|
||||||
|
return self.__next
|
||||||
|
|
||||||
|
def match_next(self, path: str, start: int) -> bool:
|
||||||
|
if self.__next is None:
|
||||||
|
raise RuntimeError("match_next called at EOP")
|
||||||
|
return self.__next.match(path, start)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def match(self, path: str, start: int) -> bool: ...
|
||||||
|
|
||||||
|
def generate_wildness_mask(self) -> int:
|
||||||
|
if self.__next is None:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return self.wildness | self.__next.generate_wildness_mask()
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def wildness(self) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
|
class AnySegments(PathMatcher):
|
||||||
|
"""
|
||||||
|
Matches zero, one or more segments. This represents the ** wildcard.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def match(self, path: str, start: int) -> bool:
|
||||||
|
last_sep = prefix = len(path)
|
||||||
|
match = False
|
||||||
|
while not match and prefix > start:
|
||||||
|
match = self.match_next(path, prefix)
|
||||||
|
if not match:
|
||||||
|
# backtrack by removing an entire segment from the prefix
|
||||||
|
last_sep = path.rfind(os.path.sep, start, last_sep)
|
||||||
|
prefix = start if last_sep == -1 else last_sep + len(os.path.sep)
|
||||||
|
# last try without consuming anything
|
||||||
|
if not match:
|
||||||
|
match = self.match_next(path, prefix)
|
||||||
|
return match
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wildness(self) -> int:
|
||||||
|
return 8
|
||||||
|
|
||||||
|
|
||||||
|
class AnyName(PathMatcher):
|
||||||
|
"""
|
||||||
|
Matches any filename. This represents the * wildcard.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def match(self, path: str, start: int) -> bool:
|
||||||
|
next_sep = path.find(os.path.sep, start)
|
||||||
|
prefix = len(path) if next_sep == -1 else next_sep
|
||||||
|
match = False
|
||||||
|
while not match and prefix >= start:
|
||||||
|
match = self.match_next(path, prefix)
|
||||||
|
if not match:
|
||||||
|
prefix -= 1
|
||||||
|
return match
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wildness(self) -> int:
|
||||||
|
return 4
|
||||||
|
|
||||||
|
|
||||||
|
class OneOf(PathMatcher):
|
||||||
|
"""
|
||||||
|
Matches any of the sub-expressions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__matchers: tuple["PathMatcher", ...]
|
||||||
|
|
||||||
|
def __init__(self, matchers: Iterable["PathMatcher"]):
|
||||||
|
super().__init__()
|
||||||
|
self.__matchers = tuple(matchers)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
super().__eq__(other)
|
||||||
|
and isinstance(other, OneOf)
|
||||||
|
and other.__matchers == self.__matchers
|
||||||
|
)
|
||||||
|
|
||||||
|
def _repr_attrs(self):
|
||||||
|
return "(" + ", ".join(repr(m) for m in self.__matchers) + ")"
|
||||||
|
|
||||||
|
def append(self, matcher: "PathMatcher") -> "PathMatcher":
|
||||||
|
next_matcher = super().append(matcher)
|
||||||
|
for choice_matcher in self.__matchers:
|
||||||
|
choice_matcher.set_next(next_matcher)
|
||||||
|
return next_matcher
|
||||||
|
|
||||||
|
def match(self, path: str, start: int) -> bool:
|
||||||
|
match = False
|
||||||
|
for choice_matcher in self.__matchers:
|
||||||
|
match = choice_matcher.match(path, start)
|
||||||
|
if match:
|
||||||
|
break
|
||||||
|
return match
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wildness(self) -> int:
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
class AnyCharacter(PathMatcher):
|
||||||
|
"""
|
||||||
|
Matches a single character. This represents the ? wildcard.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return super().__eq__(other) and isinstance(other, AnyCharacter)
|
||||||
|
|
||||||
|
def match(self, path: str, start: int) -> bool:
|
||||||
|
return start < len(path) and self.match_next(path, start + 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wildness(self) -> int:
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterSet(PathMatcher):
|
||||||
|
"""
|
||||||
|
Matches any/none of the characters in a set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__set: str
|
||||||
|
__negation: bool
|
||||||
|
|
||||||
|
def __init__(self, character_set: str, negation: bool):
|
||||||
|
super().__init__()
|
||||||
|
self.__set = character_set
|
||||||
|
self.__negation = negation
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
super().__eq__(other)
|
||||||
|
and isinstance(other, CharacterSet)
|
||||||
|
and other.__set == self.__set
|
||||||
|
and other.__negation == self.__negation
|
||||||
|
)
|
||||||
|
|
||||||
|
def _repr_attrs(self):
|
||||||
|
return repr(self.__set) + ", " + repr(self.__negation)
|
||||||
|
|
||||||
|
def match(self, path: str, start: int) -> bool:
|
||||||
|
return (
|
||||||
|
start < len(path)
|
||||||
|
and (self.__set.find(path[start]) == -1) == self.__negation
|
||||||
|
and self.match_next(path, start + 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wildness(self) -> int:
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
class FixedName(PathMatcher):
|
||||||
|
"""
|
||||||
|
Matches a fixed filename.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__text: str
|
||||||
|
|
||||||
|
def __init__(self, text: str):
|
||||||
|
super().__init__()
|
||||||
|
self.__text = text
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
super().__eq__(other)
|
||||||
|
and isinstance(other, FixedName)
|
||||||
|
and other.__text == self.__text
|
||||||
|
)
|
||||||
|
|
||||||
|
def _repr_attrs(self):
|
||||||
|
return repr(self.__text)
|
||||||
|
|
||||||
|
def match(self, path: str, start: int) -> bool:
|
||||||
|
return path.startswith(self.__text, start) and self.match_next(
|
||||||
|
path, start + len(self.__text)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wildness(self) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class EndOfSegment(PathMatcher):
|
||||||
|
"""
|
||||||
|
Matches the end of a single segment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def match(self, path: str, start: int) -> bool:
|
||||||
|
if start >= len(path):
|
||||||
|
return self.match_next(path, start)
|
||||||
|
else:
|
||||||
|
return path.startswith(os.path.sep, start) and self.match_next(
|
||||||
|
path, start + len(os.path.sep)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wildness(self) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class EndOfPath(PathMatcher):
|
||||||
|
"""
|
||||||
|
Matches the end of the string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def match(self, path: str, start: int) -> bool:
|
||||||
|
return start >= len(path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wildness(self) -> int:
|
||||||
|
return 0
|
||||||
265
kpmatch/parse.py
Normal file
265
kpmatch/parse.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import os.path
|
||||||
|
from typing import Optional, Never
|
||||||
|
|
||||||
|
from kpmatch.matchers import (
|
||||||
|
PathMatcher,
|
||||||
|
EndOfPath,
|
||||||
|
AnySegments,
|
||||||
|
AnyName,
|
||||||
|
FixedName,
|
||||||
|
CharacterSet,
|
||||||
|
AnyCharacter,
|
||||||
|
OneOf,
|
||||||
|
EndOfSegment,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PatternParsingError(ValueError):
|
||||||
|
def __init__(self, message, pattern, position):
|
||||||
|
super().__init__(f'{message} at position {position} in pattern "{pattern}"')
|
||||||
|
|
||||||
|
|
||||||
|
def _append(a: Optional[PathMatcher], b: PathMatcher) -> PathMatcher:
|
||||||
|
if a is None:
|
||||||
|
return b
|
||||||
|
else:
|
||||||
|
a.append(b)
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_pattern(pattern: str) -> PathMatcher:
|
||||||
|
# pattern <- (any_segments / fixed_segment)*
|
||||||
|
i = 0
|
||||||
|
first = None
|
||||||
|
while i < len(pattern):
|
||||||
|
(segment, i) = _parse_any_segments(pattern, i)
|
||||||
|
if segment is None:
|
||||||
|
(segment, i) = _parse_fixed_segment(pattern, i)
|
||||||
|
if segment is None:
|
||||||
|
raise PatternParsingError(
|
||||||
|
f'unexpected character "{pattern[i]}"', pattern, i
|
||||||
|
)
|
||||||
|
first = _append(first, segment)
|
||||||
|
first = _append(first, EndOfPath())
|
||||||
|
return first
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_any_segments(pattern: str, start: int) -> tuple[Optional[PathMatcher], int]:
|
||||||
|
# any_segments <- "**" (SEP / EOS)
|
||||||
|
i = start
|
||||||
|
matcher = None
|
||||||
|
if pattern.startswith("**", i):
|
||||||
|
i += 2
|
||||||
|
if pattern.startswith(os.path.sep, i):
|
||||||
|
i += len(os.path.sep)
|
||||||
|
matcher = AnySegments()
|
||||||
|
elif i == len(pattern):
|
||||||
|
matcher = AnySegments()
|
||||||
|
else:
|
||||||
|
i = start
|
||||||
|
return matcher, i
|
||||||
|
|
||||||
|
|
||||||
|
def _raise_unexpected_name_part_error(pattern: str, start: int) -> Never:
|
||||||
|
if pattern.startswith(os.path.sep, start):
|
||||||
|
message = "Empty path segment"
|
||||||
|
elif pattern.startswith("[", start):
|
||||||
|
message = 'Missing closing bracket "]" to match "["'
|
||||||
|
elif pattern.startswith("{", start):
|
||||||
|
message = 'Missing closing bracket "}" to match "{"'
|
||||||
|
else:
|
||||||
|
message = f'unexpected character "{pattern[start]}"'
|
||||||
|
raise PatternParsingError(message, pattern, start)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_fixed_segment(pattern: str, start: int) -> tuple[PathMatcher, int]:
|
||||||
|
# fixed_segment <- name_part+ (SEP / EOS)
|
||||||
|
# consume at least one part
|
||||||
|
(first, i) = _parse_name_part(pattern, start)
|
||||||
|
if first is None:
|
||||||
|
_raise_unexpected_name_part_error(pattern, i)
|
||||||
|
ended = False
|
||||||
|
while not ended and i < len(pattern):
|
||||||
|
# stop if we encounter a path separator
|
||||||
|
if pattern.startswith(os.path.sep, i):
|
||||||
|
i += len(os.path.sep)
|
||||||
|
ended = True
|
||||||
|
# parse the next part otherwise
|
||||||
|
else:
|
||||||
|
(other, i) = _parse_name_part(pattern, i)
|
||||||
|
if other is None:
|
||||||
|
_raise_unexpected_name_part_error(pattern, i)
|
||||||
|
first = _append(first, other)
|
||||||
|
first = _append(first, EndOfSegment())
|
||||||
|
return first, i
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_name_part(pattern: str, start: int) -> tuple[Optional[PathMatcher], int]:
|
||||||
|
# name_part <- any_name / one_of / simple_name_part / ","
|
||||||
|
i = start
|
||||||
|
(part, i) = _parse_any_name(pattern, i)
|
||||||
|
if part is None:
|
||||||
|
(part, i) = _parse_one_of(pattern, i)
|
||||||
|
if part is None:
|
||||||
|
(part, i) = _parse_simple_name_part(pattern, i)
|
||||||
|
if part is None:
|
||||||
|
if pattern.startswith(",", i):
|
||||||
|
i += 1
|
||||||
|
part = FixedName(",")
|
||||||
|
return part, i
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_any_name(pattern: str, start: int) -> tuple[Optional[PathMatcher], int]:
|
||||||
|
# any_name = "*"+
|
||||||
|
i = start
|
||||||
|
matcher = None
|
||||||
|
while pattern.startswith("*", i):
|
||||||
|
i += 1
|
||||||
|
matcher = AnyName()
|
||||||
|
return matcher, i
|
||||||
|
|
||||||
|
|
||||||
|
def _raise_error_in_one_of(pattern: str, start: int) -> bool:
|
||||||
|
if pattern.startswith("{", start):
|
||||||
|
raise PatternParsingError(
|
||||||
|
'Character "{" is not allowed inside a one-of pattern',
|
||||||
|
pattern,
|
||||||
|
start,
|
||||||
|
)
|
||||||
|
if pattern.startswith("*", start):
|
||||||
|
raise PatternParsingError(
|
||||||
|
'A star "*" wildcard is not allowed inside a one-of pattern',
|
||||||
|
pattern,
|
||||||
|
start,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_one_of(pattern: str, start: int) -> tuple[Optional[PathMatcher], int]:
|
||||||
|
# one_of = "{" one_of_choice ("," one_of_choice)* "}"
|
||||||
|
i = start
|
||||||
|
matcher = None
|
||||||
|
if pattern.startswith("{", i):
|
||||||
|
i += 1
|
||||||
|
ended = False
|
||||||
|
choices = []
|
||||||
|
|
||||||
|
_raise_error_in_one_of(pattern, i)
|
||||||
|
(choice, j) = _parse_one_of_choice(pattern, i)
|
||||||
|
choices.append((choice, i))
|
||||||
|
i = j
|
||||||
|
|
||||||
|
while not ended and i < len(pattern):
|
||||||
|
_raise_error_in_one_of(pattern, i)
|
||||||
|
if pattern.startswith(",", i):
|
||||||
|
i += 1
|
||||||
|
_raise_error_in_one_of(pattern, i)
|
||||||
|
(choice, j) = _parse_one_of_choice(pattern, i)
|
||||||
|
choices.append((choice, i))
|
||||||
|
i = j
|
||||||
|
else:
|
||||||
|
ended = True
|
||||||
|
|
||||||
|
if pattern.startswith("}", i):
|
||||||
|
i += 1
|
||||||
|
choice_matchers = []
|
||||||
|
for choice_matcher, choice_start in choices:
|
||||||
|
if choice_matcher is None:
|
||||||
|
raise PatternParsingError(
|
||||||
|
"Empty choice in a one-of pattern", pattern, choice_start
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
choice_matchers.append(choice_matcher)
|
||||||
|
matcher = OneOf(choice_matchers)
|
||||||
|
else:
|
||||||
|
i = start
|
||||||
|
return matcher, i
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_one_of_choice(pattern: str, start: int) -> tuple[Optional[PathMatcher], int]:
|
||||||
|
# one_of_choice = simple_name_part+
|
||||||
|
i = start
|
||||||
|
first = None
|
||||||
|
ended = False
|
||||||
|
while not ended and i < len(pattern):
|
||||||
|
(segment, i) = _parse_simple_name_part(pattern, i)
|
||||||
|
if segment is None:
|
||||||
|
ended = True
|
||||||
|
else:
|
||||||
|
first = _append(first, segment)
|
||||||
|
return first, i
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_simple_name_part(
|
||||||
|
pattern: str, start: int
|
||||||
|
) -> tuple[Optional[PathMatcher], int]:
|
||||||
|
# simple_name_part = any_character / character_set / fixed_name
|
||||||
|
i = start
|
||||||
|
(part, i) = _parse_any_character(pattern, i)
|
||||||
|
if part is None:
|
||||||
|
(part, i) = _parse_character_set(pattern, i)
|
||||||
|
if part is None:
|
||||||
|
(part, i) = _parse_fixed_name(pattern, i)
|
||||||
|
return part, i
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_any_character(pattern: str, start: int) -> tuple[Optional[PathMatcher], int]:
|
||||||
|
# any_character = "?"
|
||||||
|
i = start
|
||||||
|
matcher = None
|
||||||
|
if pattern.startswith("?", i):
|
||||||
|
i += 1
|
||||||
|
matcher = AnyCharacter()
|
||||||
|
return matcher, i
|
||||||
|
|
||||||
|
|
||||||
|
def _is_allowed_in_character_set(pattern: str, start: int) -> bool:
|
||||||
|
return not pattern.startswith(os.path.sep, start) and pattern[start] != "]"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_immediate_error_in_character_set(pattern: str, start: int) -> bool:
|
||||||
|
return pattern[start] == "!" or pattern[start] == "["
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_character_set(pattern: str, start: int) -> tuple[Optional[PathMatcher], int]:
|
||||||
|
# character_set = "[" "!"? (!("[" / "!" / "]" / SEP) .)+ "]"
|
||||||
|
i = start
|
||||||
|
matcher = None
|
||||||
|
if pattern.startswith("[", i):
|
||||||
|
i += 1
|
||||||
|
negation = False
|
||||||
|
if pattern.startswith("!", i):
|
||||||
|
i += 1
|
||||||
|
negation = True
|
||||||
|
set_start = i
|
||||||
|
while i < len(pattern) and _is_allowed_in_character_set(pattern, i):
|
||||||
|
if _is_immediate_error_in_character_set(pattern, i):
|
||||||
|
raise PatternParsingError(
|
||||||
|
f'Character "{pattern[i]}" is not allowed inside a character set',
|
||||||
|
pattern,
|
||||||
|
i,
|
||||||
|
)
|
||||||
|
i += 1
|
||||||
|
if i > set_start and pattern.startswith("]", i):
|
||||||
|
matcher = CharacterSet(pattern[set_start:i], negation)
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
i = start
|
||||||
|
return matcher, i
|
||||||
|
|
||||||
|
|
||||||
|
def _is_allowed_in_fixed_name(pattern: str, start: int) -> bool:
|
||||||
|
return (
|
||||||
|
_is_allowed_in_character_set(pattern, start)
|
||||||
|
and "[!]*{,}?".find(pattern[start]) == -1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_fixed_name(pattern: str, start: int) -> tuple[Optional[PathMatcher], int]:
|
||||||
|
# fixed_name = (!("[" / "!" / "]" / SEP / "*" / "?" / "{" / "}" / ",") .)+
|
||||||
|
i = start
|
||||||
|
matcher = None
|
||||||
|
while i < len(pattern) and _is_allowed_in_fixed_name(pattern, i):
|
||||||
|
i += 1
|
||||||
|
if i > start:
|
||||||
|
matcher = FixedName(pattern[start:i])
|
||||||
|
return matcher, i
|
||||||
325
poetry.lock
generated
Normal file
325
poetry.lock
generated
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "25.11.0"
|
||||||
|
description = "The uncompromising code formatter."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"},
|
||||||
|
{file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"},
|
||||||
|
{file = "black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37"},
|
||||||
|
{file = "black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03"},
|
||||||
|
{file = "black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a"},
|
||||||
|
{file = "black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170"},
|
||||||
|
{file = "black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc"},
|
||||||
|
{file = "black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e"},
|
||||||
|
{file = "black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac"},
|
||||||
|
{file = "black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96"},
|
||||||
|
{file = "black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd"},
|
||||||
|
{file = "black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409"},
|
||||||
|
{file = "black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b"},
|
||||||
|
{file = "black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd"},
|
||||||
|
{file = "black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993"},
|
||||||
|
{file = "black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c"},
|
||||||
|
{file = "black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170"},
|
||||||
|
{file = "black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545"},
|
||||||
|
{file = "black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda"},
|
||||||
|
{file = "black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664"},
|
||||||
|
{file = "black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06"},
|
||||||
|
{file = "black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2"},
|
||||||
|
{file = "black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc"},
|
||||||
|
{file = "black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc"},
|
||||||
|
{file = "black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b"},
|
||||||
|
{file = "black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=8.0.0"
|
||||||
|
mypy-extensions = ">=0.4.3"
|
||||||
|
packaging = ">=22.0"
|
||||||
|
pathspec = ">=0.9.0"
|
||||||
|
platformdirs = ">=2"
|
||||||
|
pytokens = ">=0.3.0"
|
||||||
|
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||||
|
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
|
d = ["aiohttp (>=3.10)"]
|
||||||
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.0"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"},
|
||||||
|
{file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
groups = ["dev"]
|
||||||
|
markers = "platform_system == \"Windows\""
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coverage"
|
||||||
|
version = "7.11.3"
|
||||||
|
description = "Code coverage measurement for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0c986537abca9b064510f3fd104ba33e98d3036608c7f2f5537f869bc10e1ee5"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28c5251b3ab1d23e66f1130ca0c419747edfbcb4690de19467cd616861507af7"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4f2bb4ee8dd40f9b2a80bb4adb2aecece9480ba1fa60d9382e8c8e0bd558e2eb"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e5f4bfac975a2138215a38bda599ef00162e4143541cf7dd186da10a7f8e69f1"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f4cbfff5cf01fa07464439a8510affc9df281535f41a1f5312fbd2b59b4ab5c"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:31663572f20bf3406d7ac00d6981c7bbbcec302539d26b5ac596ca499664de31"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9799bd6a910961cb666196b8583ed0ee125fa225c6fdee2cbf00232b861f29d2"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:097acc18bedf2c6e3144eaf09b5f6034926c3c9bb9e10574ffd0942717232507"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6f033dec603eea88204589175782290a038b436105a8f3637a81c4359df27832"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd9ca2d44ed8018c90efb72f237a2a140325a4c3339971364d758e78b175f58e"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-win32.whl", hash = "sha256:900580bc99c145e2561ea91a2d207e639171870d8a18756eb57db944a017d4bb"},
|
||||||
|
{file = "coverage-7.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:c8be5bfcdc7832011b2652db29ed7672ce9d353dd19bce5272ca33dbcf60aaa8"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:200bb89fd2a8a07780eafcdff6463104dec459f3c838d980455cfa84f5e5e6e1"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d264402fc179776d43e557e1ca4a7d953020d3ee95f7ec19cc2c9d769277f06"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:385977d94fc155f8731c895accdfcc3dd0d9dd9ef90d102969df95d3c637ab80"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0542ddf6107adbd2592f29da9f59f5d9cff7947b5bb4f734805085c327dcffaa"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60bf4d7f886989ddf80e121a7f4d140d9eac91f1d2385ce8eb6bda93d563297"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0a3b6e32457535df0d41d2d895da46434706dd85dbaf53fbc0d3bd7d914b362"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:876a3ee7fd2613eb79602e4cdb39deb6b28c186e76124c3f29e580099ec21a87"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a730cd0824e8083989f304e97b3f884189efb48e2151e07f57e9e138ab104200"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b5cd111d3ab7390be0c07ad839235d5ad54d2ca497b5f5db86896098a77180a4"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:074e6a5cd38e06671580b4d872c1a67955d4e69639e4b04e87fc03b494c1f060"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-win32.whl", hash = "sha256:86d27d2dd7c7c5a44710565933c7dc9cd70e65ef97142e260d16d555667deef7"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:ca90ef33a152205fb6f2f0c1f3e55c50df4ef049bb0940ebba666edd4cdebc55"},
|
||||||
|
{file = "coverage-7.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:56f909a40d68947ef726ce6a34eb38f0ed241ffbe55c5007c64e616663bcbafc"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b771b59ac0dfb7f139f70c85b42717ef400a6790abb6475ebac1ecee8de782f"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:603c4414125fc9ae9000f17912dcfd3d3eb677d4e360b85206539240c96ea76e"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:77ffb3b7704eb7b9b3298a01fe4509cef70117a52d50bcba29cffc5f53dd326a"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4d4ca49f5ba432b0755ebb0fc3a56be944a19a16bb33802264bbc7311622c0d1"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05fd3fb6edff0c98874d752013588836f458261e5eba587afe4c547bba544afd"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e920567f8c3a3ce68ae5a42cf7c2dc4bb6cc389f18bff2235dd8c03fa405de5"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4bec8c7160688bd5a34e65c82984b25409563134d63285d8943d0599efbc448e"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:adb9b7b42c802bd8cb3927de8c1c26368ce50c8fdaa83a9d8551384d77537044"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c8f563b245b4ddb591e99f28e3cd140b85f114b38b7f95b2e42542f0603eb7d7"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e2a96fdc7643c9517a317553aca13b5cae9bad9a5f32f4654ce247ae4d321405"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-win32.whl", hash = "sha256:e8feeb5e8705835f0622af0fe7ff8d5cb388948454647086494d6c41ec142c2e"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:abb903ffe46bd319d99979cdba350ae7016759bb69f47882242f7b93f3356055"},
|
||||||
|
{file = "coverage-7.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:1451464fd855d9bd000c19b71bb7dafea9ab815741fb0bd9e813d9b671462d6f"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84b892e968164b7a0498ddc5746cdf4e985700b902128421bb5cec1080a6ee36"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f761dbcf45e9416ec4698e1a7649248005f0064ce3523a47402d1bff4af2779e"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1410bac9e98afd9623f53876fae7d8a5db9f5a0ac1c9e7c5188463cb4b3212e2"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:004cdcea3457c0ea3233622cd3464c1e32ebba9b41578421097402bee6461b63"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f067ada2c333609b52835ca4d4868645d3b63ac04fb2b9a658c55bba7f667d3"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07bc7745c945a6d95676953e86ba7cebb9f11de7773951c387f4c07dc76d03f5"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bba7e4743e37484ae17d5c3b8eb1ce78b564cb91b7ace2e2182b25f0f764cb5"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbffc22d80d86fbe456af9abb17f7a7766e7b2101f7edaacc3535501691563f7"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0dba4da36730e384669e05b765a2c49f39514dd3012fcc0398dd66fba8d746d5"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae12fe90b00b71a71b69f513773310782ce01d5f58d2ceb2b7c595ab9d222094"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-win32.whl", hash = "sha256:12d821de7408292530b0d241468b698bce18dd12ecaf45316149f53877885f8c"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:6bb599052a974bb6cedfa114f9778fedfad66854107cf81397ec87cb9b8fbcf2"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:bb9d7efdb063903b3fdf77caec7b77c3066885068bdc0d44bc1b0c171033f944"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fb58da65e3339b3dbe266b607bb936efb983d86b00b03eb04c4ad5b442c58428"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d16bbe566e16a71d123cd66382c1315fcd520c7573652a8074a8fe281b38c6a"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8258f10059b5ac837232c589a350a2df4a96406d6d5f2a09ec587cbdd539655"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c5627429f7fbff4f4131cfdd6abd530734ef7761116811a707b88b7e205afd7"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:465695268414e149bab754c54b0c45c8ceda73dd4a5c3ba255500da13984b16d"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ebcddfcdfb4c614233cff6e9a3967a09484114a8b2e4f2c7a62dc83676ba13f"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13b2066303a1c1833c654d2af0455bb009b6e1727b3883c9964bc5c2f643c1d0"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d8750dd20362a1b80e3cf84f58013d4672f89663aee457ea59336df50fab6739"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ab6212e62ea0e1006531a2234e209607f360d98d18d532c2fa8e403c1afbdd71"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b17c2b5e0b9bb7702449200f93e2d04cb04b1414c41424c08aa1e5d352da76"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-win32.whl", hash = "sha256:426559f105f644b69290ea414e154a0d320c3ad8a2bb75e62884731f69cf8e2c"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:90a96fcd824564eae6137ec2563bd061d49a32944858d4bdbae5c00fb10e76ac"},
|
||||||
|
{file = "coverage-7.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:1e33d0bebf895c7a0905fcfaff2b07ab900885fc78bba2a12291a2cfbab014cc"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fdc5255eb4815babcdf236fa1a806ccb546724c8a9b129fd1ea4a5448a0bf07c"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fe3425dc6021f906c6325d3c415e048e7cdb955505a94f1eb774dafc779ba203"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4ca5f876bf41b24378ee67c41d688155f0e54cdc720de8ef9ad6544005899240"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9061a3e3c92b27fd8036dafa26f25d95695b6aa2e4514ab16a254f297e664f83"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abcea3b5f0dc44e1d01c27090bc32ce6ffb7aa665f884f1890710454113ea902"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:68c4eb92997dbaaf839ea13527be463178ac0ddd37a7ac636b8bc11a51af2428"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:149eccc85d48c8f06547534068c41d69a1a35322deaa4d69ba1561e2e9127e75"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:08c0bcf932e47795c49f0406054824b9d45671362dfc4269e0bc6e4bff010704"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:39764c6167c82d68a2d8c97c33dba45ec0ad9172570860e12191416f4f8e6e1b"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3224c7baf34e923ffc78cb45e793925539d640d42c96646db62dbd61bbcfa131"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-win32.whl", hash = "sha256:c713c1c528284d636cd37723b0b4c35c11190da6f932794e145fc40f8210a14a"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:c381a252317f63ca0179d2c7918e83b99a4ff3101e1b24849b999a00f9cd4f86"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:3e33a968672be1394eded257ec10d4acbb9af2ae263ba05a99ff901bb863557e"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f9c96a29c6d65bd36a91f5634fef800212dff69dacdb44345c4c9783943ab0df"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ec27a7a991d229213c8070d31e3ecf44d005d96a9edc30c78eaeafaa421c001"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:72c8b494bd20ae1c58528b97c4a67d5cfeafcb3845c73542875ecd43924296de"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:60ca149a446da255d56c2a7a813b51a80d9497a62250532598d249b3cdb1a926"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5069074db19a534de3859c43eec78e962d6d119f637c41c8e028c5ab3f59dd"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac5d5329c9c942bbe6295f4251b135d860ed9f86acd912d418dce186de7c19ac"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e22539b676fafba17f0a90ac725f029a309eb6e483f364c86dcadee060429d46"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2376e8a9c889016f25472c452389e98bc6e54a19570b107e27cde9d47f387b64"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4234914b8c67238a3c4af2bba648dc716aa029ca44d01f3d51536d44ac16854f"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0b4101e2b3c6c352ff1f70b3a6fcc7c17c1ab1a91ccb7a33013cb0782af9820"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-win32.whl", hash = "sha256:305716afb19133762e8cf62745c46c4853ad6f9eeba54a593e373289e24ea237"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9245bd392572b9f799261c4c9e7216bafc9405537d0f4ce3ad93afe081a12dc9"},
|
||||||
|
{file = "coverage-7.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:9a1d577c20b4334e5e814c3d5fe07fa4a8c3ae42a601945e8d7940bab811d0bd"},
|
||||||
|
{file = "coverage-7.11.3-py3-none-any.whl", hash = "sha256:351511ae28e2509c8d8cae5311577ea7dd511ab8e746ffc8814a0896c3d33fbe"},
|
||||||
|
{file = "coverage-7.11.3.tar.gz", hash = "sha256:0f59387f5e6edbbffec2281affb71cdc85e0776c1745150a3ab9b6c1d016106b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.1.0"
|
||||||
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
|
||||||
|
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
description = "Core utilities for Python packages"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
|
||||||
|
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "0.12.1"
|
||||||
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||||
|
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.5.0"
|
||||||
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"},
|
||||||
|
{file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"]
|
||||||
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"]
|
||||||
|
type = ["mypy (>=1.18.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytokens"
|
||||||
|
version = "0.3.0"
|
||||||
|
description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"},
|
||||||
|
{file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.3.0"
|
||||||
|
description = "A lil' TOML parser"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
markers = "python_version == \"3.10\""
|
||||||
|
files = [
|
||||||
|
{file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"},
|
||||||
|
{file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"},
|
||||||
|
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"},
|
||||||
|
{file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"},
|
||||||
|
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"},
|
||||||
|
{file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"},
|
||||||
|
{file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"},
|
||||||
|
{file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"},
|
||||||
|
{file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"},
|
||||||
|
{file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"},
|
||||||
|
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"},
|
||||||
|
{file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"},
|
||||||
|
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"},
|
||||||
|
{file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"},
|
||||||
|
{file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"},
|
||||||
|
{file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"},
|
||||||
|
{file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"},
|
||||||
|
{file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"},
|
||||||
|
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"},
|
||||||
|
{file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"},
|
||||||
|
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"},
|
||||||
|
{file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"},
|
||||||
|
{file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"},
|
||||||
|
{file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"},
|
||||||
|
{file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"},
|
||||||
|
{file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"},
|
||||||
|
{file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
markers = "python_version == \"3.10\""
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||||
|
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.1"
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
content-hash = "2afe710a5499b666d6de8a1b2f1427995d8e5597cbf0fa7caeff697e9a391d00"
|
||||||
21
pyproject.toml
Normal file
21
pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[project]
|
||||||
|
name = "kpmatch"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = [
|
||||||
|
{name = "Louis DEVIE",email = "contact@louisdevie.fr"}
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
black = "^25.11.0"
|
||||||
|
coverage = "^7.11.3"
|
||||||
|
|
||||||
3
test/__init__.py
Normal file
3
test/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .match import MatchTests
|
||||||
|
from .parse import ParseTests
|
||||||
|
from .specificity import SpecificityTests
|
||||||
74
test/match.py
Normal file
74
test/match.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import kpmatch
|
||||||
|
|
||||||
|
|
||||||
|
class MatchTests(TestCase):
|
||||||
|
def test_match_empty(self):
|
||||||
|
pattern = kpmatch.compile("")
|
||||||
|
self.assertTrue(pattern.match(""))
|
||||||
|
self.assertFalse(pattern.match("index.html"))
|
||||||
|
self.assertFalse(pattern.match("about"))
|
||||||
|
self.assertFalse(pattern.match("about/about.html"))
|
||||||
|
self.assertFalse(pattern.match("content/deep/deeper/file.txt"))
|
||||||
|
|
||||||
|
def test_match_any(self):
|
||||||
|
pattern = kpmatch.compile("**")
|
||||||
|
self.assertTrue(pattern.match(""))
|
||||||
|
self.assertTrue(pattern.match("index.html"))
|
||||||
|
self.assertTrue(pattern.match("about"))
|
||||||
|
self.assertTrue(pattern.match("about/about.html"))
|
||||||
|
self.assertTrue(pattern.match("content/deep/deeper/file.txt"))
|
||||||
|
|
||||||
|
def test_match_any_name(self):
|
||||||
|
pattern = kpmatch.compile("*.html")
|
||||||
|
self.assertTrue(pattern.match("index.html"))
|
||||||
|
self.assertTrue(pattern.match("about.html"))
|
||||||
|
self.assertFalse(pattern.match("about"))
|
||||||
|
self.assertFalse(pattern.match("about/about.html"))
|
||||||
|
self.assertFalse(pattern.match("content/deep/deeper/file.txt"))
|
||||||
|
|
||||||
|
def test_match_any_name_anywhere(self):
|
||||||
|
pattern = kpmatch.compile("**/*.html")
|
||||||
|
self.assertTrue(pattern.match("index.html"))
|
||||||
|
self.assertTrue(pattern.match("about.html"))
|
||||||
|
self.assertFalse(pattern.match("about"))
|
||||||
|
self.assertTrue(pattern.match("about/about.html"))
|
||||||
|
self.assertTrue(pattern.match("docs/getting-started/installation.html"))
|
||||||
|
self.assertFalse(pattern.match("content/deep/deeper/file.txt"))
|
||||||
|
|
||||||
|
def test_match_any_directory_name(self):
|
||||||
|
pattern = kpmatch.compile("packages/*/package.json")
|
||||||
|
self.assertFalse(pattern.match("packages/package.json"))
|
||||||
|
self.assertTrue(pattern.match("packages/a/package.json"))
|
||||||
|
self.assertTrue(pattern.match("packages/b/package.json"))
|
||||||
|
self.assertFalse(pattern.match("packages/a/b/package.json"))
|
||||||
|
|
||||||
|
def test_match_one_of(self):
|
||||||
|
pattern = kpmatch.compile("*.{png,jpg,jpeg,webp}")
|
||||||
|
self.assertTrue(pattern.match("image.png"))
|
||||||
|
self.assertTrue(pattern.match("image.jpg"))
|
||||||
|
self.assertTrue(pattern.match("image.jpeg"))
|
||||||
|
self.assertTrue(pattern.match("image.webp"))
|
||||||
|
self.assertFalse(pattern.match("image.svg"))
|
||||||
|
|
||||||
|
def test_match_character_set(self):
|
||||||
|
pattern = kpmatch.compile("*.[jt]s")
|
||||||
|
self.assertTrue(pattern.match("file.js"))
|
||||||
|
self.assertTrue(pattern.match("file.ts"))
|
||||||
|
self.assertFalse(pattern.match("file.rs"))
|
||||||
|
self.assertFalse(pattern.match("file.cs"))
|
||||||
|
|
||||||
|
def test_match_negative_character_set(self):
|
||||||
|
pattern = kpmatch.compile("*.[!jt]s")
|
||||||
|
self.assertFalse(pattern.match("file.js"))
|
||||||
|
self.assertFalse(pattern.match("file.ts"))
|
||||||
|
self.assertTrue(pattern.match("file.rs"))
|
||||||
|
self.assertTrue(pattern.match("file.cs"))
|
||||||
|
|
||||||
|
def test_match_any_character(self):
|
||||||
|
pattern = kpmatch.compile("*.?s")
|
||||||
|
self.assertTrue(pattern.match("file.js"))
|
||||||
|
self.assertTrue(pattern.match("file.ts"))
|
||||||
|
self.assertTrue(pattern.match("file.rs"))
|
||||||
|
self.assertTrue(pattern.match("file.cs"))
|
||||||
222
test/parse.py
Normal file
222
test/parse.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import os.path
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from kpmatch import PatternParsingError
|
||||||
|
import kpmatch.matchers as m
|
||||||
|
import kpmatch.parse as p
|
||||||
|
|
||||||
|
|
||||||
|
def join_matchers(first: m.PathMatcher, *others: m.PathMatcher) -> m.PathMatcher:
|
||||||
|
for matcher in others:
|
||||||
|
first.append(matcher)
|
||||||
|
return first
|
||||||
|
|
||||||
|
|
||||||
|
class ParseTests(TestCase):
|
||||||
|
def test_parse_pattern(self):
|
||||||
|
# pattern <- (any_segments / fixed_segment)*
|
||||||
|
self.assertEqual(
|
||||||
|
p._parse_pattern("?a[bc]/**/*.{def,ghi}"),
|
||||||
|
join_matchers(
|
||||||
|
m.AnyCharacter(),
|
||||||
|
m.FixedName("a"),
|
||||||
|
m.CharacterSet("bc", False),
|
||||||
|
m.EndOfSegment(),
|
||||||
|
m.AnySegments(),
|
||||||
|
m.AnyName(),
|
||||||
|
m.FixedName("."),
|
||||||
|
m.OneOf((m.FixedName("def"), m.FixedName("ghi"))),
|
||||||
|
m.EndOfSegment(),
|
||||||
|
m.EndOfPath(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parse_any_segments(self):
|
||||||
|
# any_segments <- "**" (SEP / EOS)
|
||||||
|
self.assertTupleEqual(p._parse_any_segments("**", 0), (m.AnySegments(), 2))
|
||||||
|
self.assertTupleEqual(p._parse_any_segments("**/a", 0), (m.AnySegments(), 3))
|
||||||
|
self.assertTupleEqual(p._parse_any_segments("**a", 0), (None, 0))
|
||||||
|
|
||||||
|
def test_parse_fixed_segment(self):
|
||||||
|
# fixed_segment <- name_part+ (SEP / EOS)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_segment("?a[bc]/*.{def,ghi}", 0),
|
||||||
|
(
|
||||||
|
join_matchers(
|
||||||
|
m.AnyCharacter(),
|
||||||
|
m.FixedName("a"),
|
||||||
|
m.CharacterSet("bc", False),
|
||||||
|
m.EndOfSegment(),
|
||||||
|
),
|
||||||
|
7,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_segment("?a[bc]/*.{def,ghi}", 7),
|
||||||
|
(
|
||||||
|
join_matchers(
|
||||||
|
m.AnyName(),
|
||||||
|
m.FixedName("."),
|
||||||
|
m.OneOf((m.FixedName("def"), m.FixedName("ghi"))),
|
||||||
|
m.EndOfSegment(),
|
||||||
|
),
|
||||||
|
18,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parse_name_part(self):
|
||||||
|
# name_part <- any_name / one_of / simple_name_part / ","
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_name_part("?a[bc],*{def,ghi}", 0), (m.AnyCharacter(), 1)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_name_part("?a[bc],*{def,ghi}", 1), (m.FixedName("a"), 2)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_name_part("?a[bc],*{def,ghi}", 2), (m.CharacterSet("bc", False), 6)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_name_part("?a[bc],*{def,ghi}", 6), (m.FixedName(","), 7)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_name_part("?a[bc],*{def,ghi}", 7), (m.AnyName(), 8)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_name_part("?a[bc],*{def,ghi}", 8),
|
||||||
|
(m.OneOf((m.FixedName("def"), m.FixedName("ghi"))), 17),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parse_any_name(self):
|
||||||
|
# any_name = "*"+
|
||||||
|
self.assertTupleEqual(p._parse_any_name("*a", 0), (m.AnyName(), 1))
|
||||||
|
self.assertTupleEqual(p._parse_any_name("a*", 1), (m.AnyName(), 2))
|
||||||
|
self.assertTupleEqual(p._parse_any_name("a***a", 1), (m.AnyName(), 4))
|
||||||
|
|
||||||
|
def test_parse_one_of(self):
|
||||||
|
# one_of = "{" simple_name_part+ ("," simple_name_part+)* "}"
|
||||||
|
self.assertTupleEqual(p._parse_one_of("", 0), (None, 0))
|
||||||
|
self.assertTupleEqual(p._parse_one_of("abc,def}", 0), (None, 0))
|
||||||
|
self.assertTupleEqual(p._parse_one_of("{abc,def", 0), (None, 0))
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_one_of("{abc,def}", 0),
|
||||||
|
(m.OneOf((m.FixedName("abc"), m.FixedName("def"))), 9),
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_one_of("{a[bc],d?f,ghi}", 0),
|
||||||
|
(
|
||||||
|
m.OneOf(
|
||||||
|
(
|
||||||
|
join_matchers(m.FixedName("a"), m.CharacterSet("bc", False)),
|
||||||
|
join_matchers(
|
||||||
|
m.FixedName("d"), m.AnyCharacter(), m.FixedName("f")
|
||||||
|
),
|
||||||
|
m.FixedName("ghi"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
15,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertRaises(PatternParsingError, p._parse_one_of, "{abc{def}", 0)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_one_of("{abc{def}", 4), (m.OneOf((m.FixedName("def"),)), 9)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_one_of("{abc}def}", 0), (m.OneOf((m.FixedName("abc"),)), 5)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_one_of(os.path.join("{abc", "def}"), 0),
|
||||||
|
(None, 0),
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(p._parse_one_of("{,def", 0), (None, 0))
|
||||||
|
self.assertRaises(PatternParsingError, p._parse_one_of, "{,def}", 0)
|
||||||
|
|
||||||
|
def test_parse_one_of_choice(self):
|
||||||
|
# one_of_choice = simple_name_part+
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_one_of_choice("?a[bc],def", 0),
|
||||||
|
(
|
||||||
|
join_matchers(
|
||||||
|
m.AnyCharacter(), m.FixedName("a"), m.CharacterSet("bc", False)
|
||||||
|
),
|
||||||
|
6,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parse_simple_name_part(self):
|
||||||
|
# simple_name_part = any_character / character_set / fixed_name
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_simple_name_part("?a[bc]", 0), (m.AnyCharacter(), 1)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_simple_name_part("?a[bc]", 1), (m.FixedName("a"), 2)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_simple_name_part("?a[bc]", 2), (m.CharacterSet("bc", False), 6)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parse_any_character(self):
|
||||||
|
self.assertTupleEqual(p._parse_any_character("?a", 0), (m.AnyCharacter(), 1))
|
||||||
|
self.assertTupleEqual(p._parse_any_character("a?", 1), (m.AnyCharacter(), 2))
|
||||||
|
|
||||||
|
def test_parse_character_set(self):
|
||||||
|
self.assertTupleEqual(p._parse_character_set("", 0), (None, 0))
|
||||||
|
self.assertTupleEqual(p._parse_character_set("abc]", 0), (None, 0))
|
||||||
|
self.assertTupleEqual(p._parse_character_set("[abc", 0), (None, 0))
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_character_set("[abcdef]", 0), (m.CharacterSet("abcdef", False), 8)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_character_set("[!abcdef]", 0), (m.CharacterSet("abcdef", True), 9)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(p._parse_character_set("[abcdef]", 3), (None, 3))
|
||||||
|
self.assertRaises(PatternParsingError, p._parse_character_set, "[abc[def]", 0)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_character_set("[abc[def]", 4), (m.CharacterSet("def", False), 9)
|
||||||
|
)
|
||||||
|
self.assertRaises(PatternParsingError, p._parse_character_set, "[abc!def]", 0)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_character_set("[abc]def]", 0), (m.CharacterSet("abc", False), 5)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_character_set(os.path.join("[abc", "def]"), 0),
|
||||||
|
(None, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parse_fixed_name(self):
|
||||||
|
self.assertTupleEqual(p._parse_fixed_name("", 0), (None, 0))
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name("abcdef", 0), (m.FixedName("abcdef"), 6)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name("abcdef", 2), (m.FixedName("cdef"), 6)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name("abc[def", 0), (m.FixedName("abc"), 3)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name("abc!def", 0), (m.FixedName("abc"), 3)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name("abc]def", 0), (m.FixedName("abc"), 3)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name("abc{def", 0), (m.FixedName("abc"), 3)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name("abc,def", 0), (m.FixedName("abc"), 3)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name("abc}def", 0), (m.FixedName("abc"), 3)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name("abc*def", 0), (m.FixedName("abc"), 3)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name("abc?def", 0), (m.FixedName("abc"), 3)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name(os.path.join("abc", "def"), 0), (m.FixedName("abc"), 3)
|
||||||
|
)
|
||||||
|
self.assertTupleEqual(
|
||||||
|
p._parse_fixed_name(os.path.join("abc", "def"), 2), (m.FixedName("c"), 3)
|
||||||
|
)
|
||||||
60
test/public_api.py
Normal file
60
test/public_api.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import fnmatch
|
||||||
|
from configparser import ParsingError
|
||||||
|
from typing import Callable, Any
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
import kpmatch
|
||||||
|
from kpmatch import PatternParsingError
|
||||||
|
|
||||||
|
|
||||||
|
class ParseTests(TestCase):
|
||||||
|
def test_compile(self):
|
||||||
|
pattern = kpmatch.compile("*.txt")
|
||||||
|
self.assertIsInstance(pattern, kpmatch.Pattern)
|
||||||
|
self.assertTrue(pattern.match("file.txt"))
|
||||||
|
self.assertEqual(pattern.specificity, 12)
|
||||||
|
|
||||||
|
def test_kpmatch(self):
|
||||||
|
self.assertTrue(kpmatch.kpmatch("file.txt", "*.txt"))
|
||||||
|
self.assertFalse(kpmatch.kpmatch("file.jpg", "*.txt"))
|
||||||
|
|
||||||
|
def test_is_valid_pattern(self):
|
||||||
|
self.assertTrue(kpmatch.is_valid_pattern("*.txt"))
|
||||||
|
self.assertFalse(kpmatch.is_valid_pattern("[.txt"))
|
||||||
|
|
||||||
|
def test_specificity(self):
|
||||||
|
self.assertEqual(kpmatch.specificity(""), 0)
|
||||||
|
self.assertEqual(kpmatch.specificity("**"), 1)
|
||||||
|
self.assertEqual(kpmatch.specificity("*.txt"), 12)
|
||||||
|
self.assertEqual(kpmatch.specificity("file.txt"), 15)
|
||||||
|
|
||||||
|
def assertRaisesPPE(self, error_message: str, position: int, pattern: str):
|
||||||
|
with self.assertRaises(PatternParsingError) as cm:
|
||||||
|
kpmatch.compile(pattern)
|
||||||
|
self.assertEqual(
|
||||||
|
str(PatternParsingError(error_message, pattern, position)),
|
||||||
|
str(cm.exception),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_error_messages(self):
|
||||||
|
self.assertRaisesPPE("Empty path segment", 4, "abc//def")
|
||||||
|
|
||||||
|
self.assertRaisesPPE('Missing closing bracket "]" to match "["', 3, "abc[def")
|
||||||
|
self.assertRaisesPPE(
|
||||||
|
'Character "!" is not allowed inside a character set', 6, "abc[de!f]"
|
||||||
|
)
|
||||||
|
self.assertRaisesPPE(
|
||||||
|
'Character "[" is not allowed inside a character set', 6, "abc[de[f]"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaisesPPE('Missing closing bracket "}" to match "{"', 3, "abc{def")
|
||||||
|
self.assertRaisesPPE(
|
||||||
|
'Character "{" is not allowed inside a one-of pattern', 6, "abc{de{f}"
|
||||||
|
)
|
||||||
|
self.assertRaisesPPE(
|
||||||
|
'A star "*" wildcard is not allowed inside a one-of pattern',
|
||||||
|
8,
|
||||||
|
"abc{def,*}",
|
||||||
|
)
|
||||||
|
self.assertRaisesPPE("Empty choice in a one-of pattern", 8, "abc{def,}")
|
||||||
|
self.assertRaisesPPE("Empty choice in a one-of pattern", 4, "abc{,def}")
|
||||||
21
test/specificity.py
Normal file
21
test/specificity.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from kpmatch import specificity
|
||||||
|
|
||||||
|
|
||||||
|
class SpecificityTests(TestCase):
|
||||||
|
def test_compare_specificity(self):
|
||||||
|
self.assertGreater(specificity("**"), specificity(""))
|
||||||
|
self.assertGreater(specificity("**/a"), specificity("**"))
|
||||||
|
self.assertGreater(specificity("**/a/file.txt"), specificity("**/a/*.txt"))
|
||||||
|
self.assertGreater(specificity("file.txt"), specificity("*.txt"))
|
||||||
|
self.assertGreater(specificity("*.txt"), specificity("*"))
|
||||||
|
self.assertGreater(specificity("file.{txt,html}"), specificity("*.txt"))
|
||||||
|
self.assertGreater(specificity("image_???.png"), specificity("image_*.png"))
|
||||||
|
self.assertGreater(specificity("image.png"), specificity("image_???.png"))
|
||||||
|
|
||||||
|
self.assertEqual(specificity("**/a"), specificity("**/b"))
|
||||||
|
self.assertEqual(specificity("*.txt"), specificity("*.html"))
|
||||||
|
self.assertEqual(
|
||||||
|
specificity("image_?.png"), specificity("image_[0123456789].png")
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user