| #!/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!" |