Skip to content

Commit

Permalink
Add typing, remove old pickle format, expand testing
Browse files Browse the repository at this point in the history
  • Loading branch information
linuxdaemon committed Apr 4, 2024
1 parent 2253e2b commit ff2f88f
Show file tree
Hide file tree
Showing 17 changed files with 523 additions and 247 deletions.
11 changes: 8 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,22 @@
"EditorConfig.EditorConfig",
"GitHub.vscode-pull-request-github",
"github.vscode-github-actions",
"redhat.vscode-yaml"
"redhat.vscode-yaml",
"ms-python.black-formatter",
"ms-python.isort"
],
"settings": {
"python.pythonPath": "~/.virtualenvs/polymatch/bin/python",
"python.defaultInterpreterPath": "~/.virtualenvs/polymatch/bin/python",
"python.testing.pytestArgs": ["--no-cov"],
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/usr/bin/zsh"
}
},
"terminal.integrated.defaultProfile.linux": "zsh"
"terminal.integrated.defaultProfile.linux": "zsh",
"[python]": {
"editor.defaultFormatter": "ms-python.python"
}
}
}
}
Expand Down
27 changes: 21 additions & 6 deletions polymatch/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
from . import error
from .base import CaseAction, PolymorphicMatcher
from .error import *
from .registry import pattern_registry
from polymatch.base import CaseAction, PolymorphicMatcher
from polymatch.error import (
DuplicateMatcherRegistrationError,
NoMatchersAvailableError,
NoSuchMatcherError,
PatternCompileError,
PatternNotCompiledError,
PatternTextTypeMismatchError,
)
from polymatch.registry import pattern_registry

__version__ = "0.2.0"

__all__ = ("PolymorphicMatcher", "CaseAction", "pattern_registry")
__all__ += error.__all__
__all__ = [
"PolymorphicMatcher",
"CaseAction",
"pattern_registry",
"NoSuchMatcherError",
"NoMatchersAvailableError",
"PatternNotCompiledError",
"PatternCompileError",
"PatternTextTypeMismatchError",
"DuplicateMatcherRegistrationError",
]
162 changes: 102 additions & 60 deletions polymatch/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from abc import ABCMeta, abstractmethod
from enum import Enum
from typing import AnyStr, Callable, Generic, Optional, Tuple, Type, TypeVar

