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

Preserve __slots__ metadata on Undefined types #2026

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

nitzmahone
Copy link

@nitzmahone nitzmahone commented Oct 1, 2024

Restores erroneous deletions of __slots__ metadata from Undefined types that breaks various serialization/copy mechanisms on Python >= 3.6. Also:

  • fix ChainedUndefined to raise AttributeError on unimplemented dunder methods to prevent breaking core Python protocols (including copy)
  • add tests

fixes #2025

* fix copy/deepcopy/pickle of Undefined objects on Python >= 3.6
* fix ChainedUndefined to raise AttributeError on unimplemented dunder methods to prevent breaking core Python protocols (including copy)
* add tests
@@ -982,10 +982,15 @@ class ChainableUndefined(Undefined):
def __html__(self) -> str:
return str(self)

def __getattr__(self, _: str) -> "ChainableUndefined":
def __getattr__(self, name: str) -> "ChainableUndefined":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this change. Can you make the comment clearer?

Copy link
Author

@nitzmahone nitzmahone Oct 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same (incomplete, BTW) heuristic exclusion already used by Undefined.__getattr__. ChainableUndefined needs it for the same reason, but there weren't previously any tests that exhibited the bugs caused by the current unconditional behavior.

There are a number of Python interaction protocols that sniff for the presence of a particular dunder method to support builtin behavior. This is one of the places where Jinja's getattr/getitem equivalency causes problems. In this case, copy uses the presence of a __setstate__ method on the type to decide how it will create and populate the copy. Since hasattr(ChainableUndefined, '__setstate__') is true with the current impl, Python assumes it can call that to populate the empty storage for the copy, but blows up when it actually tries to invoke the Undefined object it receives instead of a bound method to fill in an empty object instance.

In general, __getattr__ should raise AttibuteError on a request for any dunder method it doesn't know about- there are all sorts of weird things that can happen in various places in Python if an object confuses the runtime about its support for a particular interaction protocol.

Happy to include a more detailed inline explanation along those lines in the code. but should probably either copy/paste to the original usage or actually share that logic between them.

Also happy to correct the heuristic in both places to only exclude __XYZ__ instead of the current __.* - while probably unlikely, the current impl would incorrectly exclude, eg __fooattr.

return self

__getitem__ = __getattr__ # type: ignore
def __getitem__(self, _: str) -> "ChainableUndefined": # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use _ as an argument name. Use specific ignores, not a bare ignore.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, that's what PyCharm coughed up for the override signature- I assumed the base class did the same, but apparently it was just confused by the dynamic _fail_with_undefined_error aliasing.

The base class is already doing the same blanket type: ignore on that method due to the aforementioned dynamic aliasing- I detest overly-broad ignores as well, so happy to figure out a more targeted ignore (and of course use a proper arg name).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the ignore, run mypy, add an ignore for the actual finding. Yeah, there's still a bunch of bare ignores from when we first added typing, but I don't want to persist that going forward.

* tighten up dunder method exclusion heuristic and clarify raison d'etre in Undefined/ChainableUndefined.__getattr__
* use a more granular type ignore for incorrect base class declaration on ChainableUndefined.__getattr__
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

Successfully merging this pull request may close these issues.

Undefined objects can't be copied or pickled by Python > 3.5
2 participants