#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals
import inspect
from copy import deepcopy
from pprint import pprint
from collections import OrderedDict
try:
from .pkg.pylru import lrudecorator
from .pkg.sixmini import integer_types, string_types, add_metaclass
from .pkg.inspect_mate import (
is_class_method, is_regular_method, get_all_attributes,
)
from .pkg.pytest import approx
from .pkg.superjson import json
except: # pragma: no cover
from constant2.pkg.pylru import lrudecorator
from constant2.pkg.sixmini import integer_types, string_types, add_metaclass
from constant2.pkg.inspect_mate import (
is_class_method, is_regular_method, get_all_attributes,
)
from constant2.pkg.pytest import approx
from constant2.pkg.superjson import json
try:
del json._dumpers["collections.OrderedDict"]
except KeyError: # pragma: no cover
pass
class _Constant(object):
"""Generic Constantant.
Inherit from this class to define a data container class.
all nested Constant class automatically inherit from :class:`Constant`.
"""
__creation_index__ = 0 # Used for sorting
def __init__(self):
"""
.. versionadded:: 0.0.3
"""
for attr, value in self.__class__.Items():
value = deepcopy(value)
setattr(self, attr, value)
for attr, Subclass in self.Subclasses():
value = Subclass()
setattr(self, attr, value)
self.__creation_index__ = Constant.__creation_index__
Constant.__creation_index__ += 1
def __repr__(self):
items_str = ", ".join([
"%s=%r" % (attr, value) for attr, value in self.items()
])
nested_str = ", ".join([
"%s=%r" % (attr, subclass) for attr, subclass in self.subclasses()
])
l = list()
if items_str:
l.append(items_str)
if nested_str:
l.append(nested_str)
return "{classname}({args})".format(
classname=self.__class__.__name__, args=", ".join(l))
@classmethod
def Items(cls):
"""non-class attributes ordered by alphabetical order.
::
>>> class MyClass(Constant):
... a = 1 # non-class attributre
... b = 2 # non-class attributre
...
... class C(Constant):
... pass
...
... class D(Constant):
... pass
>>> MyClass.Items()
[("a", 1), ("b", 2)]
.. versionadded:: 0.0.5
"""
l = list()
for attr, value in get_all_attributes(cls):
# if it's not a class(Constant)
if not inspect.isclass(value):
l.append((attr, value))
return list(sorted(l, key=lambda x: x[0]))
def items(self):
"""non-class attributes ordered by alphabetical order.
::
>>> class MyClass(Constant):
... a = 1 # non-class attributre
... b = 2 # non-class attributre
...
... class C(Constant):
... pass
...
... class D(Constant):
... pass
>>> my_class = MyClass()
>>> my_class.items()
[("a", 1), ("b", 2)]
.. versionchanged:: 0.0.5
"""
l = list()
# 为什么这里是 get_all_attributes(self.__class__) 而不是
# get_all_attributes(self) ? 因为有些实例不支持
# get_all_attributes(instance) 方法, 会报错。
# 所以我们从类里得到所有的属性信息, 然后获得这些属性在实例中
# 对应的值。
for attr, value in get_all_attributes(self.__class__):
value = getattr(self, attr)
# if it is not a instance of class(Constant)
if not isinstance(value, Constant):
l.append((attr, value))
return list(sorted(l, key=lambda x: x[0]))
def __eq__(self, other):
return self.items() == other.items()
@classmethod
def Keys(cls):
"""All non-class attribute name list.
.. versionadded:: 0.0.5
"""
return [attr for attr, _ in cls.Items()]
def keys(self):
"""All non-class attribute name list.
.. versionchanged:: 0.0.5
"""
return [attr for attr, _ in self.items()]
@classmethod
def Values(cls):
"""All non-class attribute value list.
.. versionadded:: 0.0.5
"""
return [value for _, value in cls.Items()]
def values(self):
"""All non-class attribute value list.
.. versionchanged:: 0.0.5
"""
return [value for _, value in self.items()]
@classmethod
def ToDict(cls):
"""Return regular class variable and it's value as a dictionary data.
.. versionadded:: 0.0.5
"""
return dict(cls.Items())
def to_dict(self):
"""Return regular class variable and it's value as a dictionary data.
.. versionchanged:: 0.0.5
"""
return dict(self.items())
@classmethod
def Subclasses(cls, sort_by=None, reverse=False):
"""Get all nested Constant class and it's name pair.
:param sort_by: the attribute name used for sorting.
:param reverse: if True, return in descend order.
:returns: [(attr, value),...] pairs.
::
>>> class MyClass(Constant):
... a = 1 # non-class attributre
... b = 2 # non-class attributre
...
... class C(Constant):
... pass
...
... class D(Constant):
... pass
>>> MyClass.Subclasses()
[("C", MyClass.C), ("D", MyClass.D)]
.. versionadded:: 0.0.3
"""
l = list()
for attr, value in get_all_attributes(cls):
try:
if issubclass(value, Constant):
l.append((attr, value))
except:
pass
if sort_by is None:
sort_by = "__creation_index__"
l = list(
sorted(l, key=lambda x: getattr(x[1], sort_by), reverse=reverse))
return l
def subclasses(self, sort_by=None, reverse=False):
"""Get all nested Constant class instance and it's name pair.
:param sort_by: the attribute name used for sorting.
:param reverse: if True, return in descend order.
:returns: [(attr, value),...] pairs.
::
>>> class MyClass(Constant):
... a = 1 # non-class attributre
... b = 2 # non-class attributre
...
... class C(Constant):
... pass
...
... class D(Constant):
... pass
>>> my_class = MyClass()
>>> my_class.subclasses()
[("C", my_class.C), ("D", my_class.D)]
.. versionadded:: 0.0.4
"""
l = list()
for attr, _ in self.Subclasses(sort_by, reverse):
value = getattr(self, attr)
l.append((attr, value))
return l
@classmethod
@lrudecorator(size=64)
def GetFirst(cls, attr, value, e=0.000001, sort_by="__name__"):
"""Get the first nested Constant class that met ``klass.attr == value``.
:param attr: attribute name.
:param value: value.
:param e: used for float value comparison.
:param sort_by: nested class is ordered by <sort_by> attribute.
.. versionadded:: 0.0.5
"""
for _, klass in cls.Subclasses(sort_by=sort_by):
try:
if klass.__dict__[attr] == approx(value, e):
return klass
except:
pass
return None
def get_first(self, attr, value, e=0.000001,
sort_by="__name__", reverse=False):
"""Get the first nested Constant class that met ``klass.attr == value``.
:param attr: attribute name.
:param value: value.
:param e: used for float value comparison.
:param sort_by: nested class is ordered by <sort_by> attribute.
.. versionchanged:: 0.0.5
"""
for _, klass in self.subclasses(sort_by, reverse):
try:
if getattr(klass, attr) == approx(value, e):
return klass
except:
pass
return None
@classmethod
@lrudecorator(size=64)
def GetAll(cls, attr, value, e=0.000001, sort_by="__name__"):
"""Get all nested Constant class that met ``klass.attr == value``.
:param attr: attribute name.
:param value: value.
:param e: used for float value comparison.
:param sort_by: nested class is ordered by <sort_by> attribute.
.. versionadded:: 0.0.5
"""
matched = list()
for _, klass in cls.Subclasses(sort_by=sort_by):
try:
if klass.__dict__[attr] == approx(value, e):
matched.append(klass)
except: # pragma: no cover
pass
return matched
def get_all(self, attr, value, e=0.000001,
sort_by="__name__", reverse=False):
"""Get all nested Constant class that met ``klass.attr == value``.
:param attr: attribute name.
:param value: value.
:param e: used for float value comparison.
:param sort_by: nested class is ordered by <sort_by> attribute.
.. versionchanged:: 0.0.5
"""
matched = list()
for _, klass in self.subclasses(sort_by, reverse):
try:
if getattr(klass, attr) == approx(value, e):
matched.append(klass)
except: # pragma: no cover
pass
return matched
@classmethod
def ToIds(cls, klass_list, id_field="id"):
return [getattr(klass, id_field) for klass in klass_list]
def to_ids(self, instance_list, id_field="id"):
return [getattr(instance, id_field) for instance in instance_list]
@classmethod
def ToClasses(cls, klass_id_list, id_field="id"):
return [cls.GetFirst(id_field, klass_id) for klass_id in klass_id_list]
def to_instances(self, instance_id_list, id_field="id"):
return [self.get_first(id_field, instance_id) for instance_id in instance_id_list]
@classmethod
def SubIds(cls, id_field="id", sort_by=None, reverse=False):
return [
getattr(klass, id_field)
for _, klass in cls.Subclasses(sort_by=sort_by, reverse=reverse)
]
def sub_ids(self, id_field="id", sort_by=None, reverse=False):
return [
getattr(instance, id_field)
for _, instance in self.subclasses(sort_by=sort_by, reverse=reverse)
]
@classmethod
def BackAssign(cls,
other_entity_klass,
this_entity_backpopulate_field,
other_entity_backpopulate_field,
is_many_to_one=False):
"""
Assign defined one side mapping relationship to other side.
For example, each employee belongs to one department, then one department
includes many employees. If you defined each employee's department,
this method will assign employees to ``Department.employees`` field.
This is an one to many (department to employee) example.
Another example would be, each employee has multiple tags. If you defined
tags for each employee, this method will assign employees to
``Tag.employees`` field. This is and many to many (employee to tag) example.
Support:
- many to many mapping
- one to many mapping
:param other_entity_klass: a :class:`Constant` class.
:param this_entity_backpopulate_field: str
:param other_entity_backpopulate_field: str
:param is_many_to_one: bool
:return:
"""
data = dict()
for _, other_klass in other_entity_klass.Subclasses():
other_field_value = getattr(
other_klass, this_entity_backpopulate_field)
if isinstance(other_field_value, (tuple, list)):
for self_klass in other_field_value:
self_key = self_klass.__name__
try:
data[self_key].append(other_klass)
except KeyError:
data[self_key] = [other_klass, ]
else:
if other_field_value is not None:
self_klass = other_field_value
self_key = self_klass.__name__
try:
data[self_key].append(other_klass)
except KeyError:
data[self_key] = [other_klass, ]
if is_many_to_one:
new_data = dict()
for key, value in data.items():
try:
new_data[key] = value[0]
except: # pragma: no cover
pass
data = new_data
for self_key, other_klass_list in data.items():
setattr(getattr(cls, self_key),
other_entity_backpopulate_field, other_klass_list)
@classmethod
def dump(cls):
"""Dump data into a dict.
.. versionadded:: 0.0.2
"""
d = OrderedDict(cls.Items())
d["__classname__"] = cls.__name__
for attr, klass in cls.Subclasses():
d[attr] = klass.dump()
return OrderedDict([(cls.__name__, d)])
@classmethod
def load(cls, data):
"""Construct a Constant class from it's dict data.
.. versionadded:: 0.0.2
"""
if len(data) == 1:
for key, value in data.items():
if "__classname__" not in value: # pragma: no cover
raise ValueError
name = key
bases = (Constant,)
attrs = dict()
for k, v in value.items():
if isinstance(v, dict):
if "__classname__" in v:
attrs[k] = cls.load({k: v})
else:
attrs[k] = v
else:
attrs[k] = v
return type(name, bases, attrs)
else: # pragma: no cover
raise ValueError
@classmethod
def pprint(cls): # pragma: no cover
"""Pretty print it's data.
.. versionadded:: 0.0.2
"""
pprint(cls.dump())
@classmethod
def jprint(cls): # pragma: no cover
"""Json print it's data.
.. versionadded:: 0.0.2
"""
print(json.dumps(cls.dump(), pretty=4))
_reserved_attrs = {
"Items", "Keys", "Values",
"items", "keys", "values",
"ToDict", "to_dict",
"Subclasses", "subclasses",
"GetFirst", "get_first",
"GetAll", "get_all",
"ToIds", "to_ids",
"SubIds", "sub_ids",
"BackAssign",
"ToClasses", "to_instances",
"dump", "load", "pprint", "jprint",
}
@add_metaclass(Meta)
class Constant(_Constant):
pass
[docs]def is_same_dict(d1, d2):
"""Test two dictionary is equal on values. (ignore order)
"""
for k, v in d1.items():
if isinstance(v, dict):
is_same_dict(v, d2[k])
else:
assert d1[k] == d2[k]
for k, v in d2.items():
if isinstance(v, dict):
is_same_dict(v, d1[k])
else:
assert d1[k] == d2[k]
if __name__ == "__main__":
class Food(Constant):
class Fruit:
id = 1
name = "fruit"
class Apple:
id = 1
name = "apple"
class RedApple:
id = 1
name = "red apple"
class GreenApple:
id = 2
name = "green apple"
class Banana:
id = 2
name = "banana"
class YellowBanana:
id = 1
name = "yellow banana"
class GreenBanana:
id = 2
name = "green banana"
class Meat:
id = 2
name = "meat"
class Pork:
id = 1
name = "pork"
class Beef:
id = 2
name = "beef"
food = Food()