attrs
attrs copied to clipboard
Use `TypeGuard` for `has` in Python 3.10 and above
Summary
Conditionally defines has using TypeGuard in Python 3.10+ in the stub file.
xref: https://github.com/python-attrs/attrs/issues/987
Pull Request Check List
- [x] Added tests for changed code. Our CI fails if coverage is not 100%.
- [ ] New features have been added to our Hypothesis testing strategy.
- [ ] Changes or additions to public APIs are reflected in our type stubs (files ending in
.pyi).- [ ] ...and used in the stub test file
tests/typing_example.py. - [ ] If they've been added to
attr/__init__.pyi, they've also been re-imported inattrs/__init__.pyi.
- [ ] ...and used in the stub test file
- [ ] Updated documentation for changed code.
- [ ] New functions/classes have to be added to
docs/api.rstby hand. - [ ] Changes to the signature of
@attr.s()have to be added by hand too. - [ ] Changed/added classes/methods/functions have appropriate
versionadded,versionchanged, ordeprecateddirectives. Find the appropriate next version in our__init__.pyfile.
- [ ] New functions/classes have to be added to
- [ ] Documentation in
.rstfiles is written using semantic newlines. - [ ] Changes (and possible deprecations) have news fragments in
changelog.d. - [x] Consider granting push permissions to the PR branch, so maintainers can fix minor issues themselves without pestering you.
I don't see a direct use-case for this (since attrs classes should be statically known now) but I don't see the harm either.
The class might not be statically known to be an attrs class (#987, #996) or it can appear in a union with other types.
So what exactly does this do?
It essentially makes has work at compile time too.
Let's say your function takes a class (not an instance, but an actual class). You will type your argument as type (or type[C]). You can check if it's an attrs class with has, and if that's true you can pass it to a different function that takes type[AttrsInstance]. But Mypy will still yell at you, since it doesn't know that has means it's an attrs class.
If we annotate has with TypeGuard, Mypy will know.
Code example:
def needs_attrs_class(cls: type[AttrsInstance]) -> None:
pass
def my_function(cls: type) -> None:
if has(cls):
needs_attrs_class(cls) # Mypy's gonna be unhappy, unless it knows `has` narrows the type of is argument
It tells the type checker that if the condition is true then cls is of type type[AttrsInstance]. Simplified example:
def is_dict_no_typeguard(value: object) -> bool:
return isinstance(value, dict)
def is_dict_typeguard(value: object) -> TypeGuard[dict]:
return isinstance(value, dict)
might_be_a_dict = ...
if is_dict_no_typeguard(might_be_a_dict):
"type of might_be_a_dict is object"
if is_dict_typeguard(might_be_a_dict):
"type of might_be_a_dict is dict"
Sounds cool! Any reason against it? If not: make it happen Tin. ;)
Would you mind coming up with a way to verify this works in https://github.com/python-attrs/attrs/blob/main/tests/typing_example.py ?
There's a test in test_mypy.yml. Should I move it over to typing_example.py?
No, both would be great, since they work somewhat differently. Maybe if has(c): print(c.__attrs_attrs__) or something like that? That should pass, right?
Just so I understand this correctly: the import from typing_extensions only works, because we have the stubs in pyi files that are only touched by mypy which in turn brings in typing_extensions? IOW: if we'd use inline type hints, this would fall apart?
That's right. The typeshed has typing_extensions in the stdlib. I assume they did this so that they can use new typing constructs in the typeshed without tripping MyPy. At runtime, you'd have to do something like:
import typing
if typing.TYPE_CHECKING:
from typing_extensions import TypeGuard
# Use TypeGuard either in quotes or with `from __future__ import annotations`
Thanks!