Repository navigation
Expand file tree
/
Copy pathterm.py
More file actions
183 lines (138 loc) · 5.47 KB
/
Copy pathterm.py
File metadata and controls
183 lines (138 loc) · 5.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import re
from abc import ABC, abstractmethod
from re import Pattern
from typing import Any, Optional
from regexsolver._generated.models import Term as GeneratedTerm
from regexsolver._generated.models.term_fair import TermFair
from regexsolver._generated.models.term_regex import TermRegex
from regexsolver.models.cardinality import Cardinality
from regexsolver.models.length import Length
from regexsolver.models.term_properties_mixin import TermPropertiesMixin
EMPTY_LANGUAGE_PATTERN = "[]"
"""How the engine renders a language that matches no string at all."""
class Term(ABC):
"""Represents a mathematical term (Regex or FAIR) on which operations can be performed."""
def __init__(self, value: str):
self._value = value
# Shared Cache (Internal)
self._cardinality: Optional[Cardinality] = None
self._length: Optional[Length] = None
self._empty: Optional[bool] = None
self._empty_string: Optional[bool] = None
self._total: Optional[bool] = None
self._pattern: Optional[str] = None
self._dot: Optional[str] = None
self._compiled_regex: Optional[Pattern] = None
@abstractmethod
def get_pattern(self) -> Optional[str]:
pass
@abstractmethod
def get_fair(self) -> Optional[str]:
pass
@abstractmethod
def to_dto(self) -> GeneratedTerm:
pass
@abstractmethod
def serialize(self) -> str:
pass
@classmethod
def regex(cls, pattern: str) -> "Term":
return RegexTerm(pattern)
@classmethod
def fair(cls, payload: str) -> "Term":
return FairTerm(payload)
# --- Shared Behavior ---
def get_value(self) -> str:
return self._value
def _set_properties_mixin(self, properties_mixin: TermPropertiesMixin):
empty = properties_mixin.is_empty()
if empty is not None:
self._empty = empty
empty_string = properties_mixin.is_empty_string()
if empty_string is not None:
self._empty_string = empty_string
total = properties_mixin.is_total()
if total is not None:
self._total = total
def matches(self, string: str) -> bool:
"""Client-side matching implementation."""
pattern = self.get_pattern()
if pattern is None:
raise RuntimeError(
"The regex pattern of this term is not defined yet, call get_pattern() on the client to set it."
)
# The engine renders the empty language as "[]". By definition it matches
# nothing, and `re` rejects the pattern outright.
if pattern == EMPTY_LANGUAGE_PATTERN:
return False
if self._compiled_regex is None:
try:
self._compiled_regex = re.compile(rf"\A(?:{pattern})\Z", re.DOTALL)
except re.error as e:
raise ValueError(
f"Invalid regular expression for Python's `re` engine: {pattern}"
) from e
return self._compiled_regex.match(string) is not None
@classmethod
def deserialize(cls, serialized: str) -> Optional["Term"]:
if not serialized or "=" not in serialized:
return None
index = serialized.find("=")
type_str = serialized[:index]
val = serialized[index + 1 :]
if type_str.lower() == "regex":
return cls.regex(val)
elif type_str.lower() == "fair":
return cls.fair(val)
return None
@classmethod
def from_dto(cls, dto: GeneratedTerm) -> "Term":
actual_instance = dto.actual_instance
if actual_instance is None:
raise RuntimeError("Invalid Term DTO provided.")
if isinstance(actual_instance, TermFair):
deterministic = (
actual_instance.metadata.deterministic
if actual_instance.metadata is not None
else None
)
return FairTerm(actual_instance.value, deterministic=deterministic)
return cls.regex(actual_instance.value)
# --- Shared Getters/Setters ---
def __eq__(self, other: Any) -> bool:
if self is other:
return True
if not isinstance(other, Term):
return False
return self.serialize() == other.serialize()
def __hash__(self) -> int:
return hash(self.serialize())
def __str__(self) -> str:
return self.serialize()
def __repr__(self) -> str:
return self.serialize()
class RegexTerm(Term):
def get_pattern(self) -> Optional[str]:
return self.get_value()
def get_fair(self) -> Optional[str]:
return None
def to_dto(self) -> GeneratedTerm:
return GeneratedTerm(TermRegex(type="regex", value=self.get_value()))
def serialize(self) -> str:
return f"regex={self.get_value()}"
class FairTerm(Term):
def __init__(self, value: str, deterministic: Optional[bool] = None):
super().__init__(value)
self._deterministic = deterministic
@property
def is_deterministic(self) -> Optional[bool]:
"""Whether this FAIR encodes a deterministic automaton, or None if unknown."""
return self._deterministic
def get_pattern(self) -> Optional[str]:
return self._pattern
def get_fair(self) -> Optional[str]:
return self.get_value()
def to_dto(self) -> GeneratedTerm:
return GeneratedTerm(TermFair(type="fair", value=self.get_value()))
def serialize(self) -> str:
return f"fair={self.get_value()}"