Skip to content

Instantly share code, notes, and snippets.

@amirsoroush
Last active November 2, 2023 20:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save amirsoroush/5f232a1fc7d5a93a244401249fcec83e to your computer and use it in GitHub Desktop.
Save amirsoroush/5f232a1fc7d5a93a244401249fcec83e to your computer and use it in GitHub Desktop.
Super() implementation in pure Python
import unittest
from inspect import isfunction, ismethod
def _get_attr(obj, name):
"""Bypass object's normal attribute look up with __getattribute__"""
return object.__getattribute__(obj, name)
class Super:
"""
A simulated `super` class which supports non-zero arguments form.
super(type) -> unbound super object
super(type, obj) -> bound super object; requires isinstance(obj, type)
super(type, type2) -> bound super object; requires issubclass(type2, type)
"""
__slots__ = "__self", "__type"
def __init__(self, type_, obj_or_type=None) -> None:
if not isinstance(type_, type):
raise TypeError("Super() argument 1 must be type.")
if obj_or_type is not None:
if not (
isinstance(obj_or_type, type_)
or (isinstance(obj_or_type, type) and issubclass(obj_or_type, type_))
):
raise TypeError(
"Super(type, obj): obj must be an instance or subtype of type."
)
self.__type = type_
self.__self = obj_or_type
@property
def __thisclass__(self):
return _get_attr(self, f"_{type(self).__name__}__type")
@property
def __self__(self):
return _get_attr(self, f"_{type(self).__name__}__self")
@property
def __self_class__(self):
__self__ = _get_attr(self, "__self__")
if __self__ is None:
return None
if isinstance(__self__, type):
return __self__
__thisclass__ = _get_attr(self, "__thisclass__")
if isinstance(__self__, __thisclass__):
return type(_get_attr(self, "__self__"))
def __get__(self, instance, owner=None):
"""convert unbound super to bound super."""
if _get_attr(self, "__self__") is None and instance is not None:
return Super(_get_attr(self, "__thisclass__"), instance)
return self
def __getattribute__(self, name):
# Route to properties
if name in ("__thisclass__", "__self__", "__self_class__"):
return _get_attr(self, name)
__self__ = _get_attr(self, "__self__")
if __self__ is None:
# This is an unbound Super
return _get_attr(self, name)
# Delegate to __self__
mro = __self__.__mro__ if isinstance(__self__, type) else type(__self__).__mro__
for cls in mro[mro.index(_get_attr(self, "__thisclass__")) + 1 :]:
if name in cls.__dict__:
o = cls.__dict__[name]
if hasattr(o, "__get__") and not isinstance(__self__, type):
return o.__get__(__self__, _get_attr(self, "__thisclass__"))
else:
return o
raise AttributeError(
f"'{type(self).__name__}' object has no attribute '{name}'"
)
def __repr__(self) -> str:
type_ = _get_attr(self, "__thisclass__")
__self__ = _get_attr(self, "__self__")
if __self__:
return "<Super: <class '{}'>, <{} object>>".format(
type_.__name__, type(__self__).__name__
)
return f"<Super: <class '{type_.__name__}'>, NULL>"
# ---------------------------------- Some tests ----------------------------------
class TestInstantiation(unittest.TestCase):
def setUp(self) -> None:
class Person:
pass
self.C = Person
def test_valid_arguments(self):
class Another(self.C):
pass
Super(self.C)
Super(self.C, self.C())
Super(self.C, self.C)
Super(self.C, Another)
Super(self.C, Another())
def test_invalid_arguments(self):
class Another:
pass
class Another2(self.C):
pass
with self.assertRaises(TypeError):
Super(self.C, Another)
with self.assertRaises(TypeError):
Super(Another2, self.C)
with self.assertRaises(TypeError):
Super(self.C())
with self.assertRaises(TypeError):
Super(self.C, Another())
class TestFunctionality(unittest.TestCase):
def test_call(self):
class Parent:
def fn(self):
return 10
class Child(Parent):
def fn(self):
s = Super(Child, self)
return s.fn()
self.assertEqual(Child().fn(), 10)
def test_properties_bound_and_unbound(self):
class B(object):
pass
class C(B):
pass
class D(C):
pass
d = D()
# instance-bound syntax
bsup = super(C, d)
self.assertIs(bsup.__thisclass__, C)
self.assertIs(bsup.__self__, d)
self.assertIs(bsup.__self_class__, D)
# class-bound syntax
Bsup = super(C, D)
self.assertIs(Bsup.__thisclass__, C)
self.assertIs(Bsup.__self__, D)
self.assertIs(Bsup.__self_class__, D)
# unbound syntax
usup = super(C)
self.assertIs(usup.__thisclass__, C)
self.assertIs(usup.__self__, None)
self.assertIs(usup.__self_class__, None)
def test_unbound_lookup(self):
class Parent:
a = 1
class Child(Parent):
sup = Super(Parent)
self.assertIsNone(sup.__self__)
obj = Child()
self.assertIsNotNone(obj.sup.__self__)
with self.assertRaises(AttributeError):
obj.sup.a
def test_unbound_get_value(self):
class B:
a = 1
class C(B):
pass
class D(C):
with self.assertRaises(AttributeError):
Super(C).a
sup = Super(C)
self.assertEqual(D().sup.a, 1)
def test_return_function_or_method(self):
class B1:
def f(self):
pass
class C1(B1):
pass
s1 = Super(C1, C1)
s2 = Super(C1, C1())
self.assertTrue(isfunction(s1.f))
self.assertTrue(ismethod(s2.f))
def test_return_grandparent(self):
class GrandParent:
def fn(self):
return 1
class Parent(GrandParent):
def fn(self):
return 2
class Child(Parent):
def fn(self):
return Super(Parent, self).fn()
self.assertEqual(Child().fn(), 1)
def test_dunder_doc_and_dunder_name(self):
class B:
"This is class B"
class C(B):
pass
self.assertEqual(Super(C, C).__doc__, B.__doc__)
with self.assertRaises(AttributeError):
Super(C, C).__name__
def test_metaclass_attribute(self):
class M(type):
a = 1
class B(metaclass=M):
pass
class C(B):
pass
with self.assertRaises(AttributeError):
super(C, C).a
if __name__ == "__main__":
unittest.main()
@amirsoroush
Copy link
Author

amirsoroush commented Jan 5, 2023

This is a re-implementation of the builtin super class in pure Python(a bit more complete that what Guido showed us in this article.

super is a class which returns a proxy object (not the superclass). Its behavior is kind of less obvious especially for beginners.

This Super(with capital 'S') class lacks the ability to be called without arguments, just like with Python before 3.0 where you had to explicitly specify arguments. The reason for that is in order to successfully call super with zero argument, Python compiler has to do some magic behind the scene and it would get so complicated to implement and eventually diverge from the actual point of this gist.

I tried my best to take all possible scenarios into account so that Super acts exactly like builtin super. There are some tests at the end.

Of-course this is for educational purpose only.

Hope it clarifies some of its secrets.
You're welcome to fix my mistakes or add something to it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment