Source code for hbutils.model.clazz

"""
Overview:
    Useful functions for building class models. This module provides decorators and utilities
    for enhancing Python classes with features like automatic field access, visual representation,
    constructor generation, hash/equality operations, and property accessors.
"""
import os
import textwrap
from typing import Optional, Iterable

from ._info import _PACKAGE_RST
from ..config.meta import __VERSION__
from ..design.singleton import SingletonMark
from ..reflection import fassign

__all__ = [
    'get_field',
    'asitems', 'visual', 'constructor', 'hasheq', 'accessor',
]

CLASS_WRAPPER_UPDATES = ()


def _cls_field_name(cls: type, name: str):
    """
    Get the mangled field name for a class.

    :param cls: The class type.
    :type cls: type
    :param name: The field name.
    :type name: str
    :return: The mangled field name.
    :rtype: str

    .. note::
        This function handles Python's name mangling for private attributes (those starting with __).
    """
    if name.startswith('__'):
        return f'_{cls.__name__.lstrip("_")}__{name[2:]}'
    else:
        return name


_NO_DEFAULT_VALUE = SingletonMark('no_default_value')


[docs] def get_field(obj, name: str, default=_NO_DEFAULT_VALUE): """ Get field from object. Private field is supported. :param obj: The given object. :type obj: object :param name: Field to be got. :type name: str :param default: Default value when failed. Defaults to _NO_DEFAULT_VALUE. :return: Field value of the field. :raises AttributeError: If the field does not exist and no default is provided. Example:: >>> class MyClass: ... def __init__(self): ... self.__private_field = 42 >>> obj = MyClass() >>> get_field(obj, '__private_field') 42 >>> get_field(obj, 'nonexistent', default='default_value') 'default_value' """ _field_name = _cls_field_name(type(obj), name) if default is _NO_DEFAULT_VALUE: return getattr(obj, _field_name) else: return getattr(obj, _field_name, default)
def _cls_private_prefix(cls): """ Get the private field prefix for a class. :param cls: The class type. :type cls: type :return: The private field prefix string. :rtype: str .. note:: This returns the prefix used for name-mangled private attributes. """ cls_name = cls.__name__.lstrip('_') return f'_{cls_name}__' def _auto_get_items_from_cls(cls): """ Get items from class's __items__ attribute. :param cls: The class type. :type cls: type :return: The items list. :rtype: list :raises AttributeError: If the class does not have __items__ attribute. """ return cls.__items__ def _auto_get_items(obj, cls_prefix=None): """ Automatically get items from an object by inspecting its attributes. :param obj: The object to inspect. :type obj: object :param cls_prefix: The class private prefix. Defaults to None. :type cls_prefix: Optional[str] :return: Sorted list of item names. :rtype: list[str] .. note:: This function first tries to get items from the class's __items__ attribute. If that fails, it inspects the object's attributes and extracts private field names. """ _type = type(obj) try: return _auto_get_items_from_cls(_type) except AttributeError: cls_prefix = cls_prefix or _cls_private_prefix(_type) items = [] for name in dir(obj): if name.startswith(cls_prefix): items.append(name[len(cls_prefix):]) return sorted(items) def _get_value(self, vname: str, cls_prefix=None): """ Get value from object by name, supporting both regular and private attributes. :param self: The object instance. :type self: object :param vname: The value name. :type vname: str :param cls_prefix: The class private prefix. Defaults to None. :type cls_prefix: Optional[str] :return: The attribute value. :raises AttributeError: If the attribute does not exist. """ cls_prefix = cls_prefix or _cls_private_prefix(type(self)) try: return getattr(self, vname) except AttributeError: return getattr(self, cls_prefix + vname)
[docs] def asitems(items: Iterable[str]): """ Define fields in the class level. :param items: Field name items. :type items: Iterable[str] :return: Decorator to decorate the given class. :rtype: Callable Example:: >>> @visual() >>> @constructor(doc=''' ... Overview: ... This is the constructor of class :class:`T`. ... ''') >>> @asitems(['x', 'y']) >>> class T: ... @property ... def x(self): ... return self.__first ... ... @property ... def y(self): ... return self.__second .. note:: This decorator sets the __items__ attribute on the class, which is used by other decorators to determine which fields to process. """ def _decorator(cls): _cls_prefix = _cls_private_prefix(cls) cls.__items__ = list(items) return cls return _decorator
[docs] def visual(items: Optional[Iterable] = None, show_id: bool = False): """ Decorate class to be visible by ``repr``. :param items: Items to be displayed. Default is None, which means automatically find the private fields and display them. :type items: Optional[Iterable] :param show_id: Show hex id in representation string or not. Defaults to False. :type show_id: bool :return: Decorator to decorate the given class. :rtype: Callable Example:: >>> @visual() >>> class T: ... def __init__(self, x, y): ... self.__first = x ... self.__second = y >>> repr(T(1, 2)) '<T first: 1, second: 2>' >>> @visual(show_id=True) >>> class T2: ... def __init__(self, x): ... self.__x = x >>> repr(T2(42)) '<T2 0x... x: 42>' .. note:: This decorator automatically generates a __repr__ method for the class that displays the specified fields or all private fields if none are specified. """ def _decorator(cls): _cls_prefix = _cls_private_prefix(cls) def _get_items(self): if items is None: return _auto_get_items(self, _cls_prefix) else: return items def __repr__(self): f""" Get representation format of class {cls.__name__}. .. note:: Created by {_PACKAGE_RST}, v{__VERSION__}. """ str_items = [] for item in _get_items(self): if isinstance(item, str): _name, _repr = item, repr else: _name, _repr = item _value = _get_value(self, _name, _cls_prefix) try: _vrepr = _repr(_value) except ValueError: pass else: str_items.append(f'{_name}: {_vrepr}') sentences = [] if show_id: sentences.append(hex(id(self))) if str_items: sentences.append(', '.join(str_items)) if sentences: str_sentences = ' ' + ' '.join(sentences) else: str_sentences = '' return f'<{type(self).__name__}{str_sentences}>' cls.__repr__ = __repr__ return cls return _decorator
_INDENT = ' ' * 4
[docs] def constructor(params: Optional[Iterable] = None, doc: Optional[str] = None): r""" Decorate class to create an init function. :param params: Parameters of constructor, should be an iterator of items, each item should be a single string or a tuple of string and default value. Default is None, which means no arguments. :type params: Optional[Iterable] :param doc: Documentation of constructor function. Defaults to None. :type doc: Optional[str] :return: Decorator to decorate the given class. :rtype: Callable Example:: >>> @constructor(['x', ('y', 2)], ''' ... Overview: ... This is the constructor of class :class:`T`. ... ''') >>> class T: ... # the same as: ... # def __init__(self, x, y=2): ... # self.__x = x ... # self.__y = y ... ... @property ... def x(self): ... return self.__x ... ... @property ... def y(self): ... return self.__y >>> t = T(1) >>> t.x, t.y (1, 2) .. note:: This decorator automatically generates an __init__ method for the class that initializes the specified fields with the provided parameters. """ def _decorator(cls): _cls_prefix = _cls_private_prefix(cls) if params is None: items = _auto_get_items_from_cls(cls) else: items = params or [] actual_items = [] arg_items = [] for it in items: if isinstance(it, str): itn, itv = it, _NO_DEFAULT_VALUE else: itn, itv = it actual_items.append((itn, itv)) if itv is _NO_DEFAULT_VALUE: arg_items.append(itn) else: arg_items.append(f'{itn}={repr(itv)}') _init_func_str = f""" def __init__(self, {', '.join(arg_items)}): {os.linesep.join(f'{_INDENT}self.{_cls_prefix}{name} = {name}' for name, _ in actual_items)} """ fres = {} exec(_init_func_str, fres) _init_func = fres['__init__'] _init_func = fassign(__doc__=doc or textwrap.dedent(f''' Constructor of class {cls.__name__}. .. note:: Created by {_PACKAGE_RST}, v{__VERSION__}. The values of arguments supplied to this constructor \ will be put into the fields specified. '''))(_init_func) cls.__init__ = _init_func return cls return _decorator
[docs] def hasheq(items: Optional[Iterable] = None): """ Decorate class to support hashing and equality comparison. :param items: Items to be hashed and compared. Default is None, which means automatically find the private fields and use them. :type items: Optional[Iterable] :return: Decorator to decorate the given class. :rtype: Callable Example:: >>> @hasheq(['x', 'y']) >>> class T: ... def __init__(self, x, y): ... self.__first = x ... self.__second = y >>> t1 = T(1, 2) >>> t2 = T(1, 2) >>> t1 == t2 True >>> hash(t1) == hash(t2) True Using with :func:`asitems`:: >>> @hasheq() >>> @constructor() >>> @asitems(['x', 'y']) >>> class T1: ... pass >>> t1 = T1(1, 2) >>> t2 = T1(1, 2) >>> t1 == t2 True .. note:: This decorator generates __hash__, __eq__, and __ne__ methods for the class based on the specified fields. Objects are considered equal if they are of the same class and all specified field values are equal. """ def _decorator(cls): _cls_prefix = _cls_private_prefix(cls) def _get_items(self): if items is None: return _auto_get_items(self, _cls_prefix) else: return items def _get_obj_values(self): return tuple(_get_value(self, name, _cls_prefix) for name in _get_items(self)) def __hash__(self): f""" Hash value of class {cls.__name__}'s instances. .. note:: Created by {_PACKAGE_RST}, v{__VERSION__}. The return value is calculated based on the \ values of the internally specified fields. """ return hash(_get_obj_values(self)) def __eq__(self, other): f""" Equality between class {cls.__name__}'s instances. .. note:: Created by {_PACKAGE_RST}, v{__VERSION__}. Currently, the return value is True only if both objects \\ are of class {cls.__name__} (subclasses are not permitted) and the \\ values of the internally specified fields are all equal; \\ otherwise, it is always False. """ if self is other: return True elif type(self) == type(other): return _get_obj_values(self) == _get_obj_values(other) else: return False def __ne__(self, other): f""" Non-equality between class {cls.__name__}'s instances. .. note:: Created by {_PACKAGE_RST}, v{__VERSION__}. Currently, the return value is False only if both objects \\ are of class {cls.__name__} (subclasses are not permitted) and the \\ values of the internally specified fields are all equal; \\ otherwise, it is always True. """ return not __eq__(self, other) cls.__hash__ = __hash__ cls.__eq__ = __eq__ cls.__ne__ = __ne__ return cls return _decorator
_READONLY_MARKS = {'r', 'ro', 'readonly'} _WRITABLE_MARKS = {'w', 'rw', 'writable'} def _is_writable(mark: str): """ Check if an accessibility mark indicates writable access. :param mark: The accessibility mark string. :type mark: str :return: True if writable, False if readonly. :rtype: bool :raises ValueError: If the mark is not recognized. .. note:: Valid readonly marks: 'r', 'ro', 'readonly' Valid writable marks: 'w', 'rw', 'writable' """ if mark.lower() in _READONLY_MARKS: return False elif mark.lower() in _WRITABLE_MARKS: return True else: raise ValueError(f'Unknown accessible mark - {repr(mark)}.')
[docs] def accessor(items: Optional[Iterable] = None, readonly: bool = False): """ Decorate class to be accessible by property accessors. :param items: Items to be accessed. Default is None, which means automatically find the private fields and create accessors for them. :type items: Optional[Iterable] :param readonly: Default readonly or not. Default is False, which means make the accessor be writable when ``rw`` option is not given. :type readonly: bool :return: Decorator to decorate the given class. :rtype: Callable Example:: >>> @accessor(readonly=True) >>> @asitems(['x', 'y']) >>> class T: ... def __init__(self, x, y): ... self.__x = x ... self.__y = y >>> t = T(2, 100) >>> t.x # 2 2 >>> t.y # 100 100 With writable access: >>> @accessor(readonly=False) >>> @asitems(['x', 'y']) >>> class T2: ... def __init__(self, x, y): ... self.__x = x ... self.__y = y >>> t = T2(2, 100) >>> t.x = 42 >>> t.x 42 With mixed access control: >>> @accessor([('x', 'ro'), ('y', 'rw')]) >>> class T3: ... def __init__(self, x, y): ... self.__x = x ... self.__y = y >>> t = T3(1, 2) >>> t.y = 42 # OK >>> # t.x = 10 # This would raise AttributeError .. note:: This decorator automatically generates property accessors for the specified fields. Each item can be either a string (field name) or a tuple of (field name, access mode). Access modes: 'r'/'ro'/'readonly' for read-only, 'w'/'rw'/'writable' for read-write. """ def _decorator(cls): _cls_prefix = _cls_private_prefix(cls) if items is None: actual_items = _auto_get_items_from_cls(cls) elif isinstance(items, str): actual_items = [items] else: actual_items = items or [] pitems = [] for it in actual_items: if isinstance(it, str): itn, itv = it, ('ro' if readonly else 'rw') else: itn, itv = it itv = _is_writable(itv) pitems.append((itn, itv)) for itn, itv in pitems: _getter_func_str = f""" def get_{itn}(self): {_INDENT}return self.{_cls_prefix}{itn} """ fres = {} exec(_getter_func_str, fres) getter_func = fres[f'get_{itn}'] getter_func = fassign(__doc__=f""" Property {itn}. .. note:: Created by {_PACKAGE_RST}, v{__VERSION__}. {'Both reading and writing are' if itv else 'Only reading is'} \\ permitted with the accessor ``{itn}``. """)(getter_func) if itv: _setter_func_str = f""" def set_{itn}(self, new_value): {_INDENT}self.{_cls_prefix}{itn} = new_value """ fres = {} exec(_setter_func_str, fres) setter_func = fres[f'set_{itn}'] p = property(getter_func, setter_func) else: p = property(getter_func) setattr(cls, itn, p) return cls return _decorator