-
Notifications
You must be signed in to change notification settings - Fork 158
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
Warn about buggy type resolution #529
Warn about buggy type resolution #529
Conversation
This type annotation resolution is a flawed implementation -- ideally a proper fix can come later, but for now it's worth warning about the present behavior. `dataclasses-json` does not behave correctly when using string-style annotations (a frequent tactic for annotating types that have not yet been defined at that point in the file). The core problem comes from the assumption that type annotations in a module will match `sys.modules()` -- `maybe_resolved` will frequently resolve to a different value than a typechecker would! A short example of the error ============================ `example_types.py`: ```python from dataclasses import dataclass from dataclasses_json import dataclass_json @dataclass_json @DataClass class Config: options: list["Option"] @dataclass_json @DataClass class Option: label: str ``` Expected behavior ----------------- `_decode_items` correctly identifies `Option` as coming from `example_types`: ```python >>> from example_types import Config >>> print(Config.from_dict({"options": [{"label": "repro"}]}).options) [Option(label='repro')] ``` Unexpected behavior ------------------- `_decode_items()` incorrectly identifies `"Option"` as from a third party module that is not in scope at time of annotation: `repro.py`: ```python import click.parser # Has `Option` from example_types import Config print(Config.from_dict({"options": [{"label": "repro"}]}).options) ``` ```bash $ python3.10 repro.py [{'label': 'repro'}] ``` Related fix -- truthiness ========================= The conditional `if maybe_resolved` is meant to handle the case of an attribute not being found in the module (it could basically be written as `if maybe_resolved is not None`). However, that doesn't cover the case of types that are falsy at runtime! ```python CustomNoneType = None values: list['CustomNoneType'] = [None, None] ``` We can just use `hasattr()` instead.
I expect that #442 may well address this issue with a refactor (which is why I didn't make any effort to change behavior here). However, I at least wanted to document this issue & hopefully help others who stumble on it! |
For anybody else reading this (and using this package on Python 3.7 through 3.10) a good workaround is to use +from __future__ import annotations
@dataclass_json
@dataclass
class Config:
- options: list["Option"]
+ options: list[Option] |
@lidatong - do you have any thoughts about this? I'm hoping a warning can save some others a lot of time & confusion -- took me a little while to realize that this package resolves names to the first module that happens to have a variable of that name. |
@DavidCain is it possible for you to add some units proving the fix? Also note that after 3.11 this is not super relevant since 'self' types are resolved there w/o string hacks |
No you should not use that import with DCJ as it breaks the type handling in a lot of cases. Please review documentation :) |
@george-zubrienko - I can certainly add unit tests. Just for clarity though, are you suggesting that I:
Yup, agreed that Python 3.11 helps! That's why I recommended Though note that the issue demonstrated here is not annotating the type of the class itself, but instead annotating a class that comes later in the file and hasn't been interpreted yet at time of declaration. |
@george-zubrienko , I assume you're referring to the line here -- https://github.com/lidatong/dataclasses-json?tab=readme-ov-file#handle-recursive-dataclasses I understand that the In other words, using |
You misunderstand the concept of future imports. Those are things that might not even make it into next release. Using those is always at your own risk and no, no stable and reliable code should rely or support future imports. This is especially important when you are working with type handling which in Python is screwed to the rest of the language with rusty nails |
Easy choice that works for easy cases. Metaclasses, apps that rely on pickling such as pyspark will have issues with that specific import, not to mention that by adding it you implicitly add a necessity to verify all your other imports behaviours as they might be affected as well. |
It is a tough one to fix pre-3.11. You'll have play the string matching game which is tough, I fixed that for one of the cases some months ago, but I feel like its just chasing the mice without addressing the root issue. Unfortunately we have to wait for 3.10 EOL, so a warning should be fine, I'd just like to see smth that covers the code you are adding |
Fair enough! I know that any future import may ultimately never be added to the language ("MandatoryRelease may also be None, meaning that a planned feature got dropped or that it is not yet decided.") https://docs.python.org/3/library/__future__.html What I meant to say is that all current possible imports in >>> from __future__ import annotations
>>> annotations.getMandatoryRelease()
(3, 11, 0, 'alpha', 0)
Disagree --
Hah no disagreement there, but again really can't stress enough how much "pick the first module that happens to have a variable of this name" is also screwed. |
Great, I'd be happy to add some test coverage for what I've done here! If I have the time for a proper fix, I can follow up with a proposal. |
We have a running joke in the office that all Python development is hope-driven ;) |
This demonstrates that the core functionality still works, we just add some basic warnings to help end users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome! Let's get this merged :)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [dataclasses-json](https://togithub.com/lidatong/dataclasses-json) ([changelog](https://togithub.com/lidatong/dataclasses-json/releases)) | `0.6.6` -> `0.6.7` | [![age](https://developer.mend.io/api/mc/badges/age/pypi/dataclasses-json/0.6.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/pypi/dataclasses-json/0.6.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/pypi/dataclasses-json/0.6.6/0.6.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/pypi/dataclasses-json/0.6.6/0.6.7?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes <details> <summary>lidatong/dataclasses-json (dataclasses-json)</summary> ### [`v0.6.7`](https://togithub.com/lidatong/dataclasses-json/releases/tag/v0.6.7) [Compare Source](https://togithub.com/lidatong/dataclasses-json/compare/v0.6.6...v0.6.7) #### What's Changed - feat: support abstract collections by [@​PJCampi](https://togithub.com/PJCampi) in [https://github.com/lidatong/dataclasses-json/pull/532](https://togithub.com/lidatong/dataclasses-json/pull/532) - Warn about buggy type resolution by [@​DavidCain](https://togithub.com/DavidCain) in [https://github.com/lidatong/dataclasses-json/pull/529](https://togithub.com/lidatong/dataclasses-json/pull/529) #### New Contributors - [@​DavidCain](https://togithub.com/DavidCain) made their first contribution in [https://github.com/lidatong/dataclasses-json/pull/529](https://togithub.com/lidatong/dataclasses-json/pull/529) **Full Changelog**: lidatong/dataclasses-json@v0.6.6...v0.6.7 </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/ixm-one/pytest-cmake-presets). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4zOTMuMCIsInVwZGF0ZWRJblZlciI6IjM3LjM5My4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZTpkZXBlbmRlbmNpZXMiXX0=--> Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
This type annotation resolution is a flawed implementation -- ideally a
proper fix can come later, but for now it's worth warning about the
present behavior.
dataclasses-json
does not behave correctly when using string-styleannotations (a frequent tactic for annotating types that have not yet
been defined at that point in the file).
The core problem comes from the assumption that type annotations in a
module will match
sys.modules()
--maybe_resolved
will frequentlyresolve to a different value than a typechecker would!
A short example of the error
example_types.py
:Expected behavior
_decode_items
correctly identifiesOption
as coming fromexample_types
:Unexpected behavior
_decode_items()
incorrectly identifies"Option"
as from a thirdparty module that is not in scope at time of annotation:
repro.py
:Related fix -- truthiness
The conditional
if maybe_resolved
is meant to handle the case of anattribute not being found in the module (it could basically be written
as
if maybe_resolved is not None
). However, that doesn't cover thecase of types that are falsy at runtime!
We can just use
hasattr()
instead.