diff --git a/CHANGES.md b/CHANGES.md index 3890d86..e5bdb48 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## 0.2.2 (unreleased) +- fields with default values are properly passed to __new__()/__init__() - added support for coercion of tuples for Struct-typed fields - added support for `__getitem__` and `__setitem__` - testing a Struct for equality with itself succeeds quickly diff --git a/examples/vector.py b/examples/vector.py index 1dff6e6..6406a43 100644 --- a/examples/vector.py +++ b/examples/vector.py @@ -89,12 +89,13 @@ class MutablePoint(Struct): # # If the fields have default values, these are substituted in before # calling the constructor. Thus providing default parameter values -# in the constructor argument list is meaningless. +# in the constructor argument list is meaningless, as they will always +# be overridden by the defaults from the field's declaration. class DoublingVector2D(Struct): - x = Field - y = Field + x = Field(default=0) + y = Field(default=0) def __new__(cls, x, y): print('Vector2D.__new__() has been called') diff --git a/simplestruct/struct.py b/simplestruct/struct.py index b4b8143..ed0275b 100644 --- a/simplestruct/struct.py +++ b/simplestruct/struct.py @@ -95,6 +95,7 @@ class MetaStruct(type): Upon instantiation of a Struct subtype, set the instance's _initialized attribute to True after __init__() returns. + Preprocess its __new__/__init__() arguments as well. """ # Use OrderedDict to preserve Field declaration order. @@ -144,9 +145,23 @@ def __new__(mcls, clsname, bases, namespace, **kargs): return cls + def get_boundargs(cls, *args, **kargs): + """Return an inspect.BoundArguments object for the application + of this Struct's signature to its arguments. Add missing values + for default fields as keyword arguments. + """ + boundargs = cls._signature.bind(*args, **kargs) + # Include default arguments. + for param in cls._signature.parameters.values(): + if (param.name not in boundargs.arguments and + param.default is not param.empty): + boundargs.arguments[param.name] = param.default + return boundargs + # Mark the class as _initialized after construction. def __call__(cls, *args, **kargs): - inst = super().__call__(*args, **kargs) + boundargs = cls.get_boundargs(*args, **kargs) + inst = super().__call__(*boundargs.args, **boundargs.kwargs) inst._initialized = True return inst @@ -193,12 +208,7 @@ def __new__(cls, *args, **kargs): f = None try: - boundargs = cls._signature.bind(*args, **kargs) - # Include default arguments. - for param in cls._signature.parameters.values(): - if (param.name not in boundargs.arguments and - param.default is not param.empty): - boundargs.arguments[param.name] = param.default + boundargs = cls.get_boundargs(*args, **kargs) for f in cls._struct: setattr(inst, f.name, boundargs.arguments[f.name]) f = None diff --git a/tests/test_struct.py b/tests/test_struct.py index f88f402..5a0a1ce 100644 --- a/tests/test_struct.py +++ b/tests/test_struct.py @@ -133,10 +133,13 @@ class Foo(Struct): class Foo(Struct): a = Field() b = Field(default='b') + # Make sure default field values are passed to __init__() too. + def __init__(self, a, b): + self.c = b f = Foo(1, 2) - self.assertEqual((f.a, f.b), (1, 2)) + self.assertEqual((f.a, f.b, f.c), (1, 2, 2)) f = Foo(1) - self.assertEqual((f.a, f.b), (1, 'b')) + self.assertEqual((f.a, f.b, f.c), (1, 'b', 'b')) # Parentheses-less shorthand. class Foo(Struct):