diff options
author | Jesusaves <cpntb1@ymail.com> | 2020-12-09 13:32:01 -0300 |
---|---|---|
committer | Jesusaves <cpntb1@ymail.com> | 2020-12-09 13:32:01 -0300 |
commit | 63afe4145f410a844c647d4e3f1059f568175c1e (patch) | |
tree | 15da6a890c78d73370f44f9fd5d59badfbbe60e4 /game/python-extra/utils | |
download | client-init.tar.gz client-init.tar.bz2 client-init.tar.xz client-init.zip |
Initial commit, forked from Spheresinit
Diffstat (limited to 'game/python-extra/utils')
-rw-r--r-- | game/python-extra/utils/__init__.py | 0 | ||||
-rw-r--r-- | game/python-extra/utils/bools.py | 7 | ||||
-rw-r--r-- | game/python-extra/utils/dates.py | 225 | ||||
-rw-r--r-- | game/python-extra/utils/dicts/__init__.py | 7 | ||||
-rw-r--r-- | game/python-extra/utils/dicts/chained_dict.py | 71 | ||||
-rw-r--r-- | game/python-extra/utils/dicts/helpers.py | 99 | ||||
-rw-r--r-- | game/python-extra/utils/dicts/limited_dict.py | 40 | ||||
-rw-r--r-- | game/python-extra/utils/enum.py | 132 | ||||
-rw-r--r-- | game/python-extra/utils/lists.py | 42 | ||||
-rw-r--r-- | game/python-extra/utils/math.py | 16 | ||||
-rw-r--r-- | game/python-extra/utils/objects.py | 59 |
11 files changed, 698 insertions, 0 deletions
diff --git a/game/python-extra/utils/__init__.py b/game/python-extra/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/game/python-extra/utils/__init__.py diff --git a/game/python-extra/utils/bools.py b/game/python-extra/utils/bools.py new file mode 100644 index 0000000..f2bcab5 --- /dev/null +++ b/game/python-extra/utils/bools.py @@ -0,0 +1,7 @@ +try: + reduce +except NameError: + from functools import reduce + +def xor(*things): + return reduce(lambda x, y: bool(x) ^ bool(y), things) diff --git a/game/python-extra/utils/dates.py b/game/python-extra/utils/dates.py new file mode 100644 index 0000000..f5643cf --- /dev/null +++ b/game/python-extra/utils/dates.py @@ -0,0 +1,225 @@ +"""Useful things to do with dates""" +import datetime + + +def date_from_string(string, format_string=None): + """Runs through a few common string formats for datetimes, + and attempts to coerce them into a datetime. Alternatively, + format_string can provide either a single string to attempt + or an iterable of strings to attempt.""" + + if isinstance(format_string, str): + return datetime.datetime.strptime(string, format_string).date() + + elif format_string is None: + format_string = [ + "%Y-%m-%d", + "%m-%d-%Y", + "%m/%d/%Y", + "%d/%m/%Y", + ] + + for format in format_string: + try: + return datetime.datetime.strptime(string, format).date() + except ValueError: + continue + + raise ValueError("Could not produce date from string: {}".format(string)) + + +def to_datetime(plain_date, hours=0, minutes=0, seconds=0, ms=0): + """given a datetime.date, gives back a datetime.datetime""" + # don't mess with datetimes + if isinstance(plain_date, datetime.datetime): + return plain_date + return datetime.datetime( + plain_date.year, + plain_date.month, + plain_date.day, + hours, + minutes, + seconds, + ms, + ) + + +class TimePeriod(object): + + def __init__(self, earliest, latest): + if not isinstance(earliest, datetime.date) and earliest is not None: + raise TypeError("Earliest must be a date or None") + if not isinstance(latest, datetime.date) and latest is not None: + raise TypeError("Latest must be a date or None") + + # convert dates to datetimes, for to have better resolution + if earliest is not None: + earliest = to_datetime(earliest) + if latest is not None: + latest = to_datetime(latest, 23, 59, 59) + + if earliest is not None and latest is not None and earliest >= latest: + raise ValueError("Earliest must be earlier than latest") + + self._earliest = earliest + self._latest = latest + + def __contains__(self, key): + if isinstance(key, datetime.date): + key = to_datetime(key) + + if self._latest is None: + upper_bounded = True + else: + upper_bounded = key <= self._latest + + if self._earliest is None: + lower_bounded = True + else: + lower_bounded = self._earliest <= key + + return upper_bounded and lower_bounded + + elif isinstance(key, TimePeriod): + if self._latest is None: + upper_bounded = True + elif key._latest is None: + upper_bounded = False + else: + upper_bounded = self._latest >= key._latest + + if self._earliest is None: + lower_bounded = True + elif key._earliest is None: + lower_bounded = False + else: + lower_bounded = self._earliest <= key._earliest + + return upper_bounded and lower_bounded + + def contains(self, other): + return other in self + + def overlaps(self, other): + """does another datetime overlap with this one? this is a symmetric + property. + + TP1 |------------| + -------------------------------------------------> time + TP2 |--------------| + + TP1.overlaps(TP2) == TP2.overlaps(TP1) == True + + args: + other - a TimePeriod + """ + + return self._latest in other or self._earliest in other + + def __eq__(self, other): + return (self._earliest == other._earliest) and (self._latest == other._latest) + + def __hash__(self): + return hash((self._earliest, self._latest)) + + def __repr__(self): + return "<{}: {}-{}>".format( + self.__class__.__name__, + self._earliest, + self._latest, + ) + + @classmethod + def get_containing_period(cls, *periods): + """Given a bunch of TimePeriods, return a TimePeriod that most closely + contains them.""" + + if any(not isinstance(period, TimePeriod) for period in periods): + raise TypeError("periods must all be TimePeriods: {}".format(periods)) + + latest = datetime.datetime.min + earliest = datetime.datetime.max + + for period in periods: + # the best we can do to conain None is None! + if period._latest is None: + latest = None + elif latest is not None and period._latest > latest: + latest = period._latest + + if period._earliest is None: + earliest = None + elif earliest is not None and period._earliest < earliest: + earliest = period._earliest + + return TimePeriod(earliest, latest) + + +class DiscontinuousTimePeriod(object): + """A bunch of TimePeriods""" + + def __init__(self, *periods): + if any(not isinstance(period, TimePeriod) for period in periods): + raise TypeError("periods must all be TimePeriods: {}".format(periods)) + + periods = set(periods) + + no_overlaps_periods = [] + for period in periods: + for other_period in periods: + if id(other_period) == id(period): + continue + + # periods that overlap should be combined + if period.overlaps(other_period): + period = TimePeriod.get_containing_period(period, other_period) + + no_overlaps_periods.append(period) + + no_equals_periods = [] + reference = set(no_overlaps_periods) + for period in no_overlaps_periods: + # clean out duplicated periods + if any(other_period == period and other_period is not period for other_period in reference): + reference.remove(period) + else: + no_equals_periods.append(period) + + no_contains_periods = [] + for period in no_equals_periods: + # don't need to keep periods that are wholly contained + skip = False + for other_period in no_equals_periods: + if id(other_period) == id(period): + continue + + if period in other_period: + skip = True + + if not skip: + no_contains_periods.append(period) + self._periods = no_contains_periods + + def __contains__(self, other): + if isinstance(other, (datetime.date, TimePeriod)): + for period in self._periods: + if other in period: + return True + + +def days_ago(days, give_datetime=True): + delta = datetime.timedelta(days=days) + dt = datetime.datetime.now() - delta + if give_datetime: + return dt + else: + return dt.date() + + +def days_ahead(days, give_datetime=True): + delta = datetime.timedelta(days=days) + dt = datetime.datetime.now() + delta + if give_datetime: + return dt + else: + return dt.date() diff --git a/game/python-extra/utils/dicts/__init__.py b/game/python-extra/utils/dicts/__init__.py new file mode 100644 index 0000000..0ab7c69 --- /dev/null +++ b/game/python-extra/utils/dicts/__init__.py @@ -0,0 +1,7 @@ +"""Helper functinos for dealing with dicts. + +Things you always wished you could do more succinctly! +""" +from .limited_dict import LimitedDict +from .chained_dict import ChainedDict +from .helpers import * diff --git a/game/python-extra/utils/dicts/chained_dict.py b/game/python-extra/utils/dicts/chained_dict.py new file mode 100644 index 0000000..f1fe36f --- /dev/null +++ b/game/python-extra/utils/dicts/chained_dict.py @@ -0,0 +1,71 @@ +from collections import MutableMapping +from itertools import chain + + +class ChainedDict(MutableMapping): + + def __init__(self, parent=None, **kwargs): + self.__parent = parent + self.__deleted_keys = set() + self.__data = kwargs + + def __contains__(self, key): + if self.__parent is not None: + return ( + (key in self.__data or key in self.__parent) + and key not in self.__deleted_keys + ) + return key in self.__data + + def __getitem__(self, key): + try: + return self.__data[key] + except KeyError: + if self.__parent is not None and key not in self.__deleted_keys: + return self.__parent[key] + else: + raise + + def __setitem__(self, key, val): + self.__data[key] = val + self.__deleted_keys.discard(key) + + def __delitem__(self, key): + if key in self: + self.__deleted_keys.add(key) + try: + del self.__data[key] + except KeyError: + pass + else: + raise KeyError(key) + + def __repr__(self): + return "{}({})".format(self.__class__.__name__, dict(self.items())) + + def __iter__(self): + return self.keys() + + def __len__(self): + return len(list(self.keys())) + + def iterkeys(self): + yielded = set(self.__deleted_keys) + if self.__parent is None: + iterable = self.__data.keys() + else: + iterable = chain(self.__parent.keys(), self.__data.keys()) + + for key in iterable: + if key in yielded: + continue + yield key + yielded.add(key) + + keys = iterkeys + + def iteritems(self): + for key in self.iterkeys(): + yield key, self[key] + + items = iteritems diff --git a/game/python-extra/utils/dicts/helpers.py b/game/python-extra/utils/dicts/helpers.py new file mode 100644 index 0000000..8b1f594 --- /dev/null +++ b/game/python-extra/utils/dicts/helpers.py @@ -0,0 +1,99 @@ +from collections import namedtuple + + +def from_keyed_iterable(iterable, key, filter_func=None): + """Construct a dictionary out of an iterable, using an attribute name as + the key. Optionally provide a filter function, to determine what should be + kept in the dictionary.""" + + generated = {} + + for element in iterable: + try: + k = getattr(element, key) + except AttributeError: + raise RuntimeError("{} does not have the keyed attribute: {}".format( + element, key + )) + + if filter_func is None or filter_func(element): + if k in generated: + generated[k] += [element] + else: + generated[k] = [element] + + return generated + + +def subtract_by_key(dict_a, dict_b): + """given two dicts, a and b, this function returns c = a - b, where + a - b is defined as the key difference between a and b. + + e.g., + {1:None, 2:3, 3:"yellow", 4:True} - {2:4, 1:"green"} = + {3:"yellow", 4:True} + + """ + difference_dict = {} + for key in dict_a: + if key not in dict_b: + difference_dict[key] = dict_a[key] + + return difference_dict + + +def subtract(dict_a, dict_b, strict=False): + """a stricter form of subtract_by_key(), this version will only remove an + entry from dict_a if the key is in dict_b *and* the value at that key + matches""" + if not strict: + return subtract_by_key(dict_a, dict_b) + + difference_dict = {} + for key in dict_a: + if key not in dict_b or dict_b[key] != dict_a[key]: + difference_dict[key] = dict_a[key] + + return difference_dict + + +WinnowedResult = namedtuple("WinnowedResult", ['has', 'has_not']) +def winnow_by_keys(dct, keys=None, filter_func=None): + """separates a dict into has-keys and not-has-keys pairs, using either + a list of keys or a filtering function.""" + has = {} + has_not = {} + + for key in dct: + key_passes_check = False + if keys is not None: + key_passes_check = key in keys + elif filter_func is not None: + key_passes_check = filter_func(key) + + if key_passes_check: + has[key] = dct[key] + else: + has_not[key] = dct[key] + + return WinnowedResult(has, has_not) + + +def intersection(dict_a, dict_b, strict=True): + intersection_dict = {} + + for key in dict_a: + if key in dict_b: + if not strict or dict_a[key] == dict_b[key]: + intersection_dict[key] = dict_a[key] + + return intersection_dict + + +def setdefaults(dct, defaults): + """Given a target dct and a dict of {key:default value} pairs, + calls setdefault for all of those pairs.""" + for key in defaults: + dct.setdefault(key, defaults[key]) + + return dct diff --git a/game/python-extra/utils/dicts/limited_dict.py b/game/python-extra/utils/dicts/limited_dict.py new file mode 100644 index 0000000..c690118 --- /dev/null +++ b/game/python-extra/utils/dicts/limited_dict.py @@ -0,0 +1,40 @@ +from collections import MutableMapping + + +class LimitedDict(MutableMapping): + def __init__(self, args=None, **kwargs): + keys = kwargs.pop('keys', []) + self.__keys = keys + + self.__data = {} + + if args: + kwargs.update((key, val) for key, val in args) + + for key, val in kwargs.items(): + self[key] = val + + def __setitem__(self, key, val): + if key not in self.__keys: + raise KeyError("Illegal key: {}".format(key)) + + self.__data[key] = val + + def __getitem__(self, key): + return self.__data[key] + + def __iter__(self): + return self.__data.__iter__() + + def __delitem__(self, key): + del self.__data[key] + + def __len__(self): + return len(self.__data) + + def __repr__(self): + return "{}({}, {})".format(self.__class__.__name__, self.defined_keys, self.__data) + + @property + def defined_keys(self): + return self.__keys diff --git a/game/python-extra/utils/enum.py b/game/python-extra/utils/enum.py new file mode 100644 index 0000000..8b145a4 --- /dev/null +++ b/game/python-extra/utils/enum.py @@ -0,0 +1,132 @@ +"""Who hasn't needed a good, old-fashioned enum now and then?""" + + +class _enum(object): + + def __call__(self, enum_name, *args, **kwargs): + if args and kwargs: + raise TypeError("enums can only be made from args XOR kwargs") + + enum_items = {} + + counter = 0 + for name, val in kwargs.items(): + if val is None: + val = counter + counter += 1 + elif isinstance(val, int): + counter = val + 1 + + enum_items[name] = val + + for val, name in enumerate(args, start=counter): + enum_items[name] = val + + return type(enum_name, (Enum,), enum_items) + + def from_iterable(self, iterable): + return self(*iterable) + + def from_dict(self, dct): + return self(**dct) + + def __iter__(self): + for k, v in self.__enum_items.items(): + yield k, v + + def __repr__(self): + return "<{}: {}>".format(self.__class__.__name__, self.__enum_items.values()) +enum = _enum() + + +class EnumItem(object): + + def __init__(self, parent, name, value): + self.__parent = parent + self.__name = name + self.__value = value + + def __repr__(self): + return "<{}: {} [{}]>".format(self.__class__.__name__, self.name, self.value) + + def __eq__(self, other): + if isinstance(other, self.__class__): + if self.parent.is_strict and self.parent != other.parent: + raise ValueError("can't compare EnumItems from different enums") + return self.value == other.value + + return self.value == other + + @property + def value(self): + return self.__value + + @property + def name(self): + return self.__name + + @property + def parent(self): + return self.__parent + + +class _EnumMeta(type): + def __new__(cls, name, bases, attr_dict): + + options = attr_dict.pop('Options', object) + + attr_dict['__strict__'] = getattr(options, "strict_compare", True) + + new_enum = super(_EnumMeta, cls).__new__(cls, name, bases, {}) + + enum_items = {} + + for attr_name, attr_value in attr_dict.items(): + if attr_name.startswith('__'): + super(_EnumMeta, cls).__setattr__(new_enum, attr_name, attr_value) + continue + + if getattr(options, 'force_uppercase', False): + attr_dict.pop(attr_name) + attr_name = attr_name.upper() + + enum_item = EnumItem(new_enum, attr_name, attr_value) + + enum_items[attr_name] = enum_item + super(_EnumMeta, cls).__setattr__(new_enum, attr_name, enum_item) + + if getattr(options, "frozen", True): + super(_EnumMeta, cls).__setattr__(new_enum, '__frozen__', True) + else: + super(_EnumMeta, cls).__setattr__(new_enum, '__frozen__', False) + + if getattr(options, "strict", False): + super(_EnumMeta, cls).__setattr__(new_enum, '__strict__', True) + else: + super(_EnumMeta, cls).__setattr__(new_enum, '__strict__', False) + + super(_EnumMeta, cls).__setattr__(new_enum, '__enum_item_map__', enum_items) + + return new_enum + + def __setattr__(cls, name, val): + if getattr(cls, "__frozen__", False): + raise TypeError("can't set attributes on a frozen enum") + + if name in cls.__enum_item_map__: + val = EnumItem(cls, name, val) + cls.__enum_item_map__[name] = val + + super(_EnumMeta, cls).__setattr__(name, val) + + @property + def is_strict(cls): + return getattr(cls, "__strict__", True) + + def get_name_value_map(cls): + e = cls.__enum_item_map__ + return dict((e[i].name, e[i].value) for i in e) + + +class Enum(_EnumMeta("EnumBase", (object, ), {})): + pass diff --git a/game/python-extra/utils/lists.py b/game/python-extra/utils/lists.py new file mode 100644 index 0000000..aacffdb --- /dev/null +++ b/game/python-extra/utils/lists.py @@ -0,0 +1,42 @@ +"""List-related functions""" + + +def unlist(list_thing, complain=True): + """transforms [Something] -> Something. By default, raises a ValueError for + any other list values.""" + if complain and len(list_thing) > 1: + raise ValueError("More than one element in {}".format(list_thing)) + elif len(list_thing) == 1: + return list_thing[0] + + if complain: + raise ValueError("Nothing in {}".format(list_thing)) + return None + + +def flatten(iterable): + """Fully flattens an iterable: + In: flatten([1,2,3,4,[5,6,[7,8]]]) + Out: [1,2,3,4,5,6,7,8] + """ + container = iterable.__class__ + + placeholder = [] + for item in iterable: + try: + placeholder.extend(flatten(item)) + except TypeError: + placeholder.append(item) + + return container(placeholder) + + +def flat_map(iterable, func): + """func must take an item and return an interable that contains that + item. this is flatmap in the classic mode""" + results = [] + for element in iterable: + result = func(element) + if len(result) > 0: + results.extend(result) + return results diff --git a/game/python-extra/utils/math.py b/game/python-extra/utils/math.py new file mode 100644 index 0000000..eaae187 --- /dev/null +++ b/game/python-extra/utils/math.py @@ -0,0 +1,16 @@ +import collections +import operator + +# py3 doesn't include reduce as a builtin +try: + reduce +except NameError: + from functools import reduce + + +def product(sequence, initial=1): + """like the built-in sum, but for multiplication.""" + if not isinstance(sequence, collections.Iterable): + raise TypeError("'{}' object is not iterable".format(type(sequence).__name__)) + + return reduce(operator.mul, sequence, initial) diff --git a/game/python-extra/utils/objects.py b/game/python-extra/utils/objects.py new file mode 100644 index 0000000..f22efcd --- /dev/null +++ b/game/python-extra/utils/objects.py @@ -0,0 +1,59 @@ +_get_attr_raise_on_attribute_error = "RAISE ON EXCEPTION" + +def get_attr(obj, string_rep, default=_get_attr_raise_on_attribute_error, separator="."): + """ getattr via a chain of attributes like so: + >>> import datetime + >>> some_date = datetime.date.today() + >>> get_attr(some_date, "month.numerator.__doc__") + 'int(x[, base]) -> integer\n\nConvert a string or number to an integer, ... + """ + attribute_chain = string_rep.split(separator) + + current_obj = obj + + for attr in attribute_chain: + try: + current_obj = getattr(current_obj, attr) + except AttributeError: + if default is _get_attr_raise_on_attribute_error: + raise AttributeError( + "Bad attribute \"{}\" in chain: \"{}\"".format(attr, string_rep) + ) + return default + + return current_obj + + +class ImmutableWrapper(object): + _obj = None + _recursive = None + + def __init__(self, obj, recursive): + self._obj = obj + self._recursive = recursive + + def __setattr__(self, name, val): + if name == "_obj" and self._obj is None: + object.__setattr__(self, name, val) + return + elif name == "_recursive" and self._recursive is None: + object.__setattr__(self, name, val) + return + + raise AttributeError("This object has been marked as immutable; you cannot set its attributes.") + + def __getattr__(self, name): + if self._recursive: + return immutable(getattr(self._obj, name), recursive=self._recursive) + + return getattr(self._obj, name) + + def __repr__(self): + return "<Immutable {}: {}>".format(self._obj.__class__.__name__, self._obj.__repr__()) + + +def immutable(obj, recursive=True): + """wraps the argument in a pass-through class that disallows all attribute + setting. If the `recursive` flag is true, all attribute accesses will + return an immutable-wrapped version of the "real" attribute.""" + return ImmutableWrapper(obj, recursive) |