import polymatch
from polymatch.error import (
Expand All @@ -16,101 +17,130 @@ class CaseAction(Enum):
CASEFOLD = "casefold", "cf" # Force case-folded comparison


class PolymorphicMatcher(metaclass=ABCMeta):
AnyPattern = TypeVar("AnyPattern")

TUPLE_V1 = Tuple[AnyStr, CaseAction, bool, AnyPattern, Type[AnyStr], object]
TUPLE_V2 = Tuple[
str, AnyStr, CaseAction, bool, Optional[AnyPattern], Type[AnyStr], object
]

CompileFunc = Callable[[AnyStr], AnyPattern]
MatchFunc = Callable[[AnyPattern, AnyStr], bool]

FuncTuple = Tuple[CompileFunc[AnyStr, AnyPattern], MatchFunc[AnyPattern, AnyStr]]


class PolymorphicMatcher(Generic[AnyStr, AnyPattern], metaclass=ABCMeta):
_empty = object()

def __init__(self, pattern, case_action=CaseAction.NONE, invert=False):
self._raw_pattern = pattern
self._str_type = type(pattern)
self._compiled_pattern = self._empty
def __init__(
self,
pattern: AnyStr,
/,
case_action: CaseAction = CaseAction.NONE,
*,
invert: bool = False,
) -> None:
self._raw_pattern: AnyStr = pattern
self._str_type: Type[AnyStr] = type(pattern)
self._compiled_pattern: Optional[AnyPattern] = None
self._case_action = case_action
self._invert = invert

self._compile_func, self._match_func = self._get_case_functions()
funcs: FuncTuple[AnyStr, AnyPattern] = self._get_case_functions()
self._compile_func: CompileFunc[AnyStr, AnyPattern] = funcs[0]
self._match_func: MatchFunc[AnyPattern, AnyStr] = funcs[1]

if self._case_action is CaseAction.CASEFOLD and self._str_type is bytes:
raise TypeError("Case-folding is not supported with bytes patterns")
msg = "Case-folding is not supported with bytes patterns"
raise TypeError(msg)

def try_compile(self):
def try_compile(self) -> bool:
try:
self.compile()
except PatternCompileError:
return False

return True

def compile(self):
def compile(self) -> None: # noqa: A003
try:
self._compiled_pattern = self._compile_func(self._raw_pattern)
except Exception as e:
raise PatternCompileError(
f"Failed to compile pattern {self._raw_pattern!r}"
) from e
self._compiled_pattern = self._compile_func(self.pattern)
except Exception as e: # noqa: BLE001
msg = f"Failed to compile pattern {self.pattern!r}"
raise PatternCompileError(msg) from e

def __eq__(self, other):
def __eq__(self, other: object) -> bool:
if isinstance(other, self._str_type):
return self.match(other)

return NotImplemented

def __ne__(self, other):
def __ne__(self, other: object) -> bool:
if isinstance(other, self._str_type):
return not self.match(other)

return NotImplemented

def match(self, text):
def match(self, text: AnyStr) -> bool:
if not isinstance(text, self._str_type):
raise PatternTextTypeMismatchError(self._str_type, type(text))

if not self.is_compiled():
if self._compiled_pattern is None:
# If it wasn't compiled
raise PatternNotCompiledError("Pattern must be compiled.")
msg = "Pattern must be compiled."
raise PatternNotCompiledError(msg)

out = self._match_func(self._compiled_pattern, text)

if self._invert:
if self.inverted:
return not out

return out

def is_compiled(self):
return self._compiled_pattern is not self._empty
def is_compiled(self) -> bool:
return self._compiled_pattern is not None

@abstractmethod
def compile_pattern(self, raw_pattern):
def compile_pattern(self, raw_pattern: AnyStr) -> AnyPattern:
raise NotImplementedError

@abstractmethod
def compile_pattern_cs(self, raw_pattern):
def compile_pattern_cs(self, raw_pattern: AnyStr) -> AnyPattern:
"""Matchers should override this to compile their pattern with case-sensitive options"""
raise NotImplementedError

@abstractmethod
def compile_pattern_ci(self, raw_pattern):
def compile_pattern_ci(self, raw_pattern: AnyStr) -> AnyPattern:
"""Matchers should override this to compile their pattern with case-insensitive options"""
raise NotImplementedError

@abstractmethod
def compile_pattern_cf(self, raw_pattern):
def compile_pattern_cf(self, raw_pattern: AnyStr) -> AnyPattern:
"""Matchers should override this to compile their pattern with case-folding options"""
raise NotImplementedError

@abstractmethod
def match_text(self, pattern, text):
def match_text(self, pattern: AnyPattern, text: AnyStr) -> bool:
raise NotImplementedError

def match_text_cs(self, pattern, text):
def match_text_cs(self, pattern: AnyPattern, text: AnyStr) -> bool:
return self.match_text(pattern, text)

def match_text_ci(self, pattern, text):
def match_text_ci(self, pattern: AnyPattern, text: AnyStr) -> bool:
return self.match_text(pattern, text.lower())

def match_text_cf(self, pattern, text):
def match_text_cf(self, pattern: AnyPattern, text: AnyStr) -> bool:
if isinstance(text, bytes):
msg = "Casefold is not supported on bytes patterns"
raise TypeError(msg)

return self.match_text(pattern, text.casefold())

def _get_case_functions(self):
suffix = self._case_action.value[1]
def _get_case_functions(
self,
) -> Tuple[CompileFunc[AnyStr, AnyPattern], MatchFunc[AnyPattern, AnyStr]]:
suffix = self.case_action.value[1]

if suffix:
suffix = "_" + suffix
Expand All @@ -121,62 +151,74 @@ def _get_case_functions(self):

@classmethod
@abstractmethod
def get_type(cls):
def get_type(cls) -> str:
raise NotImplementedError

@property
def pattern(self):
def pattern(self) -> AnyStr:
return self._raw_pattern

@property
def case_action(self):
def case_action(self) -> CaseAction:
return self._case_action

@property
def inverted(self):
def inverted(self) -> bool:
return self._invert

def __str__(self):
return "{}{}:{}:{}".format(
"~" if self._invert else "",
self.get_type(),
self._case_action.value[1],
self._raw_pattern,
)
def to_string(self) -> AnyStr:
if isinstance(self.pattern, str):
return "{}{}:{}:{}".format(
"~" if self.inverted else "",
self.get_type(),
self.case_action.value[1],
self.pattern,
)

def __repr__(self):
return "{}(pattern={!r}, case_action={!r}, invert={!r})".format(
type(self).__name__,
self._raw_pattern,
self._case_action,
self._invert,
return (
"{}{}:{}:".format(
"~" if self.inverted else "", self.get_type(), self.case_action.value[1]
)
).encode() + self.pattern

def __str__(self) -> str:
res = self.to_string()
if isinstance(res, str):
return res

return res.decode()

def __repr__(self) -> str:
return "{}(pattern={!r}, case_action={}, invert={!r})".format(
type(self).__name__, self.pattern, self.case_action, self.inverted
)

def __getstate__(self):
def __getstate__(self) -> TUPLE_V2[AnyStr, AnyPattern]:
return (
polymatch.__version__,
self._raw_pattern,
self._case_action,
self._invert,
self.pattern,
self.case_action,
self.inverted,
self._compiled_pattern,
self._str_type,
self._empty,
)

def __setstate__(self, state):
if len(state) > 6:
version, *state = state
else:
version = "0.0.0"

def __setstate__(self, state: TUPLE_V2[AnyStr, AnyPattern]) -> None:
(
version,
self._raw_pattern,
self._case_action,
self._invert,
self._compiled_pattern,
_compiled_pattern,
self._str_type,
self._empty,
) = state
# This is compatibility code, we can't serialize a pickled object to match this
if _compiled_pattern is self._empty: # pragma: no cover
_compiled_pattern = None

self._compiled_pattern = _compiled_pattern
self._compile_func, self._match_func = self._get_case_functions()

if version != polymatch.__version__ and self.is_compiled():
Expand Down
19 changes: 13 additions & 6 deletions polymatch/error.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
__all__ = (
from typing import TYPE_CHECKING, AnyStr, Type

if TYPE_CHECKING:
from polymatch.base import AnyPattern

__all__ = [
"PatternCompileError",
"PatternNotCompiledError",
"PatternTextTypeMismatchError",
"DuplicateMatcherRegistrationError",
"NoSuchMatcherError",
"NoMatchersAvailable",
)
"NoMatchersAvailableError",
]


class PatternCompileError(ValueError):
Expand All @@ -17,7 +22,9 @@ class PatternNotCompiledError(ValueError):


class PatternTextTypeMismatchError(TypeError):
def __init__(self, pattern_type, text_type):
def __init__(
self, pattern_type: "Type[AnyPattern]", text_type: Type[AnyStr]
) -> None:
super().__init__(
"Pattern of type {!r} can not match text of type {!r}".format(
pattern_type.__name__, text_type.__name__
Expand All @@ -26,13 +33,13 @@ def __init__(self, pattern_type, text_type):


class DuplicateMatcherRegistrationError(ValueError):
def __init__(self, name):
def __init__(self, name: str) -> None:
super().__init__(f"Attempted o register a duplicate matcher {name!r}")


class NoSuchMatcherError(LookupError):
pass


class NoMatchersAvailable(ValueError):
class NoMatchersAvailableError(ValueError):
pass
Loading

0 comments on commit ff2f88f

Please sign in to comment.