Skip to content

Commit

Permalink
docs: add docstrings to all public API methods
Browse files Browse the repository at this point in the history
  • Loading branch information
linuxdaemon committed Apr 17, 2024
1 parent 807ce89 commit 74be5ad
Show file tree
Hide file tree
Showing 19 changed files with 329 additions and 12 deletions.
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"ms-python.black-formatter",
"ms-python.isort",
"ms-azuretools.vscode-docker",
"ms-python.mypy-type-checker"
"ms-python.mypy-type-checker",
"sunipkm.autodocstringpy"
],
"settings": {
"python.defaultInterpreterPath": "~/.virtualenvs/polymatch/bin/python",
Expand Down
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"ms-python.black-formatter",
"ms-python.isort",
"ms-azuretools.vscode-docker",
"ms-python.mypy-type-checker"
"ms-python.mypy-type-checker",
"sunipkm.autodocstringpy"
]
}
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@
"editor.wordBasedSuggestions": "off",
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.analysis.diagnosticMode": "workspace"
"python.analysis.diagnosticMode": "workspace",
"autoDocstringPy.docstringFormat": "google-notypes",
"autoDocstringPy.startOnNewLine": true
}
2 changes: 2 additions & 0 deletions polymatch/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Polymorphic pattern matchers."""

from polymatch._version import (
__version__,
__version_tuple__,
Expand Down
133 changes: 130 additions & 3 deletions polymatch/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"""Base matcher classes.
Matchers should implement `PolymorphicMatcher`.
"""

from abc import ABCMeta, abstractmethod
from enum import Enum
from typing import (
Expand All @@ -20,6 +25,8 @@


class CaseAction(Enum):
"""Pattern case matching action."""

NONE = "none", "" # Use whatever the pattern's default is
CASESENSITIVE = "case-sensitive", "cs" # Fore case sensitivity
CASEINSENSITIVE = "case-insensitive", "ci" # Force case insensitivity
Expand All @@ -42,6 +49,8 @@ class CaseAction(Enum):


class PolymorphicMatcher(Generic[AnyStr, AnyPattern], metaclass=ABCMeta):
"""Base Matcher which defines the structure and protocol for polymorphic matchers."""

_empty = object()

def _get_case_functions(
Expand Down Expand Up @@ -69,6 +78,17 @@ def __init__(
*,
invert: bool = False,
) -> None:
"""Construct a pattern object.
Args:
pattern: Pattern text to use.
case_action: Case-sensitivity setting to use, NONE means to use the matcher's default.
Defaults to CaseAction.NONE.
invert: Whether the pattern match is inverted. Defaults to False.
Raises:
TypeError: If a bytes pattern is provided and casefolding is requested.
"""
self._raw_pattern: AnyStr = pattern
self._str_type: Type[AnyStr] = type(pattern)
self._compiled_pattern: Optional[AnyPattern] = None
Expand All @@ -84,21 +104,46 @@ def __init__(
raise TypeError(msg)

def try_compile(self) -> bool:
"""Attempt to compile the pattern.
Returns:
True if the pattern compiled successfully, False otherwise
"""
try:
self.compile()
except PatternCompileError:
return False

return True

# TODO(linuxdaemon): #59 deprecate and replace with a non-conflicting name.
# https://github.com/TotallyNotRobots/poly-match/issues/59
def compile(self) -> None: # noqa: A003
"""Compile the pattern using the implementations compile_{ci,cf,cs} methods.
Raises:
PatternCompileError: If an error occurs while compiling the pattern.
"""
try:
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 match(self, text: AnyStr) -> bool:
"""Match text against the configured matcher.
Args:
text: Text to match
Raises:
PatternTextTypeMismatchError: If the type of `text` doesn't
match the pattern string type
PatternNotCompiledError: If the matcher pattern has not yet been compiled
Returns:
Whether the pattern matches the input text
"""
if not isinstance(text, self._str_type):
raise PatternTextTypeMismatchError(self._str_type, type(text))

Expand All @@ -115,38 +160,71 @@ def match(self, text: AnyStr) -> bool:
return out

def is_compiled(self) -> bool:
"""Whether the pattern is compiled."""
return self._compiled_pattern is not None

@abstractmethod
def compile_pattern(self, raw_pattern: AnyStr) -> AnyPattern:
"""Matchers must override this to compile their pattern with default case-sensitivity."""
raise NotImplementedError

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

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

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

@abstractmethod
def match_text(self, pattern: AnyPattern, text: AnyStr) -> bool:
"""Matchers must implement this to match their pattern against the text input."""
raise NotImplementedError

def match_text_cs(self, pattern: AnyPattern, text: AnyStr) -> bool:
"""Default implementation, passes the input unchanged.
Args:
pattern: Pattern to match against
text: Text input to check
Returns:
Whether the pattern matches the text
"""
return self.match_text(pattern, text)

def match_text_ci(self, pattern: AnyPattern, text: AnyStr) -> bool:
"""Default implementation, .lower()'s the input text.
Args:
pattern: Pattern to match against
text: Text input to check
Returns:
Whether the pattern matches the text
"""
return self.match_text(pattern, text.lower())

def match_text_cf(self, pattern: AnyPattern, text: AnyStr) -> bool:
"""Default implementation, case-folds the input text.
Args:
pattern: Pattern to match against
text: Text input to check
Raises:
TypeError: If a bytes object is passed for case-folding
Returns:
Whether the pattern matches the text
"""
if isinstance(text, bytes):
msg = "Casefold is not supported on bytes patterns"
raise TypeError(msg)
Expand All @@ -156,21 +234,36 @@ def match_text_cf(self, pattern: AnyPattern, text: AnyStr) -> bool:
@classmethod
@abstractmethod
def get_type(cls) -> str:
"""Get pattern type.
Implementations must implement this.
Returns:
The pattern type name
"""
raise NotImplementedError

@property
def pattern(self) -> AnyStr:
"""Raw, uncompiled pattern text."""
return self._raw_pattern

@property
def case_action(self) -> CaseAction:
"""Configured case-sensitivity setting."""
return self._case_action

@property
def inverted(self) -> bool:
"""Whether the pattern is inverted."""
return self._invert

def to_string(self) -> AnyStr:
"""Generate pattern string representation, which can be passed to `pattern_from_string()`.
Returns:
The pattern string text
"""
if isinstance(self.pattern, str):
return "{}{}:{}:{}".format(
"~" if self.inverted else "",
Expand All @@ -188,18 +281,39 @@ def to_string(self) -> AnyStr:
).encode() + self.pattern

def __eq__(self, other: object) -> bool:
"""Compare against input text.
Args:
other: The text to compare against
Returns:
Whether the other object matches this pattern
"""
if isinstance(other, self._str_type):
return self.match(other)

return NotImplemented

def __ne__(self, other: object) -> bool:
"""Compare against input text.
Args:
other: The text to compare against
Returns:
Whether the other object doesn't match this pattern
"""
if isinstance(other, self._str_type):
return not self.match(other)

return NotImplemented

def __getstate__(self) -> TUPLE_V2[AnyStr, AnyPattern]:
"""Generate object state for pickling.
Returns:
A tuple representing the current object state, used by __setstate__(),\
"""
return (
polymatch.__version__,
self.pattern,
Expand All @@ -211,6 +325,13 @@ def __getstate__(self) -> TUPLE_V2[AnyStr, AnyPattern]:
)

def __setstate__(self, state: TUPLE_V2[AnyStr, AnyPattern]) -> None:
"""Configure object based on the `state` tuple.
This is used when un-pickling the pattern objects.
Args:
state: State to set
"""
(
version,
self._raw_pattern,
Expand All @@ -231,11 +352,17 @@ def __setstate__(self, state: TUPLE_V2[AnyStr, AnyPattern]) -> None:
self.compile()

def __repr__(self) -> str:
"""Represent the matcher object in an informative way, useful for debugging.
Returns:
The repr for this object
"""
return "{}(pattern={!r}, case_action={}, invert={!r})".format(
type(self).__name__, self.pattern, self.case_action, self.inverted
)

def __str__(self) -> str:
"""Represent the pattern as its pattern string, to be passed to `pattern_from_string()`."""
res = self.to_string()
if isinstance(res, str):
return res
Expand Down
25 changes: 21 additions & 4 deletions polymatch/error.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Exceptions raised by the library."""

