-
-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: strategy for using class methods #405
Conversation
f8aed12
to
57c81c5
Compare
Pretty cool, thanks! I'll take a look in the next few days. |
57c81c5
to
69a3ed9
Compare
My local black is configured for a line length of 100 characters. Any reason why you don't use pre-commit to check potential commits locally? |
I'm not a big fan of precommit, the dummy configuration here is I think inherited from the parent organization. You can use How would you feel if we made it so that if the given function takes two arguments, we also make the strategy pass in the converter too? I'm thinking it might be convenient and easy to support. @define
class MyClass:
a: int
b: Nested
@classmethod
def _structure(cls, data: dict, converter):
return cls(data["b"] + 1, converter.structure(data['b'], Nested))
def _unstructure(self):
return {"c": self.a - 1} # unstructuring as "c", not "a" |
Makes a lot of sense. I didn't think of nested structures. |
Just to make sure we are one the same page: That would mean changing stuff in How would you handle the distinction between functions accepting and not accepting a converter argument? |
Yeah, I figured we'd use def use_class_methods(
converter: BaseConverter,
structure_method_name: Optional[str] = None,
unstructure_method_name: Optional[str] = None,
) -> None:
if structure_method_name:
def make_class_method_structure(cl: Type) -> Callable:
fn = getattr(cl, structure_method_name)
sig = signature(fn)
if len(sig.parameters) == 1:
return lambda v, _: fn(v)
if len(sig.parameters) == 2:
return lambda v, _: fn(v, converter)
raise TypeError("Provide a class method with one or two arguments.")
converter.register_structure_hook_factory(
lambda t: hasattr(t, structure_method_name), make_class_method_structure
)
if unstructure_method_name:
converter.register_unstructure_hook_func(
lambda t: hasattr(t, unstructure_method_name),
lambda v: getattr(v, unstructure_method_name)(),
) I've switched to |
How about this? No def use_class_methods(
converter: BaseConverter,
structure_method_name: Optional[str] = None,
unstructure_method_name: Optional[str] = None,
) -> None:
def call_wrapper(n, f):
n_parameters = len(signature(f).parameters)
if n_parameters == n:
return f
if n_parameters == n + 1:
return lambda *v: f(*v, converter)
raise TypeError("Provide a class method with one or two arguments.")
if structure_method_name:
converter.register_structure_hook_func(
lambda t: hasattr(t, structure_method_name),
lambda v, t: call_wrapper(1, getattr(t, structure_method_name))(v),
)
if unstructure_method_name:
converter.register_unstructure_hook_func(
lambda t: hasattr(t, unstructure_method_name),
lambda v: call_wrapper(0, getattr(v, unstructure_method_name))(),
) P.S.: The error text has to be adapted for the unstructure case. |
That'll work, but the issue there is that it calls |
Sorry, misclicked Close |
69a3ed9
to
75d8185
Compare
You're right. I used your approach and added tests and docs. See #405 |
Looks great! I might tinker with the docs after you fix the tests, but solid work. |
062634a
to
997c648
Compare
When writing the @define
class Nested:
a: Union["Nested", None]
c: int
@classmethod
def _structure(cls, data, conv):
b = data["b"]
return cls(None if b is None else conv.structure(b, cls), data["c"])
# Would something like this be possible?
return cls(conv.structure_partial(b, Nested.a), data["c"]) # or structure_partial(b, Nested, "a") |
You can do that today by just using structure: from typing import Union
from attrs import define, fields, resolve_types
from cattrs import Converter
from cattrs.strategies import use_class_methods
@define
class Nested:
a: "Union[Nested, None]"
c: int
@classmethod
def _structure(cls, data, conv):
b = data["b"]
# Would something like this be possible?
return cls(conv.structure(b, fields(Nested).a.type), data["c"])
resolve_types(Nested)
c = Converter()
use_class_methods(c, "_structure")
print(c.structure({"b": {"b": None, "c": 1}, "c": 1}, Nested)) The biggest issue is how Python handles stringified type annotations, which are necessary for self-referencing types, hence the call to |
997c648
to
712759a
Compare
Awesome! I changed the test to use your version. And I hopefully fixed all remaining issues with Python < 3.10. |
I don't understand the CI issues. Judging by the logs, the installation process just stalls. Just a hiccup on the CI runner? |
They look fine to me. On this line: /~https://github.com/python-attrs/cattrs/pull/405/files#diff-40771e4a7b976ef4fbc939a5524111d62406bf9162a342dff6ac335c33f33948R38 you need to use |
712759a
to
5499f08
Compare
Strange. For me, the logs just stopped. Anyway, |
Looks like /~https://github.com/python-attrs/cattrs/pull/405/files#diff-40771e4a7b976ef4fbc939a5524111d62406bf9162a342dff6ac335c33f33948R13 needs to be switched over to the old Union syntax too, for older Pythons. |
5499f08
to
9c0619a
Compare
Cool, thanks! |
Glad I could contribute something. |
Here's a new PR for #394.