You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

416 lines
13 KiB
Python

import unicodedata
import os
from itertools import product
from collections import deque
from typing import Callable, Iterator, List, Optional, Tuple, Type, TypeVar, Union, Dict, Any, Sequence, Iterable, AbstractSet
###{standalone
import sys, re
import logging
from dataclasses import dataclass
from typing import Generic, AnyStr
logger: logging.Logger = logging.getLogger("lark")
logger.addHandler(logging.StreamHandler())
# Set to highest level, since we have some warnings amongst the code
# By default, we should not output any log messages
logger.setLevel(logging.CRITICAL)
NO_VALUE = object()
T = TypeVar("T")
def classify(seq: Iterable, key: Optional[Callable] = None, value: Optional[Callable] = None) -> Dict:
d: Dict[Any, Any] = {}
for item in seq:
k = key(item) if (key is not None) else item
v = value(item) if (value is not None) else item
try:
d[k].append(v)
except KeyError:
d[k] = [v]
return d
def _deserialize(data: Any, namespace: Dict[str, Any], memo: Dict) -> Any:
if isinstance(data, dict):
if '__type__' in data: # Object
class_ = namespace[data['__type__']]
return class_.deserialize(data, memo)
elif '@' in data:
return memo[data['@']]
return {key:_deserialize(value, namespace, memo) for key, value in data.items()}
elif isinstance(data, list):
return [_deserialize(value, namespace, memo) for value in data]
return data
_T = TypeVar("_T", bound="Serialize")
class Serialize:
"""Safe-ish serialization interface that doesn't rely on Pickle
Attributes:
__serialize_fields__ (List[str]): Fields (aka attributes) to serialize.
__serialize_namespace__ (list): List of classes that deserialization is allowed to instantiate.
Should include all field types that aren't builtin types.
"""
def memo_serialize(self, types_to_memoize: List) -> Any:
memo = SerializeMemoizer(types_to_memoize)
return self.serialize(memo), memo.serialize()
def serialize(self, memo = None) -> Dict[str, Any]:
if memo and memo.in_types(self):
return {'@': memo.memoized.get(self)}
fields = getattr(self, '__serialize_fields__')
res = {f: _serialize(getattr(self, f), memo) for f in fields}
res['__type__'] = type(self).__name__
if hasattr(self, '_serialize'):
self._serialize(res, memo)
return res
@classmethod
def deserialize(cls: Type[_T], data: Dict[str, Any], memo: Dict[int, Any]) -> _T:
namespace = getattr(cls, '__serialize_namespace__', [])
namespace = {c.__name__:c for c in namespace}
fields = getattr(cls, '__serialize_fields__')
if '@' in data:
return memo[data['@']]
inst = cls.__new__(cls)
for f in fields:
try:
setattr(inst, f, _deserialize(data[f], namespace, memo))
except KeyError as e:
raise KeyError("Cannot find key for class", cls, e)
if hasattr(inst, '_deserialize'):
inst._deserialize()
return inst
class SerializeMemoizer(Serialize):
"A version of serialize that memoizes objects to reduce space"
__serialize_fields__ = 'memoized',
def __init__(self, types_to_memoize: List) -> None:
self.types_to_memoize = tuple(types_to_memoize)
self.memoized = Enumerator()
def in_types(self, value: Serialize) -> bool:
return isinstance(value, self.types_to_memoize)
def serialize(self) -> Dict[int, Any]: # type: ignore[override]
return _serialize(self.memoized.reversed(), None)
@classmethod
def deserialize(cls, data: Dict[int, Any], namespace: Dict[str, Any], memo: Dict[Any, Any]) -> Dict[int, Any]: # type: ignore[override]
return _deserialize(data, namespace, memo)
try:
import regex
_has_regex = True
except ImportError:
_has_regex = False
if sys.version_info >= (3, 11):
import re._parser as sre_parse
import re._constants as sre_constants
else:
import sre_parse
import sre_constants
categ_pattern = re.compile(r'\\p{[A-Za-z_]+}')
def get_regexp_width(expr: str) -> Union[Tuple[int, int], List[int]]:
if _has_regex:
# Since `sre_parse` cannot deal with Unicode categories of the form `\p{Mn}`, we replace these with
# a simple letter, which makes no difference as we are only trying to get the possible lengths of the regex
# match here below.
regexp_final = re.sub(categ_pattern, 'A', expr)
else:
if re.search(categ_pattern, expr):
raise ImportError('`regex` module must be installed in order to use Unicode categories.', expr)
regexp_final = expr
try:
# Fixed in next version (past 0.960) of typeshed
return [int(x) for x in sre_parse.parse(regexp_final).getwidth()]
except sre_constants.error:
if not _has_regex:
raise ValueError(expr)
else:
# sre_parse does not support the new features in regex. To not completely fail in that case,
# we manually test for the most important info (whether the empty string is matched)
c = regex.compile(regexp_final)
# Python 3.11.7 introducded sre_parse.MAXWIDTH that is used instead of MAXREPEAT
# See lark-parser/lark#1376 and python/cpython#109859
MAXWIDTH = getattr(sre_parse, "MAXWIDTH", sre_constants.MAXREPEAT)
if c.match('') is None:
# MAXREPEAT is a none pickable subclass of int, therefore needs to be converted to enable caching
return 1, int(MAXWIDTH)
else:
return 0, int(MAXWIDTH)
@dataclass(frozen=True)
class TextSlice(Generic[AnyStr]):
"""A view of a string or bytes object, between the start and end indices.
Never creates a copy.
Lark accepts instances of TextSlice as input (instead of a string),
when the lexer is 'basic' or 'contextual'.
Args:
text (str or bytes): The text to slice.
start (int): The start index. Negative indices are supported.
end (int): The end index. Negative indices are supported.
Raises:
TypeError: If `text` is not a `str` or `bytes`.
AssertionError: If `start` or `end` are out of bounds.
Examples:
>>> TextSlice("Hello, World!", 7, -1)
TextSlice(text='Hello, World!', start=7, end=12)
>>> TextSlice("Hello, World!", 7, None).count("o")
1
"""
text: AnyStr
start: int
end: int
def __post_init__(self):
if not isinstance(self.text, (str, bytes)):
raise TypeError("text must be str or bytes")
if self.start < 0:
object.__setattr__(self, 'start', self.start + len(self.text))
assert self.start >=0
if self.end is None:
object.__setattr__(self, 'end', len(self.text))
elif self.end < 0:
object.__setattr__(self, 'end', self.end + len(self.text))
assert self.end <= len(self.text)
@classmethod
def cast_from(cls, text: 'TextOrSlice') -> 'TextSlice[AnyStr]':
if isinstance(text, TextSlice):
return text
return cls(text, 0, len(text))
def is_complete_text(self):
return self.start == 0 and self.end == len(self.text)
def __len__(self):
return self.end - self.start
def count(self, substr: AnyStr):
return self.text.count(substr, self.start, self.end)
def rindex(self, substr: AnyStr):
return self.text.rindex(substr, self.start, self.end)
TextOrSlice = Union[AnyStr, 'TextSlice[AnyStr]']
###}
_ID_START = 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Mn', 'Mc', 'Pc'
_ID_CONTINUE = _ID_START + ('Nd', 'Nl',)
def _test_unicode_category(s: str, categories: Sequence[str]) -> bool:
if len(s) != 1:
return all(_test_unicode_category(char, categories) for char in s)
return s == '_' or unicodedata.category(s) in categories
def is_id_continue(s: str) -> bool:
"""
Checks if all characters in `s` are alphanumeric characters (Unicode standard, so diacritics, indian vowels, non-latin
numbers, etc. all pass). Synonymous with a Python `ID_CONTINUE` identifier. See PEP 3131 for details.
"""
return _test_unicode_category(s, _ID_CONTINUE)
def is_id_start(s: str) -> bool:
"""
Checks if all characters in `s` are alphabetic characters (Unicode standard, so diacritics, indian vowels, non-latin
numbers, etc. all pass). Synonymous with a Python `ID_START` identifier. See PEP 3131 for details.
"""
return _test_unicode_category(s, _ID_START)
def dedup_list(l: Iterable[T]) -> List[T]:
"""Given a list (l) will removing duplicates from the list,
preserving the original order of the list. Assumes that
the list entries are hashable."""
return list(dict.fromkeys(l))
class Enumerator(Serialize):
def __init__(self) -> None:
self.enums: Dict[Any, int] = {}
def get(self, item) -> int:
if item not in self.enums:
self.enums[item] = len(self.enums)
return self.enums[item]
def __len__(self):
return len(self.enums)
def reversed(self) -> Dict[int, Any]:
r = {v: k for k, v in self.enums.items()}
assert len(r) == len(self.enums)
return r
def combine_alternatives(lists):
"""
Accepts a list of alternatives, and enumerates all their possible concatenations.
Examples:
>>> combine_alternatives([range(2), [4,5]])
[[0, 4], [0, 5], [1, 4], [1, 5]]
>>> combine_alternatives(["abc", "xy", '$'])
[['a', 'x', '$'], ['a', 'y', '$'], ['b', 'x', '$'], ['b', 'y', '$'], ['c', 'x', '$'], ['c', 'y', '$']]
>>> combine_alternatives([])
[[]]
"""
if not lists:
return [[]]
assert all(l for l in lists), lists
return list(product(*lists))
try:
import atomicwrites
_has_atomicwrites = True
except ImportError:
_has_atomicwrites = False
class FS:
exists = staticmethod(os.path.exists)
@staticmethod
def open(name, mode="r", **kwargs):
if _has_atomicwrites and "w" in mode:
return atomicwrites.atomic_write(name, mode=mode, overwrite=True, **kwargs)
else:
return open(name, mode, **kwargs)
class fzset(frozenset):
def __repr__(self):
return '{%s}' % ', '.join(map(repr, self))
def classify_bool(seq: Iterable, pred: Callable) -> Any:
false_elems = []
true_elems = [elem for elem in seq if pred(elem) or false_elems.append(elem)] # type: ignore[func-returns-value]
return true_elems, false_elems
def bfs(initial: Iterable, expand: Callable) -> Iterator:
open_q = deque(list(initial))
visited = set(open_q)
while open_q:
node = open_q.popleft()
yield node
for next_node in expand(node):
if next_node not in visited:
visited.add(next_node)
open_q.append(next_node)
def bfs_all_unique(initial, expand):
"bfs, but doesn't keep track of visited (aka seen), because there can be no repetitions"
open_q = deque(list(initial))
while open_q:
node = open_q.popleft()
yield node
open_q += expand(node)
def _serialize(value: Any, memo: Optional[SerializeMemoizer]) -> Any:
if isinstance(value, Serialize):
return value.serialize(memo)
elif isinstance(value, list):
return [_serialize(elem, memo) for elem in value]
elif isinstance(value, frozenset):
return list(value) # TODO reversible?
elif isinstance(value, dict):
return {key:_serialize(elem, memo) for key, elem in value.items()}
# assert value is None or isinstance(value, (int, float, str, tuple)), value
return value
def small_factors(n: int, max_factor: int) -> List[Tuple[int, int]]:
"""
Splits n up into smaller factors and summands <= max_factor.
Returns a list of [(a, b), ...]
so that the following code returns n:
n = 1
for a, b in values:
n = n * a + b
Currently, we also keep a + b <= max_factor, but that might change
"""
assert n >= 0
assert max_factor > 2
if n <= max_factor:
return [(n, 0)]
for a in range(max_factor, 1, -1):
r, b = divmod(n, a)
if a + b <= max_factor:
return small_factors(r, max_factor) + [(a, b)]
assert False, "Failed to factorize %s" % n
class OrderedSet(AbstractSet[T]):
"""A minimal OrderedSet implementation, using a dictionary.
(relies on the dictionary being ordered)
"""
def __init__(self, items: Iterable[T] =()):
self.d = dict.fromkeys(items)
def __contains__(self, item: Any) -> bool:
return item in self.d
def add(self, item: T):
self.d[item] = None
def __iter__(self) -> Iterator[T]:
return iter(self.d)
def remove(self, item: T):
del self.d[item]
def __bool__(self):
return bool(self.d)
def __len__(self) -> int:
return len(self.d)
def __repr__(self):
return f"{type(self).__name__}({', '.join(map(repr,self))})"