from typing import TYPE_CHECKING, AnyStr, Type

if TYPE_CHECKING:
Expand All @@ -14,17 +16,25 @@


class PatternCompileError(ValueError):
pass
"""Error used when a pattern fails to compile."""


class PatternNotCompiledError(ValueError):
pass
"""Error for when a pattern is used without compiling."""


class PatternTextTypeMismatchError(TypeError):
"""Error for when pattern text type doesn't match input text type."""

def __init__(
self, pattern_type: "Type[AnyPattern]", text_type: Type[AnyStr]
) -> None:
"""Construct pattern type mismatch error.
Arguments:
pattern_type: Pattern matcher type
text_type: Input text type
"""
super().__init__(
"Pattern of type {!r} can not match text of type {!r}".format(
pattern_type.__name__, text_type.__name__
Expand All @@ -33,13 +43,20 @@ def __init__(


class DuplicateMatcherRegistrationError(ValueError):
"""Error for when a duplicate pattern type is registered."""

def __init__(self, name: str) -> None:
"""Construct duplicate registration error.
Args:
name: name of the duplicate matcher
"""
super().__init__(f"Attempted o register a duplicate matcher {name!r}")


class NoSuchMatcherError(LookupError):
pass
"""Matcher not found."""


class NoMatchersAvailableError(ValueError):
pass
"""No matchers to query against."""
1 change: 1 addition & 0 deletions polymatch/matchers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Core matcher implementations."""
9 changes: 9 additions & 0 deletions polymatch/matchers/glob.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Glob pattern matcher."""

from fnmatch import translate
from typing import TYPE_CHECKING, AnyStr

Expand All @@ -8,9 +10,15 @@


class GlobMatcher(RegexMatcher[AnyStr]):
"""Match glob patterns.
Implemented as a subclass of RegexMatcher, glob patterns are translated to regex on compilation.
"""

def compile_pattern(
self, raw_pattern: AnyStr, *, flags: int = 0
) -> "regex.Pattern[AnyStr]":
"""Override RegexMatcher compile to transform glob-syntax."""
if isinstance(raw_pattern, str):
res = translate(raw_pattern)
else:
Expand All @@ -23,4 +31,5 @@ def compile_pattern(

@classmethod
def get_type(cls) -> str:
"""Pattern type."""
return "glob"
Loading

0 comments on commit 74be5ad

Please sign in to comment.