Source code for cotk._utils.metaclass

"""A lib for decorator and metaclass"""
import re
import inspect
import types
from typing import Optional, Iterable, Any
from functools import wraps

[docs]class DocStringInheritor(type): """ A meta class. It make the class: * Docstring can inherit the parent classes. * {STRING_DOCS} in docs will be replaced by self.STRING_DOCS or BaseClass.STRING_DOCS. (choose the first available one) * {BaseClassName.STRING_DOCS} in docs will be replaced by BaseClassName.STRING_DOCS. (BaseClassName can be itself) * If the docstring iherited from the parent class Base, {_STRING_DOCS} try to use the value of self._STRING_DOCS first (if not existed, then trying BASE._STRING_DOCS), but {STRING_DOCS} (without underscore prefix) will use the value of Base.STRING_DOCS * The replacement can be nested. But only {BaseClassName.STRING_DOCS} or {_STRING_DOCS} are allowed in the nested replacement, for avoiding ambiguous use. A variation on http://groups.google.com/group/comp.lang.python/msg/26f7b4fcb4d66c95 by Paul McGuire from https://stackoverflow.com/questions/8100166/inheriting-methods-docstrings-in-python """ def __new__(cls, name, bases, clsdict): def find_base(base_name): if name == base_name: return clsdict for mro_cls in (mro_cls for base in bases for mro_cls in base.mro()): if mro_cls.__name__ == base_name: return mro_cls raise ValueError("No bases named %s" % base_name) def find_attr(attr_name): if "." in attr_name: base_name, attr_name = attr_name.split(".") base = find_base(base_name) if isinstance(base, dict): return base[attr_name] else: return getattr(base, attr_name) else: if attr_name in clsdict: return clsdict[attr_name] for mro_cls in (mro_cls for base in bases for mro_cls in base.mro()): if hasattr(mro_cls, attr_name): return getattr(mro_cls, attr_name) raise KeyError("No such attr.") def replace_for_clsdict(first): def replace(matched): attr_name = matched.group(1) if not first: if "." not in attr_name and not attr_name.startswith("_"): raise ValueError(("%s are not allowed in nested replacement when interpreting docstring of class %s, " "try to use BaseClassName.ATTR or _ATTR instead.") % (attr_name, name)) try: return find_attr(attr_name) except ValueError as err: if err.args[0].startswith("No bases"): raise ValueError("Can't find %s when interpreting docstring of class %s, because the class doesn't have a baseclass named %s." \ % (attr_name, name, attr_name.split(".")[0])) else: raise except (AttributeError, KeyError): raise ValueError("Can't find %s when interpreting docstring of class %s, please check whether the CONSTANT exists." \ % (attr_name, name)) return replace def replace_for(attr, first): def replace(matched): attr_name = matched.group(1) if not first: if "." not in attr_name and not attr_name.startswith("_"): raise ValueError("%s are not allowed in nested replacement when interpreting docstring of class %s, " "try to use BaseClassName.ATTR or _ATTR instead." % (attr_name, name)) try: return find_attr(attr_name) except ValueError as err: if err.args[0].startswith("No bases"): raise ValueError("Can't find %s when interpreting docstring of %s.%s, because the class doesn't have a baseclass named %s." \ % (attr_name, name, attr, attr_name.split(".")[0])) else: raise except (AttributeError, KeyError): raise ValueError("Can't find %s when interpreting docstring of %s.%s, please check whether the CONSTANT exists." \ % (attr_name, name, attr)) return replace # modify class docstring # first, inherit docstring from bases if not('__doc__' in clsdict and clsdict['__doc__']): for mro_cls in (mro_cls for base in bases for mro_cls in base.mro()): # iterate from bases in MRO if mro_cls.META_DOC: clsdict['__doc__'] = mro_cls.META_DOC break # second, subtitute CONSTANT with _ prefix first = True if clsdict.get('__doc__'): while True: doc = re.sub(r'\{\b((\w*\.)?[A-Z][A-Z_]*?)\}', replace_for_clsdict(first), clsdict['__doc__']) first = False if doc == clsdict['__doc__']: break clsdict['__doc__'] = doc # save docstring now as META_DOC, before the final subtitution clsdict['META_DOC'] = clsdict.get('__doc__') # final, do substitution for CONSTANT first = True if clsdict.get('__doc__'): while True: doc = re.sub(r'\{\b((\w*\.)?[A-Z_]+?)\}', replace_for_clsdict(first), clsdict['__doc__']) first = False if doc == clsdict['__doc__']: break clsdict['__doc__'] = doc meta_doc = clsdict['META_DOC_FOR_ATTRIBUTES'] = {} # modify attribute docstring for attr, attribute in clsdict.items(): if isinstance(attribute, (staticmethod, classmethod)): doc = attribute.__func__.__doc__ else: doc = attribute.__doc__ # first, inherit docstring from bases if not doc: for mro_cls in (mro_cls for base in bases for mro_cls in base.mro() if hasattr(mro_cls, attr)): if attr in getattr(mro_cls, "META_DOC_FOR_ATTRIBUTES", {}): doc = mro_cls.META_DOC_FOR_ATTRIBUTES[attr] break else: doc = "" # second, subtitute CONSTANT with _ prefix first = True while True: # else do substitution for CONSTANT new_doc = re.sub(r'\{\b(((\w*\.)?)[A-Z][A-Z_]*?)\}', replace_for(attr, first), doc) first = False if doc == new_doc: break doc = new_doc # save docstring now as META_DOC, before the final subtitution meta_doc[attr] = doc # final, do substitution for CONSTANT first = True while True: # else do substitution for CONSTANT new_doc = re.sub(r'\{\b(((\w*\.)?)[A-Z_]+?)\}', replace_for(attr, first), doc) first = False if doc == new_doc: break doc = new_doc # set all doc if not doc: doc = None if isinstance(attribute, (staticmethod, classmethod)): if doc == attribute.__func__.__doc__: continue else: attribute.__func__.__doc__ = doc else: if doc == attribute.__doc__: continue elif isinstance(attribute, property): clsdict[attr] = property( attribute.fget, attribute.fset, attribute.fdel, doc) # type: ignore else: attribute.__doc__ = doc return type.__new__(cls, name, bases, clsdict)
[docs]class LoadClassInterface: r"""The support of dynamic class load."""
[docs] @classmethod def get_all_subclasses(cls) -> Iterable[Any]: '''Return a generator of all subclasses. ''' for subclass in cls.__subclasses__(): yield from subclass.get_all_subclasses() yield subclass
[docs] @classmethod def load_class(cls, class_name) -> Any: '''Return a subclass of ``class_name``, case insensitively. Arguments: class_name (str): target class name. ''' result = None for subclass in cls.get_all_subclasses(): if subclass.__name__.lower() == class_name.lower(): if result is None: result = subclass else: raise RuntimeError('There are two classes with the name "{}" located at "{}" and "{}". \ You have to remove one of them to make "load_class" work normally.'.format(\ class_name, inspect.getfile(result), inspect.getfile(subclass))) return result
def fix_doc(doc, cls): cls_str = cls.__name__ return doc.replace("self.", cls_str.lower() + ".") def copy_func(target, cls, method_name): method = getattr(cls, method_name) assert callable(method) @wraps(method) def new_method(self, *args, **kwargs): return getattr(target(self), method_name)(*args, **kwargs) new_method.__doc__ = fix_doc(cls.META_DOC_FOR_ATTRIBUTES[method_name], cls) # type: ignore return new_method def copy_property(target, cls, old_property_name): def new_property_fget(self): return getattr(target(self), old_property_name) def new_property_fset(self, a): setattr(target(self), old_property_name, a) def new_property_fdel(self): delattr(target(self), old_property_name) doc = fix_doc(cls.META_DOC_FOR_ATTRIBUTES[old_property_name], cls) return property(new_property_fget, new_property_fset, new_property_fdel, doc) #type: ignore