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

frozendict should be covariant on V, like Mapping. #93

Closed
wants to merge 7 commits into from

Conversation

asford
Copy link

@asford asford commented Dec 22, 2023

Immutable collections, like Sequence, tuple and frozendict are covariant in their contained types.
The frozendict base interface, Mapping[K, V], is covariant in V.

Update the frozendict .pyi stubs to also specify a covariant V for frozendict[K,V].
This enables us to assign collections-of-subtypes to frozendict generics,
which currently raises a type error to value type invariance.

For example:

string_vals: frozendict[str, str] = frozendict(a="foo")
vals: frozendict[str, Hashable] = string_vals # should be allowed

Currently raises a type error:

Expression of type "frozendict[str, str]" cannot be assigned to declared type "frozendict[str, Hashable]"
  "frozendict[str, str]" is incompatible with "frozendict[str, Hashable]"
    Type parameter "V@frozendict" is invariant, but "str" is not the same as "Hashable"

but is a valid assignment.

@asford
Copy link
Author

asford commented Dec 22, 2023

@Marco-Sulla this is a minor fix for the stubs added in #70.

Thanks for the great work on this project.

@Marco-Sulla
Copy link
Owner

Marco-Sulla commented Dec 22, 2023

dict is invariant too.

from collections.abc import Hashable
string_vals: dict[str, str] = dict(a="foo")
vals: dict[str, Hashable] = string_vals

mypy output:

test/typed.py:38: error: Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "Dict[str, Hashable]")  [assignment]
test/typed.py:38: note: "Dict" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
test/typed.py:38: note: Consider using "Mapping" instead, which is covariant in the value type

@Marco-Sulla Marco-Sulla added the Status: Not a bug This doesn't seem right label Dec 22, 2023
@asford
Copy link
Author

asford commented Dec 22, 2023

dict implements MutableMapping, as it's a mutable container and is therefore invariant.

frozendict is a somewhat special case, as it implements the (covariant) Mapping interface.

By analogy, frozendict is to dict as tuple is to list or frozenset is to set. It's a (essentially covariant) immutable container.

I think you've extended this with non-covariant-ish update/set/ror methods, however this can be appropriately typed using a non-covariant V2 parameter returning a union type, as you've done here, because it does not modify the source frozendict.

Would you be open to reopening with updates to allow covariant behavior on V, and non-covariant extensions? This is actually a pretty useful behavior for frozen dataclasses where we may want to have a concrete, dict-like property.

@Marco-Sulla
Copy link
Owner

dict implements MutableMapping, as it's a mutable container and is therefore invariant.

Mh, you're right. This works:

from collections.abc import Hashable
string_vals: tuple[str] = ("foo", )
vals: tuple[Hashable] = string_vals

For now I simply reopen this, but I check it more deeply later.

@Marco-Sulla Marco-Sulla reopened this Dec 23, 2023
@Marco-Sulla
Copy link
Owner

(Personal rant: Python typing is a mess IMHO, and I repeat IMHO. The syntax is ugly, is quite difficult to implement and to debug.)

@Marco-Sulla
Copy link
Owner

Could you also add this to test/typed.py?

from collections.abc import Hashable
string_vals: frozendict[str, str] = frozendict(a="foo")
vals: frozendict[str, Hashable] = string_vals

@Marco-Sulla Marco-Sulla added Type: Defect Something works, but can work better Priority: Low Not a big problem... Effort: Low easy task Needs: Test Hey, it compiles! Ship it! and removed Status: Not a bug This doesn't seem right labels Dec 23, 2023
@asford
Copy link
Author

asford commented Dec 24, 2023

(Personal rant: Python typing is a mess IMHO, and I repeat IMHO. The syntax is ugly, is quite difficult to implement and to debug.)

I definitely ...don't disagree.... 😉

Thanks for pointing me to test/typed.py. I took the liberty of:

  • Updating test/typed.py file to use assert_type, which documents the expected type of a statement and verifies this type via the checker.
  • Adding typing checks with pyright and mypy (if installed) to mytest.sh

In doing so, I found a few small bugs in __init__.pyi. I've updated these type signatures to clarify the covariance issues we described above as well as tightened up the type declarations for the extended set and setdefault methods.

This fixes a few errors I was seeing in type inference under pylance in vscode.

@asford
Copy link
Author

asford commented Dec 31, 2023

Fantastic, thanks for the review.

I'm happy to setup a quick tox-based test config that includes your existing suite and these type tests if you'd like.

It might help if you have other contributors come by and could be run in actions.

@Marco-Sulla
Copy link
Owner

It might help if you have other contributors

I can contribute, but please, let me know about the open points, especially the ones about __init__.pyi

@asford
Copy link
Author

asford commented Jan 1, 2024

I'm not sure I understand? There aren't any review comments open right now. The test suite with type assertions is passing on this branch w/ mypy and pyright.

@Marco-Sulla
Copy link
Owner

There aren't any review comments open right now.

Emh. Can you scroll this discussion up? :D

@Marco-Sulla
Copy link
Owner

In particular all the "Why you removed" etc

@asford
Copy link
Author

asford commented Jan 16, 2024

In particular all the "Why you removed" etc

I don't see any review comments, maybe they're in draft?

K2 = TypeVar("K2")
V2 = TypeVar("V2")
SelfT = TypeVar("SelfT", bound=frozendict[K, V])
Copy link
Owner

Choose a reason for hiding this comment

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

Why you removed bound?


class frozendict(Mapping[K, V]):
@overload
def __new__(cls: Type[SelfT]) -> SelfT: ...
@overload
def __new__(cls: Type[SelfT], **kwargs: V) -> frozendict[str, V]: ...
Copy link
Owner

Choose a reason for hiding this comment

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

Why you removed many Type[SelfT]

def key(self: SelfT, index: int) -> K: ...
def value(self: SelfT, index: int) -> V: ...
def item(self: SelfT, index: int) -> Tuple[K, V]: ...
@overload
Copy link
Owner

Choose a reason for hiding this comment

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

Why you removed the overload?

from collections.abc import Hashable
from typing import ItemsView, Iterator, KeysView, Mapping, ValuesView

from typing_extensions import assert_type
Copy link
Owner

Choose a reason for hiding this comment

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

Hey, that's cool! Can this file be removed and this code added to pytest suite?

popd
fi

if command -v pyright &> /dev/null
Copy link
Owner

Choose a reason for hiding this comment

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

What the d@&^ is pyright? :D

@@ -1,3 +1,15 @@
rm test/core.*

./test/debug.py && pytest

if command -v mypy &> /dev/null
Copy link
Owner

Choose a reason for hiding this comment

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

I think this code is obsolete now that you changed typed.py. In particular if the code of typed.py will be moved in pytest

@Marco-Sulla
Copy link
Owner

Mh maybe I forgot to submit review...

@Marco-Sulla
Copy link
Owner

Superseeded by ebbc70b and subsequent commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Effort: Low easy task Needs: Test Hey, it compiles! Ship it! Priority: Low Not a big problem... Type: Defect Something works, but can work better
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants