code
#!/usr/bin/env python3
import functools
import inspect
import unittest
class AutoInitRepr:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
original_init = cls.__init__
@functools.wraps(original_init)
def wrapped_init(self, *init_args, **init_kwargs):
sig = inspect.signature(original_init)
bound = sig.bind_partial(self, *init_args, **init_kwargs)
args_reprs = []
# walk through signature parameters in the order they are defined
for name, param in sig.parameters.items():
if name == "self":
continue
if name in bound.arguments:
# The parameter was explicitly passed by the caller
value = bound.arguments[name]
if param.kind == inspect.Parameter.POSITIONAL_ONLY:
args_reprs.append(repr(value))
elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
# XXX POI-1: Should we 'canonicalize', ie look at
# whether the signature uses a '=' or not, regardless
# of whether the user used a '=' or not?
if name not in init_kwargs:
# passed as arg
args_reprs.append(repr(value))
else:
# passed as kwarg
args_reprs.append(f"{name}={repr(value)}")
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
args_reprs.append(f"{name}={repr(value)}")
elif param.kind == inspect.Parameter.VAR_POSITIONAL:
# **args
# `value` is a tuple of the args
for arg in value:
args_reprs.append(repr(arg))
elif param.kind == inspect.Parameter.VAR_KEYWORD:
# **kwargs
# `value` is a dict of the kwargs
for k, v in value.items():
args_reprs.append(f"{k}={repr(v)}")
else:
# should never happen right?
raise NotImplementedError("Unknown parameter type")
else:
# The parameter was omitted by the caller
if param.kind == inspect.Parameter.POSITIONAL_ONLY:
# Missing positional-only; `__init__` will fail
# I think it's fair to add a default of `!` to
# indicate 'missing' or something, rather than
# refusing to set `_repr_str`, incase an exception
# is taken care of and `_repr_str` is expected,
# even if borked.
args_reprs.append(f"!")
elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
if param.default == inspect.Parameter.empty:
# Missing positional. Same comment as above.
args_reprs.append(f"!")
else:
# Missing kwarg with default.
# XXX POI-2: Should we even add this, as the default was not
# explicitly passed by the user? It makes the repr str
# bloated with defaults that are already set.
args_reprs.append(f"{name}={repr(param.default)}")
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
if param.default == inspect.Parameter.empty:
# Same comment as above. Keyword-only parameter
# without a default is missing, but we should
# fill it anyways incase __init__'s exception is caught.
args_reprs.append(f"{name}=!")
else:
# XXX see POI-2.
args_reprs.append(f"{name}={repr(param.default)}")
self._repr_str = f"{cls.__name__}({', '.join(args_reprs)})"
original_init(self, *init_args, **init_kwargs)
cls.__init__ = wrapped_init
def __repr__(self) -> str:
return getattr(self, '_repr_str', f"{self.__class__.__name__}()")
# Testing
class Foo(AutoInitRepr):
def __init__(self, x, /, y, z=None):
pass
class TestFoo(unittest.TestCase):
# Positional-only `x` passed as positional
## test_1: Positional-or-keyword-no-default `y` passed as positional
def test_1(self):
### Positional-or-keyword-yes-default `z` default to 'None'
self.assertEqual(repr(Foo(6, 7)), "Foo(6, 7, z=None)")
### Positional-or-keyword-yes-default `z` passed as positional
self.assertEqual(repr(Foo(6, 7, 8)), "Foo(6, 7, 8)")
### Positional-or-keyword-yes-default `z` passed as keyword
self.assertEqual(repr(Foo(6, 7, z = 8)), "Foo(6, 7, z=8)")
## test_2: Positional-or-keyword-no-default `y` passed as keyword
def test_2(self):
### Positional-or-keyword-yes-default `z` default to 'None'
self.assertEqual(repr(Foo(6, y = 7)), "Foo(6, y=7, z=None)")
### Positional-or-keyword-yes-default `z` passed as positional
### is not possible: cannot have positional after keyword
### Positional-or-keyword-yes-default `z` passed as keyword
self.assertEqual(repr(Foo(6, y = 7, z = 8)), "Foo(6, y=7, z=8)")
class Bar(AutoInitRepr):
def __init__(self, *args, name, age=40, **kwargs):
pass
class TestBar(unittest.TestCase):
# Keyword-only-no-default `name` passed as keyword
## test_1*: Keyword-only-yes-default `age` default to '40'
def test_1A(self):
### test_1A: No *args
#### No **kwargs
self.assertEqual(repr(Bar(name = "Alice")), "Bar(name='Alice', age=40)")
#### Some **kwargs after `name`
self.assertEqual(repr(Bar(name = "Alice", alive = True)), "Bar(name='Alice', age=40, alive=True)")
#### Some **kwargs before `name`
self.assertEqual(repr(Bar(alive = True, name = "Alice")), "Bar(name='Alice', age=40, alive=True)")
def test_1B(self):
### test_1B: Some *args
#### No **kwargs
self.assertEqual(repr(Bar(1, name = "Alice")), "Bar(1, name='Alice', age=40)")
self.assertEqual(repr(Bar(1, 2, name = "Alice")), "Bar(1, 2, name='Alice', age=40)")
self.assertEqual(repr(Bar(1, 2, 3, name = "Alice")), "Bar(1, 2, 3, name='Alice', age=40)")
#### Some **kwargs after `name`
self.assertEqual(repr(Bar(4, 5, 6, name = "Alice", alive = True)), "Bar(4, 5, 6, name='Alice', age=40, alive=True)")
#### Some **kwargs before `name`
self.assertEqual(repr(Bar(4, 5, 6, alive = True, name = "Alice")), "Bar(4, 5, 6, name='Alice', age=40, alive=True)")
## test_2*: Keyword-only-yes-default `age` passed as keyword
def test_2A(self):
### test_2A: No *args
#### No **kwargs
##### All permutations of ordering for passed:
##### keyword-only-no-default `name` and keyword-only-yes-default `age`
self.assertEqual(repr(Bar(name = "Beatrice", age = 32)), "Bar(name='Beatrice', age=32)")
self.assertEqual(repr(Bar(age = 32, name = "Beatrice")), "Bar(name='Beatrice', age=32)")
#### Some **kwargs
##### All permutations of ordering for passed:
##### keyword-only-no-default `name` and keyword-only-yes-default `age` and kwarg `alive`
self.assertEqual(repr(Bar(name = "Beatrice", age = 32, alive = True)), "Bar(name='Beatrice', age=32, alive=True)")
self.assertEqual(repr(Bar(age = 32, name = "Beatrice", alive = True)), "Bar(name='Beatrice', age=32, alive=True)")
self.assertEqual(repr(Bar(alive = True, age = 32, name = "Beatrice")), "Bar(name='Beatrice', age=32, alive=True)")
self.assertEqual(repr(Bar(age = 32, alive = True, name = "Beatrice")), "Bar(name='Beatrice', age=32, alive=True)")
self.assertEqual(repr(Bar(name = "Beatrice", alive = True, age = 32)), "Bar(name='Beatrice', age=32, alive=True)")
self.assertEqual(repr(Bar(alive = True, name = "Beatrice", age = 32)), "Bar(name='Beatrice', age=32, alive=True)")
def test_2B(self):
### test_2B: Some *args
### XXX bored at this point; tests are probably not exhausted here
self.assertEqual(repr(Bar(4, name = "Beatrice", okay = "yes")), "Bar(4, name='Beatrice', age=40, okay='yes')")
class Baz(AutoInitRepr):
# Tried to be exotic with Baz's signature
def __init__(self, v, *args, lorem, dolor="sit", **kwargs):
pass
class TestBaz(unittest.TestCase):
# Keyword-only-no-default `lorem` passed as keyword
def test_all(self):
# Positional-or-keyword-no-default `v` passed as positional
self.assertEqual(repr(Baz("book", 7, 8, 9, lorem="ipsum")), "Baz('book', 7, 8, 9, lorem='ipsum', dolor='sit')")
# Positional-or-keyword-no-default `v` passed as keyword before empty *args before `lorem`
self.assertEqual(repr(Baz(v="book", lorem="ipsum")), "Baz(v='book', lorem='ipsum', dolor='sit')")
# Positional-or-keyword-no-default `v` passed as keyword after `lorem` after empty *args
self.assertEqual(repr(Baz(lorem="ipsum", v="book")), "Baz(v='book', lorem='ipsum', dolor='sit')")
# Very back to front order here: in the signature
# `eof` in kwargs, `dolor` has default, `lorem` no default, `v` before *args
# but repr string comes out fine
self.assertEqual(repr(Baz(eof=True, dolor="$", lorem="ipsum", v="book")), "Baz(v='book', lorem='ipsum', dolor='$', eof=True)")
if __name__ == "__main__":
unittest.main()
The only main downside therefore is if one changes member variables that were assigned to in __init__:
PS: No vibecoding this time (my previous reply, which I self-tagged as Gemini assisted, was removed; first and only time vibecoding here
). The new version is worth it too with the human-hand-written attention to detail and tests. @davidism Thanks.