This metaclass utility is written to make subclassing builtins easier, such as int
, str
, and most importantly, complex
. All dependencies are part of the standard library. I'm concerned with 1. readability, and 2. possible performance implications of wrapping every single inherited method of the created classes. The option enforce_all=True
complicates this further by creating an intermediate class, which is then wrapped, for the reason of making calls to super()
return the correct (subclass) type. This option simplifies development down the line.
I've tried my best to implement best practices such as formatting, docstrings, testing, etc. I am aware that there are several unused imports.
Metaclass
#!/usr/bin/env python
from types import MemberDescriptorType, WrapperDescriptorType
# Test for inherited funcs
# [getattr(complex, a) == getattr(super(complex), a, None) for a,o in complex.__dict__.items()]
default_exclude = {"__new__", "__getattribute__", "__getattr__", "__setatt__"}
class TypeEnforcerMetaclass(type):
"""
Black magic return type wrapper. Ensures that methods inhereted return
proper class (that of the inheritee)
:param enforce_all: Enforces return type of super() through Blacker magic.
:type enforce_all: ``bool``
:param exclude: Ignores types
:type enforce_all: ``bool``
Example::
>>> class A(int, metaclass=TypeEnforcerMetaclass, enforce_all=True, exclude={"real"}):
... def __repr__(self):
... return f"A({super().real})"
>>> A(1)
A(1)
>>> A(1) + A(4)
A(5)
>>> A(3) * A(-3)
A(-9)
>>> super(A)
<super: <class 'A'>, NULL>
>>> type(A)
<class '__main__.TypeEnforcerMetaclass'>
"""
def __new__(meta,
name,
bases,
classdict,
enforce_all=False,
exclude=default_exclude):
exclude = exclude.union(default_exclude)
# Creates a new abstraction layer, so super() returns wrapped class
# that has all its methods wrapped.
# ┌────────────┐ ┌────────────┐ ┌────────────┐
# │ Point │⇦│ _compl │⇦│ complex │
# └────────────┘ └────────────┘ └────────────┘
superclass = bases[0]
if enforce_all:
# Somehow, name mangling doesn't show up on the class. Probably
# __repr__ isn't being overriden
inter_name = meta.__name__ + "." + superclass.__name__
inter_dict = dict(superclass.__dict__)
inter_class = super(TypeEnforcerMetaclass,
meta).__new__(meta, inter_name, bases,
inter_dict)
bases = (inter_class, *bases) # Neat trick for tuples
subclass = super(TypeEnforcerMetaclass,
meta).__new__(meta, name, bases, classdict)
# print("new bases", bases)
# print("sub:", subclass, "meta:", type(subclass))
# print("inter:", inter_class, "meta:", type(inter_class))
# # print(cls, cls.__name__, cls.mro(), cls.__dict__, sep="\n")
# # print(cls, "inherets from", cls.mro()[1])
base_to_wrap = subclass.mro()[1]
type_compare = subclass
type_to_convert = subclass
if enforce_all:
type_compare = inter_class
base_to_wrap = inter_class.mro()[1]
for attr, obj in base_to_wrap.__dict__.items():
if isinstance(obj, MemberDescriptorType):
continue
# Traverse the mro
# == testing is exactly what we want for comparing overridden
# definitions,
if obj == getattr(type_compare, attr,
None) and attr not in exclude:
# Dont override __new__!
# Check if the method is inhereted from base_to_wrap
# Iff inherited, wrap the return type
setattr(
type_compare, attr,
TypeEnforcerMetaclass.return_wrapper(
superclass, type_to_convert, obj))
return subclass
def return_wrapper(cls, convert_cls, func):
"""Wraps class methods and enforces type"""
# print(cls, func.__name__)
def convert_if(val):
if isinstance(val, cls):
# print("Converting", val)
# print("Class:", type(val))
return convert_cls(val)
else:
# print("Not Converting", val)
# print("Class:", val.__class__)
# print("Required class", cls.__class__)
return val
def wrapper(*args, **kwargs):
# print("Wrapper around", func)
return convert_if(func(*args, **kwargs))
wrapper.__wrapped__ = func
return wrapper
if __name__ == "__main__":
import doctest
doctest.testmod()
Demo code (what it makes possible)
#!/usr/bin/env python
from .._utils._return_wrapper import TypeEnforcerMetaclass, no_override
from typing import ClassVar, Union
from math import sin, cos, tan
Point = ClassVar["Point"]
class Point(complex, metaclass=TypeEnforcerMetaclass, enforce_all=True):
r"""
:param x: x coordinate of the point
:type x: ``int``, ``float``
:param y: y coordinate of the point. Stored as the imaginary component
:type y: ``int``, ``float``
::
.. highlight:: python
>>> a = Point(2, 3)
>>> b = Point(-3, 5)
>>> a
Point(2.0, 3.0)
>>> a + b
Point(-1.0, 8.0)
>>> a.midpoint(b)
Point(-0.5, 4.0)
>>> a * b
Point(-6.0, 15.0)
"""
ORIGIN = 0 # Works because complex assumes 0 == 0+0j
# def __new__(cls,*args, **kwargs):
# if len(args) == 1:
# if isinstance(args[0], complex):
# return super(object, cls).__new__(args[0])
# else:
# return super().__new__(*args, **kwargs)
def __new__(cls, *args, **kwargs):
if len(args) == 1 and isinstance(args[0], complex):
return super().__new__(cls, args[0].real, args[0].imag, *args[1:], **kwargs)
else:
return super().__new__(cls, *args, **kwargs)
def __init__(self, x, y:float=0):
# self.real = x
# self.imag = y
pass
def __repr__(self):
return f"Point({self.real}, {self.imag})"
def __mul__(self, other) -> "Point":
"""Element-wise multiplication of two points, unless other is a real.number or a conjugate. Falls back on conjugate behavior.
:param other: Another point, real number, or conjugate
:type other: ``int``, ``float``, ``point``, ``complex``
>>> Point(-3, 6) * Point(2, -3)
Point(-6.0, -18.0)
"""
if isinstance(other, Point):
return Point(self.real * other.real, self.imag * other.imag)
return super().__mul__(other)
@property
def x(self):
return self.real
@x.setter
def x(self, val):
self.real = val
@property
def y(self):
return self.imag
@y.setter
def y(self, val):
self.imag = val
def midpoint(self, other):
""""""
return self * 0.5 + other * 0.5
def distance(self, other:Point=ORIGIN) -> float:
"""Euclidian distance between points
:param other: Point or conjugate. Interprets real numbers as (real, 0)
:returns: float
>>> Point(3.0, 4.0).distance()
5.0
"""
return abs(self - other)
def rotate(self, angle:float, center:Point=ORIGIN) -> Point:
"""Rotates point around center by angle
:param angle: Angle in radians
:param center: Center of rotation. Defaults to ORIGIN
:returns: New Point with the transformation
"""
raise NotImplementedError
@classmethod
def __conj_rotation(*args):
"""
>>> Point._Point__conj_rotation(1.0)
1.0
"""
raise NotImplementedError
#return complex(cos(angle), sin(angle))
if __name__ == "__main__":
import doctest
doctest.testmod()