From ca2a739574fb259f156faba005aaa802e6f19faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 8 Sep 2023 02:29:47 +0200 Subject: [PATCH] Add Doc from PEP 727: https://peps.python.org/pep-0727/ (#277) Co-authored-by: Jelle Zijlstra Co-authored-by: Alex Waygood --- CHANGELOG.md | 2 ++ doc/index.rst | 31 ++++++++++++++++++++++++++++++ src/test_typing_extensions.py | 36 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 36 +++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fb1003..7ed01296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Release 4.8.0 (???) +- Add `typing_extensions.Doc`, as proposed by PEP 727. Patch by + Sebastián Ramírez. - Drop support for Python 3.7 (including PyPy-3.7). Patch by Alex Waygood. - Fix bug where `get_original_bases()` would return incorrect results when called on a concrete subclass of a generic class. Patch by Alex Waygood diff --git a/doc/index.rst b/doc/index.rst index 33492e52..28b795a3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -722,6 +722,37 @@ Functions .. versionadded:: 4.1.0 + +Annotation metadata +~~~~~~~~~~~~~~~~~~~ + +.. class:: Doc(documentation, /) + + Define the documentation of a type annotation using :data:`Annotated`, to be + used in class attributes, function and method parameters, return values, + and variables. + + The value should be a positional-only string literal to allow static tools + like editors and documentation generators to use it. + + This complements docstrings. + + The string value passed is available in the attribute ``documentation``. + + Example:: + + >>> from typing_extensions import Annotated, Doc + >>> def hi(to: Annotated[str, Doc("Who to say hi to")]) -> None: ... + + .. versionadded:: 4.8.0 + + See :pep:`727`. + + .. attribute:: documentation + + The documentation string passed to :class:`Doc`. + + Pure aliases ~~~~~~~~~~~~ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index fcebf131..97717bce 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -38,6 +38,7 @@ from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol +from typing_extensions import Doc from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated # Flags used to mark tests that only apply after a specific @@ -5898,5 +5899,40 @@ class MyAlias(TypeAliasType): pass +class DocTests(BaseTestCase): + def test_annotation(self): + + def hi(to: Annotated[str, Doc("Who to say hi to")]) -> None: pass + + hints = get_type_hints(hi, include_extras=True) + doc_info = hints["to"].__metadata__[0] + self.assertEqual(doc_info.documentation, "Who to say hi to") + self.assertIsInstance(doc_info, Doc) + + def test_repr(self): + doc_info = Doc("Who to say hi to") + self.assertEqual(repr(doc_info), "Doc('Who to say hi to')") + + def test_hashability(self): + doc_info = Doc("Who to say hi to") + self.assertIsInstance(hash(doc_info), int) + self.assertNotEqual(hash(doc_info), hash(Doc("Who not to say hi to"))) + + def test_equality(self): + doc_info = Doc("Who to say hi to") + # Equal to itself + self.assertEqual(doc_info, doc_info) + # Equal to another instance with the same string + self.assertEqual(doc_info, Doc("Who to say hi to")) + # Not equal to another instance with a different string + self.assertNotEqual(doc_info, Doc("Who not to say hi to")) + + def test_pickle(self): + doc_info = Doc("Who to say hi to") + for proto in range(pickle.HIGHEST_PROTOCOL): + pickled = pickle.dumps(doc_info, protocol=proto) + self.assertEqual(doc_info, pickle.loads(pickled)) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 97a0d1cd..c96bf90f 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -60,6 +60,7 @@ 'clear_overloads', 'dataclass_transform', 'deprecated', + 'Doc', 'get_overloads', 'final', 'get_args', @@ -2813,6 +2814,41 @@ def get_protocol_members(tp: type, /) -> typing.FrozenSet[str]: return frozenset(_get_protocol_attrs(tp)) +if hasattr(typing, "Doc"): + Doc = typing.Doc +else: + class Doc: + """Define the documentation of a type annotation using ``Annotated``, to be + used in class attributes, function and method parameters, return values, + and variables. + + The value should be a positional-only string literal to allow static tools + like editors and documentation generators to use it. + + This complements docstrings. + + The string value passed is available in the attribute ``documentation``. + + Example:: + + >>> from typing_extensions import Annotated, Doc + >>> def hi(to: Annotated[str, Doc("Who to say hi to")]) -> None: ... + """ + def __init__(self, documentation: str, /) -> None: + self.documentation = documentation + + def __repr__(self) -> str: + return f"Doc({self.documentation!r})" + + def __hash__(self) -> int: + return hash(self.documentation) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Doc): + return NotImplemented + return self.documentation == other.documentation + + # Aliases for items that have always been in typing. # Explicitly assign these (rather than using `from typing import *` at the top), # so that we get a CI error if one of these is deleted from typing.py