Skip to content
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

Question: computed fields #573

Open
sgstq opened this issue Oct 22, 2023 · 7 comments
Open

Question: computed fields #573

sgstq opened this issue Oct 22, 2023 · 7 comments

Comments

@sgstq
Copy link

sgstq commented Oct 22, 2023

Description

Sorry if I missed this in the docs or other issues but is there a more decent way to create a computed field based on the value of another field/s?
Currently my implementation looks a bit clumsy:

class MyModel(msgspec.Struct):
    field1: int
    field2: int
    fieldSum: int = 0

    def __post_init__(self):
        self.fieldSum = self.field1 + self.field2

in dataclass fields we have init=False option but not in msgstruct.field

@FHU-yezi
Copy link

FHU-yezi commented Oct 22, 2023

Maybe we can just use @Property decorator? Like this:

class MyModel(msgspec.Struct):
    field1: int
    field2: int

    @property
    def fieldSum(self) -> int:
        return self.field1 + self.field2

But it is caculated when we access it.

@sgstq
Copy link
Author

sgstq commented Oct 23, 2023

The goal is to have this field in encoded json, and in the result of to_buitins. I believe this will not work for decorated property.

@ml31415
Copy link

ml31415 commented Nov 4, 2023

I guess the cleanest way to do that, would be to hand that computed field on instantiating the class. Seems a bit out of scope.

@illeatmyhat
Copy link

illeatmyhat commented Nov 30, 2023

class MyModel(msgspec.Struct):
field1: int
field2: int
fieldSum: int = 0

For now, try:

 fieldSum: int | UnsetType = UNSET

UNSET is a special case that specifically means the value was not provided in the source data.
It's not exactly what you're looking for, but it's the closest approximation.

Maybe we can just use @Property decorator? Like this:

class MyModel(msgspec.Struct):
    field1: int
    field2: int

    @property
    def fieldSum(self) -> int:
        return self.field1 + self.field2

But it is caculated when we access it.

Until #199 is done, you could use functools.cache for really complex properties that aren't emitted from to_builtins().
Otherwise performance should be measured before making a real decision.

@jcrist
Copy link
Owner

jcrist commented Jan 5, 2024

Apologies for the delay here

Sorry if I missed this in the docs or other issues but is there a more decent way to create a computed field based on the value of another field/s?

By this do you mean "computed fields" that will be part of the encoded representation, but aren't/can't-be used for decoding? Something like:

class Ex(Struct):
    a: int

    @msgspec.computed_field
    def b(self):
        return self.a + 1

obj = Ex(1)

print(f"b = {obj.b}")
#> b = 2

msg = msgspec.json.encode(obj)
print(msg)
#> b'{"a":1,"b":2}'

obj2 = msgspec.json.decode(b'{"a":1}',` type=Ex)  # b is not needed (or used) for decoding
assert obj == obj2

This functionality doesn't currently exist in msgspec, but is something we could support. Open questions:

  • What should this feature be called? "Computed fields"? "Encoded properties"? What should the decorator name be?
  • If extra fields are forbidden when decoding (forbid_unknown_fields=True), what happens when decoding a message containing a computed field?
class Test(msgspec.Struct, forbid_unknown_fields=True):
    a: int
    @msgspec.computed_field
    def b(self):
        return self.a + 1

msg = b'{"a": 1, "b": 2}'

msgspec.json.decode(msg, type=Test)  # does this error since `b` isn't a true field?

@sgstq
Copy link
Author

sgstq commented Jan 9, 2024

@jcrist, thank you for your reply.
Yes, the snippet you provided is exactly what I'd like to have.

  • What should this feature be called? "Computed fields"? "Encoded properties"? What should the decorator name be?

Naming isn't my strongest suit :), but I think the computed_ prefix better explains the nature of the property. So, computed_field (to be recognizable for those who came from other frameworks), or computed_property / derived_property seem clear.

  • If extra fields are forbidden when decoding (forbid_unknown_fields=True), what happens when decoding a message containing a computed field?

It definitely should raise an error for the sake of specification consistency.

Another question is how to behave when forbid_unknown_fields=False and the property is provided:

  • ignore and set the computed value (I think this is more expected by devs)
  • raise an error
  • accept the provided value

@illeatmyhat
Copy link

It should be noted that there are use cases for computed fields that are encoded (above), and computed fields that aren't (recursive or cyclic data structures like OpenAPI)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants