| #!/usr/bin/env python3 | |
| import enum | |
| import io | |
| import pprint | |
| import sys | |
| from types import MappingProxyType | |
| def frozendict(*args, **kwargs): | |
| """Version of a dictionary which can't be changed.""" | |
| return MappingProxyType(dict(*args, **kwargs)) | |
| class MostlyReadOnly: | |
| """Object which is **mostly** read only. Can set if not already set. | |
| >>> class MyRO(MostlyReadOnly): | |
| ... __slots__ = ["_str", "_list", "_set", "_dict"] | |
| >>> a = MyRO() | |
| >>> a | |
| MyRO(str=None, list=None, set=None, dict=None) | |
| >>> a._str = 't' | |
| >>> a.str | |
| 't' | |
| >>> a._list = [1,2,3] | |
| >>> a.list | |
| (1, 2, 3) | |
| >>> a._set = {1, 2, 3} | |
| >>> a.set | |
| frozenset({1, 2, 3}) | |
| >>> a._dict = {'a': 1, 'b': 2, 'c': 3} | |
| >>> b = a.dict | |
| >>> b['d'] = 4 | |
| Traceback (most recent call last): | |
| ... | |
| b['d'] = 4 | |
| TypeError: 'mappingproxy' object does not support item assignment | |
| >>> sorted(b.items()) | |
| [('a', 1), ('b', 2), ('c', 3)] | |
| >>> a._dict['d'] = 4 | |
| >>> sorted(a._dict.items()) | |
| [('a', 1), ('b', 2), ('c', 3), ('d', 4)] | |
| >>> sorted(b.items()) | |
| [('a', 1), ('b', 2), ('c', 3)] | |
| >>> a | |
| MyRO(str='t', list=[1, 2, 3], set={1, 2, 3}, dict={'a': 1, 'b': 2, 'c': 3, 'd': 4}) | |
| >>> a.missing | |
| Traceback (most recent call last): | |
| ... | |
| AttributeError: 'MyRO' object has no attribute 'missing' | |
| >>> a.missing = 1 | |
| Traceback (most recent call last): | |
| ... | |
| AttributeError: missing not found on <class 'lib.collections_extra.MyRO'> | |
| >>> a.missing | |
| Traceback (most recent call last): | |
| ... | |
| AttributeError: 'MyRO' object has no attribute 'missing' | |
| """ | |
| def __setattr__(self, key, new_value=None): | |
| if key.startswith("_"): | |
| current_value = getattr(self, key[1:]) | |
| if new_value == current_value: | |
| return | |
| elif current_value is not None: | |
| raise AttributeError( | |
| "{} is already set to {}, can't be changed".format( | |
| key, current_value | |
| ) | |
| ) | |
| return super().__setattr__(key, new_value) | |
| if "_" + key not in self.__class__.__slots__: | |
| raise AttributeError( | |
| "{} not found on {}".format(key, self.__class__) | |
| ) | |
| self.__setattr__("_" + key, new_value) | |
| def __getattr__(self, key): | |
| if "_" + key not in self.__class__.__slots__: | |
| super().__getattribute__(key) | |
| value = getattr(self, "_" + key, None) | |
| if isinstance(value, | |
| (tuple, int, bytes, str, type(None), MostlyReadOnly)): | |
| return value | |
| elif isinstance(value, list): | |
| return tuple(value) | |
| elif isinstance(value, set): | |
| return frozenset(value) | |
| elif isinstance(value, dict): | |
| return frozendict(value) | |
| elif isinstance(value, enum.Enum): | |
| return value | |
| else: | |
| raise AttributeError( | |
| "Unable to return {}, don't now how to make type {} (from {!r}) read only." | |
| .format(key, type(value), value) | |
| ) | |
| def __repr__(self): | |
| attribs = [] | |
| for attr in self.__slots__: | |
| value = getattr(self, attr, None) | |
| if isinstance(value, MostlyReadOnly): | |
| rvalue = "{}()".format(value.__class__.__name__) | |
| elif isinstance(value, (dict, set)): | |
| s = io.StringIO() | |
| pprint.pprint(value, stream=s, width=sys.maxsize) | |
| rvalue = s.getvalue().strip() | |
| else: | |
| rvalue = repr(value) | |
| if attr.startswith("_"): | |
| attr = attr[1:] | |
| attribs.append("{}={!s}".format(attr, rvalue)) | |
| return "{}({})".format(self.__class__.__name__, ", ".join(attribs)) | |
| class OrderedEnum(enum.Enum): | |
| def __ge__(self, other): | |
| if self.__class__ is other.__class__: | |
| return self.name >= other.name | |
| if hasattr(other.__class__, "name"): | |
| return self.name >= other.name | |
| return NotImplemented | |
| def __gt__(self, other): | |
| if self.__class__ is other.__class__: | |
| return self.name > other.name | |
| if hasattr(other.__class__, "name"): | |
| return self.name > other.name | |
| return NotImplemented | |
| def __le__(self, other): | |
| if self.__class__ is other.__class__: | |
| return self.name <= other.name | |
| if hasattr(other.__class__, "name"): | |
| return self.name <= other.name | |
| return NotImplemented | |
| def __lt__(self, other): | |
| if self.__class__ is other.__class__: | |
| return self.name < other.name | |
| if hasattr(other.__class__, "name"): | |
| return self.name < other.name | |
| return NotImplemented | |
| class CompassDir(OrderedEnum): | |
| """ | |
| >>> print(repr(CompassDir.NN)) | |
| <CompassDir.NN: 'North'> | |
| >>> print(str(CompassDir.NN)) | |
| ( 0, -1, NN) | |
| >>> for d in CompassDir: | |
| ... print(OrderedEnum.__str__(d)) | |
| CompassDir.NW | |
| CompassDir.NN | |
| CompassDir.NE | |
| CompassDir.EE | |
| CompassDir.SE | |
| CompassDir.SS | |
| CompassDir.SW | |
| CompassDir.WW | |
| >>> for y in (-1, 0, 1): | |
| ... for x in (-1, 0, 1): | |
| ... print( | |
| ... "(%2i %2i)" % (x, y), | |
| ... str(CompassDir.from_coords(x, y)), | |
| ... str(CompassDir.from_coords((x, y))), | |
| ... ) | |
| (-1 -1) (-1, -1, NW) (-1, -1, NW) | |
| ( 0 -1) ( 0, -1, NN) ( 0, -1, NN) | |
| ( 1 -1) ( 1, -1, NE) ( 1, -1, NE) | |
| (-1 0) (-1, 0, WW) (-1, 0, WW) | |
| ( 0 0) None None | |
| ( 1 0) ( 1, 0, EE) ( 1, 0, EE) | |
| (-1 1) (-1, 1, SW) (-1, 1, SW) | |
| ( 0 1) ( 0, 1, SS) ( 0, 1, SS) | |
| ( 1 1) ( 1, 1, SE) ( 1, 1, SE) | |
| >>> print(str(CompassDir.NN.flip())) | |
| ( 0, 1, SS) | |
| >>> print(str(CompassDir.SE.flip())) | |
| (-1, -1, NW) | |
| """ | |
| NW = 'North West' | |
| NN = 'North' | |
| NE = 'North East' | |
| EE = 'East' | |
| SE = 'South East' | |
| SS = 'South' | |
| SW = 'South West' | |
| WW = 'West' | |
| # Single letter aliases | |
| N = NN | |
| E = EE | |
| S = SS | |
| W = WW | |
| @property | |
| def distance(self): | |
| return sum(a * a for a in self.coords) | |
| def __init__(self, *args, **kw): | |
| self.__cords = None | |
| pass | |
| @property | |
| def coords(self): | |
| if not self.__cords: | |
| self.__cords = self.convert_to_coords[self] | |
| return self.__cords | |
| @property | |
| def x(self): | |
| return self.coords[0] | |
| @property | |
| def y(self): | |
| return self.coords[-1] | |
| def __iter__(self): | |
| return iter(self.coords) | |
| def __getitem__(self, k): | |
| return self.coords[k] | |
| @classmethod | |
| def from_coords(cls, x, y=None): | |
| if y is None: | |
| return cls.from_coords(*x) | |
| return cls.convert_from_coords[(x, y)] | |
| def flip(self): | |
| return self.from_coords(self.flip_coords[self.coords]) | |
| def __add__(self, o): | |
| return o.__class__(o[0] + self.x, o[1] + self.y) | |
| def __radd__(self, o): | |
| return o.__class__(o[0] + self.x, o[1] + self.y) | |
| def __str__(self): | |
| return "(%2i, %2i, %s)" % (self.x, self.y, self.name) | |
| CompassDir.convert_to_coords = {} | |
| CompassDir.convert_from_coords = {} | |
| CompassDir.flip_coords = {} | |
| CompassDir.straight = [] | |
| CompassDir.angled = [] | |
| for d in list(CompassDir) + [None]: | |
| if d is None: | |
| x, y = 0, 0 | |
| else: | |
| if d.name[0] == 'N': | |
| y = -1 | |
| elif d.name[0] == 'S': | |
| y = 1 | |
| else: | |
| assert d.name[0] in ('E', 'W') | |
| y = 0 | |
| if d.name[1] == 'E': | |
| x = 1 | |
| elif d.name[1] == 'W': | |
| x = -1 | |
| else: | |
| assert d.name[1] in ('N', 'S') | |
| x = 0 | |
| CompassDir.convert_to_coords[d] = (x, y) | |
| CompassDir.convert_from_coords[(x, y)] = d | |
| CompassDir.flip_coords[(x, y)] = (-1 * x, -1 * y) | |
| length = x * x + y * y | |
| if length == 1: | |
| CompassDir.straight.append(d) | |
| elif length == 2: | |
| CompassDir.angled.append(d) | |
| if __name__ == "__main__": | |
| import doctest | |
| failure_count, test_count = doctest.testmod() | |
| assert test_count > 0 | |
| assert failure_count == 0, "Doctests failed!" |