From 441ccdade50e7dcb50c43fa780d7c78ed12e959b Mon Sep 17 00:00:00 2001 From: pavel Date: Fri, 30 Aug 2024 23:21:17 +0300 Subject: [PATCH] Add documentation for exec_type_checking --- .../dealing_with_type_checking/__init__.py | 0 .../dealing_with_type_checking/chat.py | 13 +++++++ .../error_on_analysis.py | 15 ++++++++ .../dealing_with_type_checking/main.py | 22 ++++++++++++ .../dealing_with_type_checking/message.py | 12 +++++++ docs/loading-and-dumping/extended-usage.rst | 36 +++++++++++++++++++ scripts/astpath_lint.py | 6 +++- .../_internal/type_tools/type_evaler.py | 10 ++++++ 8 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/__init__.py create mode 100644 docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/chat.py create mode 100644 docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/error_on_analysis.py create mode 100644 docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/main.py create mode 100644 docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/message.py diff --git a/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/__init__.py b/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/chat.py b/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/chat.py new file mode 100644 index 00000000..a4744b5a --- /dev/null +++ b/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/chat.py @@ -0,0 +1,13 @@ +# ruff: noqa: UP035, UP006 +from dataclasses import dataclass +from typing import TYPE_CHECKING, List + +if TYPE_CHECKING: + from .message import Message + + +@dataclass +class Chat: + id: int + name: str + messages: List["Message"] diff --git a/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/error_on_analysis.py b/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/error_on_analysis.py new file mode 100644 index 00000000..a9594e64 --- /dev/null +++ b/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/error_on_analysis.py @@ -0,0 +1,15 @@ +from typing import get_type_hints + +from .chat import Chat +from .message import Message + +try: + get_type_hints(Chat) +except NameError as e: + assert str(e) == "name 'Message' is not defined" + + +try: + get_type_hints(Message) +except NameError as e: + assert str(e) == "name 'Chat' is not defined" diff --git a/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/main.py b/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/main.py new file mode 100644 index 00000000..ad367b8c --- /dev/null +++ b/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/main.py @@ -0,0 +1,22 @@ +# ruff: noqa: UP035, UP006 +from typing import List, get_type_hints + +from adaptix.type_tools import exec_type_checking + +from . import chat, message + +# You pass the module object +exec_type_checking(chat) +exec_type_checking(message) + +# After these types can be extracted +assert get_type_hints(chat.Chat) == { + "id": int, + "name": str, + "messages": List[message.Message], +} +assert get_type_hints(chat.Message) == { + "id": int, + "text": str, + "chat": chat.Chat, +} diff --git a/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/message.py b/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/message.py new file mode 100644 index 00000000..03ad66ae --- /dev/null +++ b/docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/message.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .chat import Chat + + +@dataclass +class Message: + id: int + text: str + chat: "Chat" diff --git a/docs/loading-and-dumping/extended-usage.rst b/docs/loading-and-dumping/extended-usage.rst index b7f31485..b7a5663a 100644 --- a/docs/loading-and-dumping/extended-usage.rst +++ b/docs/loading-and-dumping/extended-usage.rst @@ -48,6 +48,42 @@ But it does not work with cyclic-referenced objects like item_category.sub_categories.append(item_category) +Dealing with ``if TYPE_CHECKING`` +=================================== + +Sometimes you want to split interdependent models into several files. +This results in some imports being visible only to type checkers. +Analysis of such type hints is not available at runtime. + + +Let's imagine that we have two files: + +.. literalinclude:: /examples/loading-and-dumping/extended_usage/dealing_with_type_checking/chat.py + :caption: File ``chat.py`` + :lines: 2- + +.. literalinclude:: /examples/loading-and-dumping/extended_usage/dealing_with_type_checking/message.py + :caption: File ``message.py`` + + +If you try to get type hints at runtime, you will fail: + +.. literalinclude:: /examples/loading-and-dumping/extended_usage/dealing_with_type_checking/error_on_analysis.py + +At runtime, these imports are not executed, so the builtin analysis function can not resolve forward refs. + +Adaptix can overcome this via :func:`.type_tools.exec_type_checking`. +It extracts code fragments defined under ``if TYPE_CHECKING`` and ``if typing.TYPE_CHECKING`` constructs +and then executes them in the context of module. +As a result, the module namespace is filled with missing names, and *any* introspection function can acquire types. + +You should call ``exec_type_checking`` after all required modules can be imported. +Usually, it must be at ``main`` module. + +.. literalinclude:: /examples/loading-and-dumping/extended_usage/dealing_with_type_checking/main.py + :caption: File ``main.py`` + :lines: 2- + Name mapping ======================== diff --git a/scripts/astpath_lint.py b/scripts/astpath_lint.py index d39cb72e..6128229e 100644 --- a/scripts/astpath_lint.py +++ b/scripts/astpath_lint.py @@ -64,7 +64,11 @@ class RuleMatch: module="typing", variable="get_type_hints", error_msg="Use type_tools.get_all_type_hints() instead of typing.get_type_hints()", - exclude=["src/adaptix/_internal/type_tools/fundamentals.py"], + exclude=[ + "src/adaptix/_internal/type_tools/fundamentals.py", + "docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/main.py", + "docs/examples/loading-and-dumping/extended_usage/dealing_with_type_checking/error_on_analysis.py", + ], ), ImportRule( module="_decimal", diff --git a/src/adaptix/_internal/type_tools/type_evaler.py b/src/adaptix/_internal/type_tools/type_evaler.py index 9ee264cc..e3236e48 100644 --- a/src/adaptix/_internal/type_tools/type_evaler.py +++ b/src/adaptix/_internal/type_tools/type_evaler.py @@ -45,6 +45,16 @@ def exec_type_checking( *, collector: Callable[[ast.Module], list[ast.stmt]] = default_collector, ) -> None: + """This function scans module source code, + collects fragments under ``if TYPE_CHECKING`` and ``if typing.TYPE_CHECKING`` + and executes them in the context of module. + After these, all imports and type definitions became available at runtime for analysis. + + By default, it ignores ``if`` with ``else`` branch. + + :param module: A module for processing + :param collector: A function collecting code fragments to execute + """ source = inspect.getsource(module) fragments = collector(ast.parse(source)) code = compile(ast.Module(fragments, type_ignores=[]), f"", "exec")