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

Add support for Annotated types #219

Merged
merged 7 commits into from
Jan 31, 2024

Conversation

ljnsn
Copy link
Contributor

@ljnsn ljnsn commented Jun 20, 2023

Add support for Annotated types.

This is a relatively simple and straightforward implementation with minimal changes and seems to work fine.

Resolves #217

@ljnsn ljnsn changed the title Add supported for Annotated types (#217) Add supported for Annotated types Jun 20, 2023
injector_test.py Outdated
Comment on lines 1696 to 1724
def test_annotated_instance_integration_works():
UserID = Annotated[int, "user_id"]

def configure(binder):
binder.bind(UserID, to=123)

injector = Injector([configure])
assert injector.get(UserID) == 123


def test_annotated_class_integration():
class Shape(abc.ABC):
pass

class Circle(Shape):
pass

first = Annotated[Shape, "first"]

def configure(binder):
binder.bind(first, to=Circle)

injector = Injector([configure])
assert isinstance(injector.get(first), Circle)
Copy link
Collaborator

@davidparsson davidparsson Jun 21, 2023

Choose a reason for hiding this comment

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

Thanks for your contribution!

I'd like to see another test or two, with multiple bindings to see that Injector can keep them apart. For example:

  • Making sure that Annotated[int, "first"] and Annotated[int, "second"] get separate bindings/values
  • Making sure that Annotated[int, "user_id"] and int get separate bindings/values

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback! Makes sense, I will add some more tests and take a look at the failing ones.

@davidparsson
Copy link
Collaborator

There are a few failing tests as well, with different problems for different versions.

@ljnsn
Copy link
Contributor Author

ljnsn commented Jun 26, 2023

@davidparsson have you had time to take a look at the changes? 🙂

@ljnsn
Copy link
Contributor Author

ljnsn commented Jun 28, 2023

Sorry about that.. I've now tested with 3.7-3.11 locally and everything passes. There are now less checks to recognise Annotated in _punch_through_alias, but I think it should be fine (actually, the isinstance check is probably sufficient).

I think the mypy error is due to a newer version of mypy being used? There's no version restriction on it and I get it also on the main branch that does not have my changes. Should I ignore it?

@ljnsn ljnsn changed the title Add supported for Annotated types Add support for Annotated types Jun 28, 2023
@jstasiak
Copy link
Collaborator

That's an interesting feature, I've been wondering about some kind of Annotated support for a while.

I'm wondering if we're not missing some edge cases with this approach that will become obvious later, I'll have another look at this PR when I have a moment

@codecov-commenter
Copy link

codecov-commenter commented Jun 29, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (44ac4e0) 95.20% compared to head (a440d70) 95.22%.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #219      +/-   ##
==========================================
+ Coverage   95.20%   95.22%   +0.01%     
==========================================
  Files           1        1              
  Lines         563      565       +2     
  Branches       95       96       +1     
==========================================
+ Hits          536      538       +2     
  Misses         20       20              
  Partials        7        7              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@ljnsn
Copy link
Contributor Author

ljnsn commented Jul 14, 2023

Have you had time to take a look yet @jstasiak ? I would like to start using this 🙂

@tobni
Copy link
Contributor

tobni commented Jul 20, 2023

This depends on this being true:

assert Annotated[int, "first"] == Annotated[int, "first"]

which I didn't know if it were or not, so I checked, and I think this holds due to _AnnotatedAlias implementing __eq__ quite intuitively (essentially does Annotated[A, B] == Annotated[C, D] if A == C and B == D).

I have checked this to be the case in python 3.10. I have also checked that the impl is order sensitive, e.g

assert Annotated[int, "first", "second"] != Annotated[int, "second", "first"]

holds.

A funny corner case due to the equality requirement is that providing Annotated[int, float("nan")] is impossible, just as Annotated[int, object()] will be.

@ljnsn
Copy link
Contributor Author

ljnsn commented Jul 24, 2023

@tobni thanks for chiming in on this.

A funny corner case due to the equality requirement is that providing Annotated[int, float("nan")] is impossible, just as Annotated[int, object()] will be.

Not sure what you mean by this, I just added a test for both of your examples and both seem to work fine.

Note that

x = float("nan")
x == x  # False

y = Annotated[int, float("nan")]
y == y  # True

@tobni
Copy link
Contributor

tobni commented Jul 24, 2023

Ah I was thinking of

Annotated[int, object()] == Annotated[int, object()] # False

Which is not an impossible (but unlikely!) scenario. In essence using bare annotations without "aliasing", and not having the metadatas __eq__ behave as intended.

But this is immaterial, the equality impl of _AnnotatedAlias is more than fine for this use case, sorry for the derail!

@ljnsn
Copy link
Contributor Author

ljnsn commented Jul 24, 2023

Ok, I see what you mean now, when passing in an annotated type with another instance in the metadata it indeed breaks down. I'd argue that's expected behaviour though, since it's clear that what you're looking up from the injector is not what you bound. It would be better to throw an error in that case though, because currently

foo = Annotated[int, object()]

def configure(binder):
    binder.bind(foo, to=123)

injector = Injector([configure])
injector.get(foo)  # returns 123
injector.get(Annotated[int, object()])  # returns 0

Not clear to my why it's returning 0 in the second case, I'll check that.

@tobni
Copy link
Contributor

tobni commented Jul 24, 2023

Probably because Annotated[int, object()]() == 0 # note the ending paren, I think that is expected behaviour? But also controversial to construct an integer with the default constructor, see #116

@jstasiak
Copy link
Collaborator

jstasiak commented Sep 5, 2023

Very nice test coverage, I'm convinced this would be good to merge but I need to think of a thing or two first – stay tuned and thanks for your patience.

@ricardo-passthrough
Copy link

is this PR supposed to also add support for @inject? doesn't look like this case works:

Foo = Annotated[int, object()]

def configure(binder):
    binder.bind(Foo, to=123)

@inject
@dataclass
class MyClass:
    foo: Foo

injector = Injector([configure])
instance = injector.get(MyClass)
assert instance.foo == 123

It looks like @inject strips out the metadata from the type and bind to the underlying type instead

image

@jstasiak jstasiak merged commit 78bb621 into python-injector:master Jan 31, 2024
8 checks passed
@petrkalos
Copy link

+1 to what @ricardo-passthrough mentioned, this PR doesn't work with @inject

@jstasiak
Copy link
Collaborator

jstasiak commented Jul 7, 2024

The implementation so far has just been released in version 0.22.0.

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.

Binding of Annotated types does not work as one would expect
7 participants