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

How to do equality comparision of objects when properties are lazily defined #43

Closed
devashishshankar opened this issue Oct 22, 2019 · 9 comments

Comments

@devashishshankar
Copy link

I'm facing an issue while using pyfields, it has to do with the fact that properties are lazily initialized. Some of my tests require doing an equality comparison of two objects.
Let's say I have the following class:

class A:
     field1: str = field(check_type=True, default=None)
     
    def __eq__(self, other):
        return type(self) is type(other) and self.__dict__ == other.__dict__

Now,

a1 = A()
a1.field1

a2 = A()

assert a1 == a2

Here, a1 !=a2 since calling a1.field would set a _field variable inside the instance.

This is not exactly a bug, but can you think of any clean way of handling this with pyfields?

@devashishshankar
Copy link
Author

Ok, so going through documentation, one solution for me would be to use init_fields (Can't use make_init since I have a custom constructor - my main reason I preferred this lib over attrs)

However, the problem is that in my usecase these fields really are lazily initialised and may not really exist in the object. I have a bunch of enrichers which add these fields as the object passes through a pipeline. Until then I really want them to be None or Empty.

init_fields with default=None also doesn't work (Since it does validation)

I realize that this may be a very custom usecase - so I need a proper eq maybe?

@devashishshankar
Copy link
Author

devashishshankar commented Oct 22, 2019

Ah, I guess defaulting to None was the problem :) Not using that solves my problem.

Sorry for creating unnecessary issue.

One reason why I was defaulting to None. My class is something like:

class A:
     field1: int = field(check_type=True, default=None)
     
    def __eq__(self, other):
        return type(self) is type(other) and self.__dict__ == other.__dict__
    
    # derived field
    @property
    def derived_field(self):
          return 50-self.field1 if self.field1 else None

I guess now I have to do a try except MandatoryFieldInitError if someone accesses the derived field? They can't simply do a None check?

Btw, One clean way which would solve my issue is adding a nullable param in the field definition. Does that make sense? If it does I can open an issue for the same and happy to contribute a PR for the same (time permitting)

@smarie
Copy link
Owner

smarie commented Oct 23, 2019

Thanks @devashishshankar for digging on this !

Let's backtrack a little bit on your initial need, not on the workarounds that you have found so far. If I understand your problem correctly,

  • you wish to have a base field field1 that is optional (users do not have to provide a value)

  • your initial need was to be able to compare objects. For this you would need the same kind of feature than attrs on top of the fields. I will soon update autoclass so that it can do this, you will be able to use either @autodict or @autoclass on your class for this. Stay tuned: Support pyfields as an alternate way to define attributes python-autoclass#28

  • you wish to create a derived property on top of the field. If the field is optional it works out of the box: you will get None in the field value

So based on the above, your need should be solved by @autodict / @autoclass, with a "more intelligent" __eq__.

Except that you seem to be talking about validation. Here comes the nullable need. If you use python 3.5+ the best solution for now is to declare the type of your field as Optional[int]. But it could make sense to have an explicit nullable argument in field though, I'll open a dedicated issue.

@smarie smarie mentioned this issue Oct 23, 2019
@devashishshankar
Copy link
Author

devashishshankar commented Oct 23, 2019

Thank you! Optional works perfectly for me. For me nullable and Optional mean the same thing, I don't see why nullable would be useful if typing already has support for Optional

Autoclass and Autodict look great! Will definitely use it in future. In this case, though, pyfields works better for me since I am inheriting from a superclass (which is in a common lib I don't want to change right now). Don't know if autoclass is compatible with inheritance.

@smarie
Copy link
Owner

smarie commented Oct 23, 2019

For me nullable and Optional mean the same thing

indeed they do, that's why many of us asked for a rename (Nonable was my actual favorite, see whole discussion).

The open issue #44 is mostly for compatibility with versions of python that do not have typing or do not wish to use it, and also to make the value validators (another feature of pyfields) skip validation in case None is passed.

Concerning autoclass (or its subtasks autodict, autohash, etc.), it sources its attribute list today from the constructor signature. Tomorrow I will have it detect pyfields and therefore source the list from the list of fields. It shoud support inheritance, yes.

Shall we close this ticket then ?

@devashishshankar
Copy link
Author

indeed they do, that's why many of us asked for a rename (Nonable was my actual favorite, see whole discussion).

Yeah, correct. Optional can refer to fields with default values (which need not be passed while constructing the object). I guess here I've made my fields both Nonable and Optional (by setting default to None) - which serves my case quite well. I don't want to differentiate between field not being set, and and field set to None, so I set them to None by default. A side effect of this is also a simpler __eq__

The open issue #44 is mostly for compatibility with versions of python that do not have typing or do not wish to use it, and also to make the value validators (another feature of pyfields) skip validation in case None is passed.

Okay, I guess typing has been backported to python2? So a user can pass Optional[int] as a type_hint? But nullable makes sense if they don't want to install/use typing.

Concerning autoclass (or its subtasks autodict, autohash, etc.), it sources its attribute list today from the constructor signature. Tomorrow I will have it detect pyfields and therefore source the list from the list of fields. It shoud support inheritance, yes.

Ah okay. How will it be different from just using pyfield's init_fields() or make_init()? It would provide additional things like autodict and autohash? pyfields looks very similar to autoprops to me except that fields are not sourced from the constructor.

Shall we close this ticket then ?

Yes, closing it.

@smarie
Copy link
Owner

smarie commented Oct 24, 2019

Ah okay. How will it be different from just using pyfield's init_fields() or make_init()? It would provide additional things like autodict and autohash?

Exactly. Using @autoclass on a class using pyfields will perform @autodict and @autohash only (and these two will be modified so that they automatically source the attribute list from fields)

pyfields looks very similar to autoprops to me except that fields are not sourced from the constructor.

Indeed ! You can consider pyfields as a much more efficient version of autoprops. When I wrote autoclass 3 years ago I was not as expert in python as I am now, so I found that this was "good enough". With the brand new pyfields I was able to greatly improve both style/compactness and performance.

Thanks again for all the feedback ! I'll work on fixing all this today and tomorrow

@smarie
Copy link
Owner

smarie commented Oct 25, 2019

For information: version 0.13.0 brings nonable feature. I hope that will do the trick for you. (if not feel free to open another ticket !)

@devashishshankar
Copy link
Author

Great! Like that it infers it is nonable if default=None

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

2 participants