-
-
Notifications
You must be signed in to change notification settings - Fork 119
feat(python): Make UNSET a PEP 661 sentinel
#1086
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
Changes from all commits
4dcdb43
6b986c7
1d6443b
f1d56d6
1839d4d
e4d4cb1
81e4003
f26cba4
f3e6790
64c1794
a687c0b
48d60e5
1fd975a
2187d9a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import copy | ||
| from typing import TYPE_CHECKING, Any, Union, get_type_hints | ||
|
|
||
| if TYPE_CHECKING: | ||
| # NOTE: Don't move this into a runtime import (yet) | ||
| from vgplot._types import UNSET | ||
|
|
||
|
|
||
| def _import_unset() -> UNSET: | ||
| # NOTE: A regular import would add: | ||
| # - `vgplot._types` to `sys.modules` | ||
| # - `UNSET` to `globals` | ||
| # And we can't use `importlib.reload`, since that would create a new object | ||
| from vgplot._types import UNSET | ||
|
|
||
| return UNSET | ||
|
|
||
|
|
||
| def test_unset_identity() -> None: | ||
| unset_1 = _import_unset() | ||
| unset_2 = _import_unset() | ||
| assert unset_1 is unset_2 | ||
|
|
||
|
|
||
| def test_unset_repr() -> None: | ||
| assert repr(_import_unset()) == "UNSET" | ||
|
|
||
|
|
||
| def test_unset_pickle() -> None: | ||
| import pickle | ||
|
|
||
| unset = _import_unset() | ||
| assert pickle.loads(pickle.dumps(unset)) is unset # noqa: S301 | ||
|
|
||
|
|
||
| def test_unset_type_expression_union() -> None: | ||
| # Adapted from https://github.com/python/typing_extensions/blob/83400e979b8e3b0b647f9a6a57f0275230e5f19f/src/test_typing_extensions.py#L9694-L9701 | ||
| from vgplot._types import UNSET | ||
|
|
||
| def func1(a: int | UNSET = UNSET) -> None: ... | ||
| def func2(a: UNSET | int = UNSET) -> None: ... | ||
|
|
||
| assert get_type_hints(func1, localns=locals())["a"] == Union[int, UNSET] # noqa: UP007 | ||
| assert get_type_hints(func2, localns=locals())["a"] == Union[UNSET, int] # noqa: UP007 | ||
|
|
||
|
|
||
| def test_unset_copy_identity() -> None: | ||
| # Adapted from https://github.com/python/typing_extensions/blob/83400e979b8e3b0b647f9a6a57f0275230e5f19f/src/test_typing_extensions.py#L9711-L9713 | ||
| unset = _import_unset() | ||
| assert unset is copy.copy(unset) | ||
| assert unset is copy.deepcopy(unset) | ||
|
|
||
|
|
||
| def test_unset_union_identity() -> None: | ||
| unset = _import_unset() | ||
| assert (unset | unset) is unset | ||
|
|
||
|
|
||
| if TYPE_CHECKING: | ||
| from typing_extensions import assert_type | ||
|
|
||
| def typing_unset( | ||
| a: UNSET, b: str | UNSET, c: Any | UNSET, d: int | None | UNSET = UNSET | ||
| ) -> None: | ||
| assert_type(a, UNSET) | ||
| assert_type(b, str | UNSET) | ||
| assert_type(c, Any | UNSET) | ||
| assert_type(d, int | None | UNSET) |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,103 @@ | ||||
| """Backwards compatibility for features not available at our [requires-python][1]. | ||||
|
|
||||
| Import from here to avoid introducing runtime dependency on [typing_extensions][2]. | ||||
|
|
||||
| [1]: https://packaging.python.org/en/latest/specifications/pyproject-toml/#requires-python | ||||
| [2]: https://github.com/python/typing_extensions | ||||
|
|
||||
| ## sentinel | ||||
| [`sentinel`][3] was introduced in `3.15` (see [PEP 661][4]). | ||||
|
|
||||
| We can remove the backport after [3.15 end-of-life][5]. | ||||
|
|
||||
| [3]: https://docs.python.org/3.15/library/functions.html#sentinel | ||||
| [4]: https://peps.python.org/pep-0661/ | ||||
| [5]: https://peps.python.org/pep-0790/#lifespan | ||||
| """ | ||||
|
|
||||
| from __future__ import annotations | ||||
|
|
||||
| import sys | ||||
| import typing | ||||
|
|
||||
| # ruff: noqa: A002 | ||||
| from contextlib import suppress | ||||
| from importlib.util import find_spec | ||||
| from typing import TYPE_CHECKING, Any, ClassVar | ||||
|
|
||||
|
|
||||
| def _sentinel_backport_pre_typing_extensions_4_16() -> Any: | ||||
| class _sentinel_backport: | ||||
| """Create a unique sentinel object. | ||||
|
|
||||
| *name* should be the name of the variable to which the return value shall be assigned. | ||||
| """ | ||||
|
|
||||
| def __init__(self, name: str, /, *, repr: str | None = None) -> None: | ||||
| self.__name__: str = name | ||||
| self._repr: str = repr if repr is not None else name | ||||
| # TODO @dangotbanned: Figure out why they didn't use the `"__main__"` default here? | ||||
| module: str | None = None | ||||
| if hasattr(sys, "_getframemodulename"): | ||||
| module = sys._getframemodulename(1) or "__main__" | ||||
| elif hasattr(sys, "_getframe"): | ||||
| with suppress(ValueError): | ||||
| module = sys._getframe(1).f_globals.get("__name__", "__main__") | ||||
|
|
||||
| # For pickling as a singleton | ||||
| self.__module__ = module # pyright: ignore[reportAttributeAccessIssue] # ty: ignore[invalid-assignment] | ||||
|
|
||||
| __init_subclass__: ClassVar[None] = None | ||||
|
|
||||
| def __repr__(self) -> str: | ||||
| return self._repr | ||||
|
|
||||
| if sys.version_info < (3, 11): | ||||
| # The presence of this method convinces typing._type_check that Sentinels are types. | ||||
| def __call__(self, *args: Any, **kwargs: Any) -> Any: | ||||
| msg = f"{type(self).__name__!r} object is not callable" | ||||
| raise TypeError(msg) | ||||
|
|
||||
| def __or__(self, other: Any) -> Any: | ||||
| return typing.Union[self, other] # noqa: UP007 | ||||
|
|
||||
| def __ror__(self, other: Any) -> Any: | ||||
| return typing.Union[other, self] # noqa: UP007 | ||||
|
|
||||
| def __reduce__(self) -> str: | ||||
| return self.__name__ | ||||
|
|
||||
| return _sentinel_backport | ||||
|
|
||||
|
|
||||
| def _sentinel_backport_pre_py_3_15() -> Any: | ||||
| """Return a [PEP 661]-compatible [`sentinel`](https://docs.python.org/3.15/library/functions.html#sentinel) factory. | ||||
|
|
||||
| [PEP 661]: https://peps.python.org/pep-0661/ | ||||
|
|
||||
| ## Notes | ||||
| - Does not depend on `typing_extensions`, but will use it if a suitable version is available | ||||
| - Fallback is adapted from [`typing_extensions==4.16.0`](https://github.com/python/typing_extensions/blob/f29cd28d8ed7642cafb1d18daf5aa41be6a5c0aa/src/typing_extensions.py#L176-L271) | ||||
| """ | ||||
| if find_spec("typing_extensions"): | ||||
| import typing_extensions | ||||
|
|
||||
| # NOTE: In the same release the name changed, this guy landed https://github.com/python/typing_extensions/pull/617 | ||||
| # `4.14-4.15` is fine for typing, but the runtime changes are too big to rely on | ||||
| if hasattr(typing_extensions, "sentinel"): | ||||
| return getattr(typing_extensions, "sentinel", typing_extensions.Sentinel) | ||||
|
|
||||
| return _sentinel_backport_pre_typing_extensions_4_16() | ||||
|
|
||||
|
|
||||
| if TYPE_CHECKING: | ||||
| # Was renamed in https://github.com/python/typing_extensions/releases/tag/4.16.0 | ||||
| from typing_extensions import Sentinel as sentinel # noqa: N813 | ||||
| else: # noqa: PLR5501 | ||||
| if sys.version_info >= (3, 15): | ||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So this part would be detected by Ruff via outdated-version-block ( Anything that uses
But on this part:
Without dependencies? With
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went with a module doc instead, since I wrote one in (https://github.com/narwhals-dev/narwhals/blob/deffd1d1e11dd593675119c1445dc7d5291426c3/src/narwhals/_typing_compat.py)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm generally a fan of updating version requirements when it helps us clean up stuff. We don't need to support the oldest Python versions so feel free to update version requirements.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we were to update requirements, adding this would be my preference as it is a single file dependency and very common: dependencies = [
"typing-extensions>=4.16 ; python_full_version < '3.15'",
]Depending on the latest python version can simplify maintainence, but it will hurt adoption of Mosaic. |
||||
| from builtins import sentinel | ||||
| else: | ||||
| sentinel = _sentinel_backport_pre_py_3_15() | ||||
|
|
||||
|
|
||||
| __all__ = ("sentinel",) | ||||


Uh oh!
There was an error while loading. Please reload this page.