I'm writing a pure-python geometry tool for technical drawings / computational geometry (not a solver, as a solver has to work with constraints). I've already mentioned a previous version on code review, as an example for Metaclass Wrapping Builtin to Enforce Subclass Type. This question is about the Point class and the documentation/style/examples or packaging. I'm using Yapf
/isort
for formatting, and it would be helpful if I could get further best practices.
Tree
$ tree --gitignore
.
├── conf.py
├── geometry
│ ├── __init__.py
│ └── point.py
├── __init__.py
├── LICENSE.MD
├── README.md
├── ROADMAP.md
├── TODO.MD
└── _utils
└── _type_enforcer.py
point.py
#!/usr/bin/env python
import math
from math import cos, sin, tan
from typing import ClassVar, Iterable, Union
from .._utils._type_enforcer import TypeEnforcerMetaclass
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, 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 0 == 0+0j
def __new__(cls, *args, **kwargs):
if len(args) == 1:
if isinstance(args[0], complex):
return super().__new__(cls, args[0].real, args[0].imag,
*args[1:], **kwargs)
elif isinstance(args[0], Iterable) and len(args[0]) == 2:
return super().__new__(cls, args[0][0], args[0][1], *args[1:],
**kwargs)
else:
raise ValueError(
"""Only complex or iterable with length 2 can be used
with single-argument form""")
else:
return super().__new__(cls, *args, **kwargs)
def __init__(self, x, y: float = 0):
pass
def __repr__(self):
return f"Point({self.real}, {self.imag})"
def __complex__(self):
"""Convert self to exact type complex"""
return complex(self)
def __getitem__(self, val):
if val not in range(0, 2):
raise IndexError("")
def __mul__(self, other) -> "Point":
"""Element-wise multiplication of two points, unless other is a real.number or a complex. Falls back on complex behavior.
:param other: Another point, real number, or complex
:type other: ``int``, ``float``, ``point``, ``complex``
::
.. highlight:: python
>>> 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):
"""
X coordinate of the point. Represented internally as real component
::
.. highlight:: python
>>> a = Point(3, 5)
>>> a.x
3.0
>>> a.x = 5
>>> a
Point(5.0, 5.0)
"""
return self.real
@x.setter
def x(self, val):
self.real = val
@property
def y(self):
"""
Y coordinate of the point. Represented internally as imag component
::
.. highlight:: python
>>> a = Point(3, 5)
>>> a.y
5.0
>>> a.y = 2
>>> a
Point(5.0, 2.0)
"""
return self.imag
@y.setter
def y(self, val):
self.imag = val
def scale(self, other, center=ORIGIN):
"""Scales self by other around center
:param other: vector to scale self by. Floats are interpreted as
scaling by (other, other)
:type other: Point, Iterable, complex, float
::
.. highlight:: python
>>> Point(2,1).scale(2)
Point(4,2)
>>> Point(3,4).scale((3, 5))
Point(9, 20)
>>> Point(2,2).scale(10+10j, center=Point(2,2))
Point(2,2)
"""
scale = 0
if isinstance(other, Point):
scale = other
elif isinstance(other, Iterable) and len(other) == 2:
scale = Point(*other)
elif isinstance(other, complex):
scale = Point(other)
elif hasattr(other, "real"):
scale = Point(other, other)
else:
raise ValueError(other, "is not a valid type for Point.scale")
local = self - center
local *= scale
return local + center
def midpoint(self, other):
"""Midpoint between self and other, in cartesian coordinates.
Floats are interpreted as Point(other, 0)
:param other: coordinate to take midpoint between it and self
:type other: Point, complex or float.
::
.. highlight:: python
>>> Point(3, 5).midpoint(Point(5, 3))
Point(4, 4)
"""
return self * 0.5 + other * 0.5
def distance(self, other: Point = ORIGIN) -> float:
"""Euclidian distance between self and other
Floats are interpreted as Point(other, 0)
:param other: coordinate to take euclidian distance of
:returns: Euclidian distance
:rtype: float
::
.. highlight:: python
>>> Point(3.0, 4.0).distance()
5.0
"""
return abs(self - other)
def taxicab_distance(self, other: Point = ORIGIN) -> float:
"""Returns taxicab distance between self and other.
Floats are interpreted as Point(other, 0)
:param other: coordinate to take taxicab distance of.
:type other: Point, complex, float
:returns: `abs(self.x - other.x) + abs(self.y - other.y)`
:rtype: float
::
.. highlight:: python
>>> Point(3.0, 4.0).taxicab_distance()
7.0
>>> # 3.0 - 0.0 + 4.0 - 0.0 = 7.0
"""
return abs(self.real - other.real) + abs(self.imag - other.imag)
def rotate(self, theta: float, center: Point = ORIGIN) -> Point:
"""Rotates point around center by theta radians. Equivalent to
`Point(self * cmath.exp(theta * 1j * cmath.pi))`
:param theta: angle of rotation in radians
:param center: center of rotation. Defaults to ORIGIN
:returns: new rotated Point
:rtype: Point
::
.. highlight:: python
>>> Point(2,3).rotate(math.pi/2)
Point(-3.0, -2.0)
"""
local_coords = self - center
# Multiplying by z, where abs(z) == 1 is the same as rotation
local_coords *= Point.__conj_rotation(theta)
local_coords += center
return local_coords
@staticmethod
def __conj_rotation(theta: float):
"""Returns a complex representing a rotation vector.
:param theta: theta, in radians of the rotation
:type theta: float
:return: representation of rotation
:rtype: complex
::
.. highlight:: python
>>> Point._Point__conj_rotation(math.pi)
(-1+...e-16j)
"""
return complex(cos(theta), sin(theta))
if __name__ == "__main__":
import doctest
doctest.testmod(optionflags=doctest.ELLIPSIS)
_type_enforcer.py
#!/usr/bin/env python
import logging
from types import MemberDescriptorType, WrapperDescriptorType
default_exclude = {"__new__", "__getattribute__", "__setattribute__"}
class TypeEnforcerMetaclass(type):
"""
Metaclass that enforces return type. Ensures that inhereted methods return
proper class (that of the inheritee), as in the case of builtins.
When `enforce_all == True`, creates intermediate class which is wrapped to
the correct type.
:param enforce_all: Enforces return type of super()
:type enforce_all: ``bool``
:param exclude: Ignore special names, such as __X__.
: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)
logging.info(f"Creating class {name} in {meta.__name__}")
# Creates a new abstraction layer (middleman class), so super()
# returns wrapped class
# that has all its methods wrapped.
# ┌────────────┐ ┌────────────┐ ┌────────────┐
# │ Point │⇦│ _compl │⇦│ complex │
# └────────────┘ └────────────┘ └────────────┘
superclass = bases[0]
if enforce_all:
logging.debug(f"Parameter enforce_all is turned ON")
# Somehow, name mangling doesn't show up when printing class.
# Probably __repr__ isn't being overriden
inter_name = meta.__name__ + "." + superclass.__name__
logging.debug(f"Creating intermediate class {inter_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)
logging.debug("New Bases", bases)
logging.debug("sub:", subclass, "meta:", type(subclass))
# Has the potential to cause errors if not enabled
# logging.debug("inter:", inter_class, "meta:", type(inter_class))
logging.debug("Class info: ",subclass, subclass.__name__, subclass.mro(), subclass.__dict__, sep="\n")
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):
logging.debug("Skipping", obj, "Due to MemberDescriptor")
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
logging.info("Wrapping", obj, "to return", type_to_convert.__name__)
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"""
if isinstance(func, (str, int, float)):
return func
logging.debug("Decorator", cls, func.__name__)
def convert_if(val):
if isinstance(val, cls):
logging.debug("Wrapped:", val.__class__, val)
return convert_cls(val)
else:
logging.debug("Skipped:", val.__class__, val)
logging.debug("Reason:", cls.__class__, "!=", val.__class__)
return val
def wrapper(*args, **kwargs):
logging.debug("Wrapper", cls, func.__name__)
return convert_if(func(*args, **kwargs))
wrapper.__wrapped__ = func
return wrapper
if __name__ == "__main__":
import doctest
logging.setLevel(logging.INFO)
logging.info("Running doctests")
doctest.testmod(report=True)
__mul__
. It is not how complex numbers are multiplied. \$\endgroup\$__mul__
, and if you read it carefully, you will see that multiplying a point by a complex number, will callsuper().__mul__(other)
, thereby achieving the correct multiplication if so desired. Even with the type enforcer metaclass, it will only change the return type to return aPoint
\$\endgroup\$