5
\$\begingroup\$

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()

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

shebang

#!/usr/bin/env python

The shebang looks beautiful, thank you. Everyone should implement like this. Very portable, and it obeys $PATH.


black, isort

... tried my best to implement best practices such as formatting, docstrings, ...

Largely you don't even have to think about such details. Just do this every now and again:

$ black . ; isort .

(First time out you might prefer black -S ., to avoid single- vs double- quote diff details.)

I was initially reluctant to adopt black. The secret to making it work effectively turns out to be adding an "extra" trailing , comma in places where you desire one item per line. Also, if you're unhappy with the layout of "... foo_expression, bar_expression, ..." it can be helpful to create some more local variables:

        foo = ...
        bar = ...
        ..., foo, bar, ...

delete comments upon merging

# Test for inherited funcs
# [getattr(complex, a) == getattr(super(complex), a, None) for a,o in complex.__dict__.items()]

Sorry, didn't quite follow along, there. I'm sure that snippet was useful at one point. But now we want to merge a PR down to main. This is the right time to delete commented code. Or put it in a private _method() with no callers.

Similarly for printing new bases.

For the various methods containing commented print statements, consider adding a keyword default argument of ... , verbose=False): to the signature. Then an if verbose: guard suffices to optionally print the debug data.

Consider using a logger at DEBUG or INFO level for such debugs.


cryptic docstring

class TypeEnforcerMetaclass(type):
    """ 
    Black magic return type wrapper. ...

Sorry, but I'm not happy with that description. It's more appropriate for a # comment. When describing your Public API to a prospective caller, it is incumbent on you to peel aside the curtain concealing the Great Oz, and reveal what expectations caller can rely upon.

Similarly for "through Blacker magic". Consider including an URL that offers a deeper exploration of the details.

The example is beautiful, thank you.

\$\endgroup\$
1
  • \$\begingroup\$ Also, if the question obeys the rules of codereview SE, please upvote! \$\endgroup\$ Aug 30, 2023 at 1:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.