# -*- coding: utf-8 -*-
# pytableaux, a multi-logic proof generator.
# Copyright (C) 2014-2023 Doug Owings.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
pytableaux.tools
----------------
"""
from __future__ import annotations
import functools
import keyword
import re
import sys
from abc import abstractmethod
from collections import defaultdict
from collections.abc import Mapping, MutableMapping, Sequence, Set
from enum import Enum
from operator import gt, lt
from types import FunctionType
from types import MappingProxyType as MapProxy
from typing import (TYPE_CHECKING, Any, Callable, Generic, Iterable, Self,
TypeVar)
__all__ = (
'absindex',
'closure',
'dictattr',
'dictns',
'dmerged',
'dund',
'EMPTY_MAP',
'EMPTY_SEQ',
'EMPTY_SET',
'for_defaults',
'getitem',
'group',
'isattrstr',
'isdund',
'isint',
'ItemMapEnum',
'KeySetAttr',
'lazy',
'MapCover',
'maxceil',
'membr',
'minfloor',
'NoSetAttr',
'PathedDict',
'sbool',
'select_fget',
'SeqCover',
'SequenceSet',
'SetView',
'slicerange',
'substitute',
'thru',
'TransMmap',
'undund',
'wraps')
EMPTY_MAP = MapProxy({})
EMPTY_SEQ = ()
EMPTY_SET = frozenset()
NOARG = object()
WRASS_SET = frozenset(functools.WRAPPER_ASSIGNMENTS)
_F = TypeVar('_F', bound=Callable)
_KT = TypeVar('_KT')
_T = TypeVar('_T')
_VT = TypeVar('_VT')
if TYPE_CHECKING:
from typing import overload
class TypeInstMap(Mapping[type[_VT], _VT]):
@overload
def __getitem__(self, key: type[_T]) -> _T: ...
@overload
def get(self, key: type[_T]) -> _T: ...
@overload
def get(self, key: Any, default: type[_T]) -> _T: ...
@overload
def copy(self:_T) -> _T: ...
@overload
def setdefault(self, key: type[_T], value: Any) -> _T: ...
@overload
def pop(self, key: type[_T]) -> _T: ...
class TypeTypeMap(Mapping[type[_VT], type[_VT]]):
@overload
def __getitem__(self, key: type[_T]) -> type[_T]: ...
@overload
def get(self, key: type[_T]) -> type[_T]: ...
@overload
def get(self, key: Any, default: type[_T]) -> type[_T]: ...
@overload
def copy(self:_T) -> _T: ...
@overload
def setdefault(self, key: type[_T], value: Any) -> type[_T]: ...
@overload
def pop(self, key: type[_T]) -> type[_T]: ...
[docs]
def dund(name: str) -> str:
"Convert name to dunder format."
return name if isdund(name) else f'__{name}__'
[docs]
def isdund(name: str) -> bool:
'Whether the string is a dunder name string.'
return (
len(name) > 4 and
name[:2] == name[-2:] == '__' and
name[2] != '_' and
name[-3] != '_')
[docs]
def undund(name: str) -> str:
"Remove dunder from the name."
if isdund(name):
return name[2:-2]
return name
[docs]
def getitem(obj, key, default = NOARG, /):
"Get by subscript similar to :func:`getattr`."
try:
return obj[key]
except (KeyError, IndexError):
if default is NOARG:
raise
return default
[docs]
def select_fget(obj):
"""Return :func:`getitem()` if the object has a callable `__getitem__`
method, else return :func:`getattr()`.
"""
if callable(getattr(obj, '__getitem__', None)):
return getitem
return getattr
[docs]
def thru(obj: _T) -> _T:
'Return the argument.'
return obj
[docs]
def group(*items):
"""Tuple builder.
Args:
*items: members.
Returns:
The tuple of arguments.
"""
return items
[docs]
def isint(obj) -> bool:
'Whether the argument is an :obj:`int` instance'
return isinstance(obj, int)
[docs]
def isattrstr(obj) -> bool:
"Whether the argument is a non-keyword identifier string"
return (
isinstance(obj, str) and
obj.isidentifier() and
not keyword.iskeyword(obj))
re_boolyes = re.compile(r'^(true|yes|1)$', re.I)
'Regex for string boolean `yes`.'
[docs]
def sbool(arg: str, /) -> bool:
"Cast string to boolean, leans toward ``False``."
return bool(re_boolyes.match(arg))
[docs]
def minfloor(floor: _T, it: Iterable[_T], default=None) -> _T:
"""Return the minimum value of ``it``, stopping when a value less than or
equal to ``floor`` is reached.
"""
return _limit_best(lt, floor, it, default, 'minfloor')
[docs]
def maxceil(ceil: _T, it: Iterable[_T], default=None) -> _T:
"""Return the maximum value of ``it``, stopping when a value greater than
or equal to ``ceil`` is reached.
"""
return _limit_best(gt, ceil, it, default, 'maxceil')
[docs]
def limit_best(better: Callable[[_T, _T], bool], limit: _T, it: Iterable[_T], default=None) -> _T:
"""Generic form for :func:`minfloor()` and :func:`maxceil()`.
Args:
better: A pairwise comparison function, e.g. :func:`operator.lt`
limit: The limit, e.g. floor or ceil.
it: The iterable.
default: A default value for an empty iterable.
"""
return _limit_best(better, limit, it, default, 'limit_best')
def _limit_best(better, limit, it, default, _name, /):
it = iter(it)
try:
best = next(it)
except StopIteration:
if default is not None:
return default
raise ValueError(
f"{_name}() arg is an empty sequence") from None
for val in it:
if val == limit or better(val, limit):
return val
if better(val, best):
best = val
return best
[docs]
def substitute(collection: _T, old, new) -> _T:
"""Return a new instance of the collection type with ``new`` substituted
for ``old``.
"""
return type(collection)(new if x == old else x for x in collection)
[docs]
def for_defaults(defaults: Mapping[_KT, _VT], override: Mapping, /) -> dict[_KT, _VT]:
"""Return a dict with keys from ``defaults``, with values from ``override``,
or ``defaults`` if missing.
"""
if not override:
return dict(defaults)
return {key: override.get(key, defval) for key, defval in defaults.items()}
[docs]
def absindex(seqlen: int, index: int, /, strict = True) -> int:
'Normalize to positive/absolute index.'
if index < 0:
index = seqlen + index
if strict and (index >= seqlen or index < 0):
raise IndexError(f'Index out of range: {index}')
return index
[docs]
def slicerange(seqlen: int, slice_: slice, values, /, strict = True) -> range:
'Get a range of indexes from a slice and new values, and perform checks.'
range_ = range(*slice_.indices(seqlen))
if len(range_) != len(values):
if strict:
raise ValueError(
f'Attempt to assign sequence of size {len(values)} '
f'to slice of size {len(range_)}')
if abs(slice_.step or 1) != 1:
raise ValueError(
f'Attempt to assign sequence of size {len(values)} '
f'to extended slice of size {len(range_)}')
return range_
def _prevmodule(thisname = __name__, /):
f = sys._getframe()
while (f := f.f_back) is not None:
val = f.f_globals.get('__name__', '__main__')
if val != thisname:
return val
[docs]
class wraps(dict):
'Replacement for :func:`functools.wraps`.'
__slots__ = ('only', 'wrapped')
def __init__(self, wrapped = None, /, *, only = WRASS_SET, exclude = EMPTY_SET, **kw):
'Initialize argument, initial input function that will be decorated.'
self.wrapped = wrapped
only = set(map(dund, only))
only.difference_update(map(dund, exclude))
only.intersection_update(WRASS_SET)
self.only = only
self.update(**kw)
if wrapped:
self.update(wrapped)
if (k := '__module__') in only and k not in self:
if (v := getattr(wrapped, '__objclass__', None)):
self.setdefault(k, v)
else:
self.setdefault(k, _prevmodule())
def __call__(self, wrapper):
'Decorate function. Receives the wrapper function and updates its attributes.'
self.update(wrapper)
if isinstance(wrapper, (classmethod, staticmethod)):
self.write(wrapper.__func__)
else:
self.write(wrapper)
return wrapper
[docs]
def write(self, wrapper):
"Write wrapped attributes to a wrapper."
for attr in filter(self.__contains__, self.only):
setattr(wrapper, attr, self[attr])
if callable(self.wrapped):
wrapper.__wrapped__ = self.wrapped
return wrapper
[docs]
def update(self, obj = None, /, **kw):
"""Read from an object/mapping and update relevant values. Any attributes
already present are ignored. Returns self.
"""
for o in obj, kw:
if o is not None:
for attr, val in self.read(o):
if attr not in self:
self[attr] = val
return self
[docs]
def read(self, obj):
"Read relevant attributes from object/mapping."
get = select_fget(obj)
for name in self.only:
if (value := get(obj, name, None)):
yield name, value
elif (value := get(obj, undund(name), None)):
yield name, value
[docs]
def setdefault(self, key, value):
"Override value if key is relevant and value is not empty."
if key in self.only:
if value:
self[key] = value
return self[key]
def __setitem__(self, key, value):
if key in self or key not in self.only:
raise KeyError(key)
super().__setitem__(key, value)
def __repr__(self):
return f'{type(self).__name__}({dict(self)})'
pass
[docs]
def closure(func: Callable[..., _T]) -> _T:
"""Closure decorator calls the argument and returns its return value.
If the return value is a function, updates its wrapper.
"""
ret = func()
if isinstance(ret, (classmethod, staticmethod, FunctionType)):
# if type(ret) is FunctionType:
wraps(func).write(ret)
return ret
pass
[docs]
@closure
def dmerged():
# TODO: memoize ...
def merger(a: Mapping, b: Mapping, /) -> dict:
'Basic dict merge copy, recursive for dict value.'
c = {}
for key, value in b.items():
if isinstance(value, Mapping):
if isinstance(a.get(key), Mapping):
c[key] = merger(a[key], value)
else:
c[key] = dcopy(value)
else:
c[key] = value
for key, value in a.items():
if key not in c:
if isinstance(value, Mapping):
c[key] = dcopy(value)
else:
c[key] = value
return c
def dcopy(a: Mapping, /) -> dict:
'Basic dict copy of a mapping, recursive for mapping values.'
return {
key: dcopy(value)
if isinstance(value, Mapping)
else value
for key, value in a.items()}
return merger
class membr:
__slots__ = ('callable', 'args', 'kwargs', '__name__', '__qualname__')
callable: Callable
args: tuple
kwargs: dict
@property
def name(self) -> str:
try:
return self.__name__
except AttributeError:
return type(self).__name__
def __init__(self, callable: Callable, *args, **kw):
self.callable = callable
self.args = args
self.kwargs = kw
def __call__(self):
return self.callable(self, *self.args, **self.kwargs)
@classmethod
def defer(cls, wrapped: Callable[[membr], _F]):
@wraps(wrapped)
def wrapper(*args, **kw):
return cls(wrapped, *args, **kw)
return wrapper
def __set_name__(self, owner, name):
self.__name__ = name
self.__qualname__ = f'{owner.__name__}.{name}'
setattr(owner, name, self())
def __repr__(self) -> str:
if not hasattr(self, '__qualname__') or not callable(self):
return object.__repr__(self)
return '<callable %s at %s>' % (self.__qualname__, hex(id(self)))
[docs]
class NoSetAttr:
'Lame thing that does a lame thing.'
enabled: bool
"Whether raising is enabled."
defaults = MapProxy(dict(
efmt = (
"Attribute '{0}' of '{1.__class__.__name__}' "
"objects is readonly").format,
# Control attribute name to check on the object,
# e.g. '_readonly', in addition to this object's
# `enabled` setting.
attr = '_readonly',
# If `True`: Check `attr` on the object's class;
# If set to a `type`, check the `attr` on that class;
# If Falsy, only check for this object's `enabled` setting.
cls = None))
__slots__ = ('cache', 'opts', 'enabled')
opts: dict[str, Any]
cache: dict[Callable, dict[tuple, Any]]
def __init__(self, /, *, enabled = True, **opts):
self.enabled = bool(enabled)
self.opts = for_defaults(self.defaults, opts)
self.cache = defaultdict(dict)
def __call__(self, base: type, **opts):
return self._make(base.__setattr__, **(self.opts | opts))
def _make(self, wrapped, /, efmt, attr, cls):
if cls is True:
check = self._clschecker(attr)
elif cls:
check = self._fixedchecker(attr, cls)
else:
check = self._selfchecker(attr)
@wraps(wrapped)
def wrapper(obj, name, value, /):
if self.enabled and check(obj):
raise AttributeError(efmt(name, obj))
wrapped(obj, name, value)
return wrapper
def cached(wrapped: _F):
@wraps(wrapped)
def wrapper(self: NoSetAttr, *args):
cache = self.cache[wrapped]
try:
return cache[args]
except KeyError:
return cache.setdefault(args, wrapped(self, *args))
return wrapper
@cached
def _fixedchecker(self, attr, obj):
return lambda _: getattr(obj, attr, False)
@cached
def _callchecker(self, attr, fget):
return lambda obj: getattr(fget(obj), attr, False)
@cached
def _clschecker(self, attr):
return self._callchecker(type, attr)
@cached
def _selfchecker(self, attr):
return lambda obj: getattr(obj, attr, False)
del(cached)
[docs]
class MapCover(Mapping[_KT, _VT]):
'Mapping reference.'
__slots__ = ('__getitem__', '_cov_mapping')
_cov_mapping: Mapping
def __init__(self, mapping: Mapping, /):
if type(mapping) is not MapProxy:
mapping = MapProxy(mapping)
self._cov_mapping = mapping
self.__getitem__ = mapping.__getitem__
def __reversed__(self):
return reversed(self._cov_mapping)
def __len__(self):
return len(self._cov_mapping)
def __iter__(self):
return iter(self._cov_mapping)
def __repr__(self):
return repr(self._asdict())
def __or__(self, other):
return dict(self) | other
def __ror__(self, other):
return other | dict(self)
def _asdict(self):
'Compatibility for JSON serialization.'
return dict(self)
[docs]
class SeqCover(Sequence):
'Sequence cover.'
class CoverAttr(frozenset, Enum):
REQUIRED = {
'__len__',
'__getitem__',
'__contains__',
'__iter__',
'count',
'index'}
OPTIONAL = {'__reversed__'}
ALL = REQUIRED | OPTIONAL
__slots__ = CoverAttr.ALL
def __new__(cls, seq: Sequence, /):
self = object.__new__(cls)
for name in cls.CoverAttr.ALL:
value = getattr(seq, name, NOARG)
if value is NOARG:
if name in cls.CoverAttr.REQUIRED:
raise AttributeError(name)
continue
setattr(self, name, value)
return self
def __repr__(self):
return f'{type(self).__name__}({list(self)})'
[docs]
class KeySetAttr:
"Mixin class for read-write attribute-key gate."
__slots__ = EMPTY_SET
def __setitem__(self, key, value, /):
super().__setitem__(key, value)
if isattrstr(key) and self._keyattr_ok(key):
super().__setattr__(key, value)
def __setattr__(self, name, value):
super().__setattr__(name, value)
if self._keyattr_ok(name):
super().__setitem__(name, value)
def __delitem__(self, key, /):
super().__delitem__(key)
if isattrstr(key) and self._keyattr_ok(key):
super().__delattr__(key)
def __delattr__(self, name):
super().__delattr__(name)
if self._keyattr_ok(name) and name in self:
super().__delitem__(name)
@classmethod
def _keyattr_ok(cls, name: str) -> bool:
'Return whether it is ok to set the attribute name.'
return not hasattr(cls, name)
[docs]
class dictattr(KeySetAttr, dict[_KT, _VT]):
"Dict attr base class."
__slots__ = EMPTY_SET
def __init__(self, *args, **kw):
self.update(*args, **kw)
pop = MutableMapping.pop
popitem = MutableMapping.popitem
setdefault = MutableMapping.setdefault
update = MutableMapping.update
[docs]
class dictns(dictattr[_KT, _VT]):
"Dict attr namespace with __dict__ slot and liberal key approval."
@classmethod
def _keyattr_ok(cls, name):
return not name.startswith('_') and not hasattr(cls, name)
[docs]
class TransMmap(MutableMapping[_KT, _VT], MapCover[_KT, _VT]):
'Mutable mapping with key/value translators'
__slots__ = (
'__delitem__',
'__getitem__',
'__setitem__')
kget = kset = vget = vset = staticmethod(thru)
def __init__(self, *args, **kw):
self._cov_mapping = MapProxy(mapping := dict(*args, **kw))
self.__getitem__ = lambda key: self.vget(mapping.__getitem__(self.kget(key)))
self.__setitem__ = lambda key, value: mapping.__setitem__(self.kset(key), self.vset(value))
self.__delitem__ = lambda key: mapping.__delitem__(self.kset(key))
[docs]
class PathedDict(dict[str, _VT]):
"A nested dict that supports key path expressions like 'a:b:c'."
separator: str = ':'
default = dict
__slots__ = EMPTY_SET
def __init__(self, *args, **kw):
self.update(*args, **kw)
def __getitem__(self, key):
try:
return super().__getitem__(key)
except KeyError:
if not isinstance(key, str) or self.separator not in key:
raise
obj = self
for key in key.split(self.separator):
obj = obj[key]
return obj
def __setitem__(self, key, value):
if not isinstance(key, str) or self.separator not in key:
return super().__setitem__(key, value)
path = key.split(self.separator)
last = path.pop()
obj = self
for key in path:
try:
obj = obj[key]
except KeyError:
obj = obj.setdefault(key, self.default())
obj[last] = value
def __delitem__(self, key):
try:
super().__delitem__(key)
except KeyError:
if not isinstance(key, str) or self.separator not in key:
raise
path = key.split(self.separator)
last = path.pop()
obj = self
for key in path:
obj = obj[key]
del obj[last]
get = MutableMapping.get
pop = MutableMapping.pop
popitem = MutableMapping.popitem
setdefault = MutableMapping.setdefault
update = MutableMapping.update
class ForObjectBuilder(Generic[_T]):
__slots__ = EMPTY_SET
@classmethod
def for_object(cls, obj: _T, /) -> Self:
return cls(
*cls.get_obj_args(obj),
**dict(cls.get_obj_kwargs(obj)))
@classmethod
@abstractmethod
def get_obj_args(cls, obj: _T, /) -> Iterable[Any]:
yield from EMPTY_SET
@classmethod
@abstractmethod
def get_obj_kwargs(cls, obj: _T, /) -> Iterable[tuple[str, Any]]:
yield from EMPTY_SET
[docs]
class ItemMapEnum(Enum):
"""Fixed mapping enum based on item tuples.
If a member value is defined as a mapping, the member's ``_value_`` attribute
is converted to a tuple of item tuples during ``__init__()``.
Implementations should always call ``super().__init__()`` if it is overridden.
"""
__slots__ = (
'__iter__',
'__getitem__',
'__len__',
'__reversed__',
'name',
'value',
'_name_',
'_value_')
def __init__(self, *args):
if len(args) == 1 and isinstance(args[0], Mapping):
self._value_ = args = tuple(args[0].items())
m = dict(args)
self.__len__ = m.__len__
self.__iter__ = m.__iter__
self.__getitem__ = m.__getitem__
self.__reversed__ = m.__reversed__
self.name = self._name_
self.value = self._value_
keys = Mapping.keys
items = Mapping.items
values = Mapping.values
get = Mapping.get
def __or__(self, other):
return dict(self) | other
def __ror__(self, other):
return other | dict(self)
def _asdict(self):
'Compatibility for JSON serialization.'
return dict(self)
from .abcs import Copyable
pass
[docs]
class SetView(Set, Copyable, immutcopy=True):
'Set cover.'
__slots__ = ('__contains__', '__iter__', '__len__')
def __new__(cls, set_, /,):
if not isinstance(set_, Set):
raise TypeError(type(set_))
self = object.__new__(cls)
self.__len__ = set_.__len__
self.__iter__ = set_.__iter__
self.__contains__ = set_.__contains__
return self
def __repr__(self):
prefix = type(self).__name__
if len(self):
return f'{prefix}{set(self)}'
return f'{prefix}''{}'
from .hybrids import EMPTY_QSET as EMPTY_QSET
from .hybrids import SequenceSet as SequenceSet
from .hybrids import qset as qset
from .hybrids import qsetf as qsetf
pass
from . import lazy as lazy