-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Use TypeIs in is_dataclass #11929
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
Use TypeIs in is_dataclass #11929
Conversation
7721a02
to
3f47361
Compare
This comment has been minimized.
This comment has been minimized.
@erictraut Is this a problem with Pyright? |
@NeilGirdhar, can you be more specific? I'm not sure what you're asking. |
I think it's about this CI failure:
I haven't looked at this code in detail to figure out whether pyright is correct here. |
So, I removed one of the overloads, but from my testing it didn't seem necessary anymore. I'll quickly try replacing it to see if it makes a difference. (Edit: still broken. Removing it since it doesn't belong anymore IMO.) The failure in PyRIght seems to be because Pyright isn't evaluating the overloads for |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@NeilGirdhar, PEP 742 allows some variation between type checkers. What you're seeing here is an example of this. Pyright's evaluation is arguably more accurate (narrower) and more consistent than mypy's, but both results are defensible. I think you'll need to change your test case to accommodate this variation. PEP 742 says:
If you try this self-contained code sample in both pyright and mypy, you'll see that mypy is inconsistent but pyright is consistent. from dataclasses import Field
from typing import Any, ClassVar, Protocol, runtime_checkable
from typing_extensions import TypeIs
@runtime_checkable
class DataclassInstance(Protocol):
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
def is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ...
def test1(x: type) -> None:
if isinstance(x, DataclassInstance):
reveal_type(x) # Both mypy and pyright reveal <subclass of "type" and "DataclassInstance">
def test2(x: type) -> None:
if is_dataclass(x):
reveal_type(x) # Mypy reveals "DataclassInstance" but pyright gives results consistent with "isinstance" |
@erictraut Sorry, but I don't understand how these two functions are supposed to be the same. In the first function, you are checking against If you change Similarly, if you change the isinstance line to Therefore, I believe this is a bug in Pyright even if as you say PEP 742 doesn't specify this case. |
Ah yes, you're correct. This is a bug in pyright. |
@NeilGirdhar, this will be fixed in the next release of pyright. I typically publish a new version every Tuesday evening PST. You could either wait until then to land this PR or you could temporarily comment out this test case. |
27bb3ff
to
e48608a
Compare
This comment has been minimized.
This comment has been minimized.
@JelleZijlstra There appear to be some errors with updating PyRight. Would you mind updating PyRight when you have a chance? |
Dependencies are updated automatically by renovate each UTC night. |
Diff from mypy_primer, showing the effect of this PR on open source code: pydantic (https://github.com/samuelcolvin/pydantic)
+ pydantic/v1/json.py:80: error: No overload variant of "asdict" matches argument type "type[DataclassInstance]" [call-overload]
+ pydantic/v1/json.py:80: note: Possible overload variants:
+ pydantic/v1/json.py:80: note: def asdict(obj: DataclassInstance) -> dict[str, Any]
+ pydantic/v1/json.py:80: note: def [_T] asdict(obj: DataclassInstance, *, dict_factory: Callable[[list[tuple[str, Any]]], _T]) -> _T
+ pydantic/deprecated/json.py:98: error: No overload variant of "asdict" matches argument type "type[DataclassInstance]" [call-overload]
+ pydantic/deprecated/json.py:98: note: Possible overload variants:
+ pydantic/deprecated/json.py:98: note: def asdict(obj: DataclassInstance) -> dict[str, Any]
+ pydantic/deprecated/json.py:98: note: def [_T] asdict(obj: DataclassInstance, *, dict_factory: Callable[[list[tuple[str, Any]]], _T]) -> _T
hydra-zen (https://github.com/mit-ll-responsible-ai/hydra-zen)
- src/hydra_zen/structured_configs/_implementations.py:1129: error: Argument 2 to "builds" of "BuildsFn" has incompatible type "**dict[str, int | float | Path | DataClass_ | type[DataClass_] | Enum | Any | Sequence[HydraSupportedType] | Mapping[Any, HydraSupportedType] | None]"; expected "Literal[False] | None" [arg-type]
- src/hydra_zen/structured_configs/_implementations.py:1129: error: Argument 2 to "builds" of "BuildsFn" has incompatible type "**dict[str, int | float | Path | DataClass_ | type[DataClass_] | Enum | Any | Sequence[HydraSupportedType] | Mapping[Any, HydraSupportedType] | None]"; expected "bool" [arg-type]
- src/hydra_zen/structured_configs/_implementations.py:1129: error: Argument 2 to "builds" of "BuildsFn" has incompatible type "**dict[str, int | float | Path | DataClass_ | type[DataClass_] | Enum | Any | Sequence[HydraSupportedType] | Mapping[Any, HydraSupportedType] | None]"; expected "Builds[Callable[[Callable[..., Any]], Callable[..., Any]]] | ZenPartialBuilds[Callable[[Callable[..., Any]], Callable[..., Any]]] | HydraPartialBuilds[Callable[[Callable[..., Any]], Callable[..., Any]]] | Just[Callable[[Callable[..., Any]], Callable[..., Any]]] | type[Builds[Callable[[Callable[..., Any]], Callable[..., Any]]]] | type[ZenPartialBuilds[Callable[[Callable[..., Any]], Callable[..., Any]]]] | type[HydraPartialBuilds[Callable[[Callable[..., Any]], Callable[..., Any]]]] | type[Just[Callable[[Callable[..., Any]], Callable[..., Any]]]] | Callable[[Callable[..., Any]], Callable[..., Any]] | str | None | Sequence[Builds[Callable[[Callable[..., Any]], Callable[..., Any]]] | ZenPartialBuilds[Callable[[Callable[..., Any]], Callable[..., Any]]] | HydraPartialBuilds[Callable[[Callable[..., Any]], Callable[..., Any]]] | Just[Callable[[Callable[..., Any]], Callable[..., Any]]] | type[Builds[Callable[[Callable[..., Any]], Callable[..., Any]]]] | type[ZenPartialBuilds[Callable[[Callable[..., Any]], Callable[..., Any]]]] | type[HydraPartialBuilds[Callable[[Callable[..., Any]], Callable[..., Any]]]] | type[Just[Callable[[Callable[..., Any]], Callable[..., Any]]]] | Callable[[Callable[..., Any]], Callable[..., Any]] | str | None]" [arg-type]
- src/hydra_zen/structured_configs/_implementations.py:1129: error: Argument 2 to "builds" of "BuildsFn" has incompatible type "**dict[str, int | float | Path | DataClass_ | type[DataClass_] | Enum | Any | Sequence[HydraSupportedType] | Mapping[Any, HydraSupportedType] | None]"; expected "Mapping[str, SupportedPrimitive] | None" [arg-type]
- src/hydra_zen/structured_configs/_implementations.py:1129: error: Argument 2 to "builds" of "BuildsFn" has incompatible type "**dict[str, int | float | Path | DataClass_ | type[DataClass_] | Enum | Any | Sequence[HydraSupportedType] | Mapping[Any, HydraSupportedType] | None]"; expected "list[str | DataClass_ | type[DataClass_] | Mapping[str, str | Sequence[str] | None]] | None" [arg-type]
- src/hydra_zen/structured_configs/_implementations.py:1129: error: Argument 2 to "builds" of "BuildsFn" has incompatible type "**dict[str, int | float | Path | DataClass_ | type[DataClass_] | Enum | Any | Sequence[HydraSupportedType] | Mapping[Any, HydraSupportedType] | None]"; expected "str | None" [arg-type]
- src/hydra_zen/structured_configs/_implementations.py:1129: error: Argument 2 to "builds" of "BuildsFn" has incompatible type "**dict[str, int | float | Path | DataClass_ | type[DataClass_] | Enum | Any | Sequence[HydraSupportedType] | Mapping[Any, HydraSupportedType] | None]"; expected "tuple[type[DataClass_], ...]" [arg-type]
- src/hydra_zen/structured_configs/_implementations.py:1129: error: Argument 2 to "builds" of "BuildsFn" has incompatible type "**dict[str, int | float | Path | DataClass_ | type[DataClass_] | Enum | Any | Sequence[HydraSupportedType] | Mapping[Any, HydraSupportedType] | None]"; expected "ZenConvert | None" [arg-type]
- src/hydra_zen/structured_configs/_implementations.py:1129: error: Argument 2 to "builds" of "BuildsFn" has incompatible type "**dict[str, int | float | Path | DataClass_ | type[DataClass_] | Enum | Any | Sequence[HydraSupportedType] | Mapping[Any, HydraSupportedType] | None]"; expected "T" [arg-type]
- src/hydra_zen/structured_configs/_implementations.py:1161: error: Incompatible types in assignment (expression has type "Just", variable has type "type[Builds[Any]]") [assignment]
pytest (https://github.com/pytest-dev/pytest)
+ src/_pytest/_io/pprint.py:117: error: Right operand of "and" is never evaluated [unreachable]
+ src/_pytest/_io/pprint.py:125: error: Statement is unreachable [unreachable]
streamlit (https://github.com/streamlit/streamlit)
+ lib/streamlit/runtime/caching/hashing.py: note: In member "_to_bytes" of class "_CacheFuncHasher":
+ lib/streamlit/runtime/caching/hashing.py:409:34: error: No overload variant of "asdict" matches argument type "Type[DataclassInstance]" [call-overload]
+ lib/streamlit/runtime/caching/hashing.py:409:34: note: Possible overload variants:
+ lib/streamlit/runtime/caching/hashing.py:409:34: note: def asdict(obj: DataclassInstance) -> Dict[str, Any]
+ lib/streamlit/runtime/caching/hashing.py:409:34: note: def [_T] asdict(obj: DataclassInstance, *, dict_factory: Callable[[List[Tuple[str, Any]]], _T]) -> _T
|
Updated. Ready for pull. |
Looking at the primer output: pydantic and streamlit are true positives. They use pytest is a false positive: It's basically complaining about this code (where if is_dataclass(object) and not isinstance(object, type): ... I'm unsure why that is. Maybe it's assuming that |
Definitely a weird choice. So, can the PR be pulled, or should we wait for the dependent projects to fix bugs? |
I'm fine with merging this, despite the false positive, but I'll leave it open for a day or two if someone has an idea how to fix/work around it. |
Sounds good 😄 ! |
Thanks for the quick merge. If anyone has time, it may be worth mentioning that most of the other uses of |
I'm seeing a lot of new errors on work codebase from code like the case srittau mentions:
Not sure if anyone has clever suggestions here |
I also see a lot of errors on this at work and it has been reported for I also see (what I would call) weird behaviour between Code sample in pyright playground import dataclasses
from typing import reveal_type
def func(x: object) -> None:
if dataclasses.is_dataclass(x):
reveal_type(x)
print(dataclasses.asdict(x)) Gives: Type of "x" is "DataclassInstance | type[DataclassInstance]"
Argument of type "DataclassInstance | type[DataclassInstance]" cannot be assigned to parameter "obj" of type "DataclassInstance" in function "asdict"
Type "DataclassInstance | type[DataclassInstance]" is incompatible with type "DataclassInstance"
"__dataclass_fields__" is defined as a ClassVar in protocol (reportArgumentType) That seems fair and is in line with Which gives (on 1.11):
Now I want to narrow Code sample in pyright playground import dataclasses
from typing import reveal_type
def func(x: object) -> None:
if dataclasses.is_dataclass(x) and isinstance(x, object):
reveal_type(x)
print(dataclasses.asdict(x)) This has no errors on
main.py:5: note: Revealed type is "Union[_typeshed.DataclassInstance, type[_typeshed.DataclassInstance]]"
main.py:6: error: Argument 1 to "asdict" has incompatible type "DataclassInstance | type[DataclassInstance]"; expected "DataclassInstance" [arg-type]
Found 1 error in 1 file (checked 1 source file) Even if it changes the behaviour I don't really understand why narrowing I don't want to point fingers as I have deep respect for the much more extensive knowledge that you maintainers have of the Python typing system but if I were to summarize this I would say:
|
@DanielNoord, I agree this looks like a bug in pyright. An |
There is a second bug in pyright here as well. This one is related to its overload matching logic. When overload matching is ambiguous because of an Mypy appears to have the same bug here. I've filed a separate bug in the mypy issue tracker. |
Thanks @erictraut (I must say I'm always amazed at the speed at which you and the I'm wondering what you think of the second issue I "identified". With the fixes marked for "addressed in next version" merged it seems we still don't have a good way to type guard an |
Once these bugs are fixed in pyright, you will be able to check for a TypedDict instance in a type-safe manner by verifying that import dataclasses
def func(x: object) -> None:
if dataclasses.is_dataclass(x) and not isinstance(x, type):
print(dataclasses.asdict(x)) It's unfortunate that |
We could consider adding |
Thanks Eric, that does indeed seem to work in the playgrounds. I don't think I'll be able to convince people to add an additional dependency to our stack for such a small utility. It is indeed too bad that the API is like this. |
Fixes #9723