Skip to content

Commit

Permalink
Add documentation for exec_type_checking
Browse files Browse the repository at this point in the history
  • Loading branch information
zhPavel committed Aug 30, 2024
1 parent 0600de6 commit 441ccda
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 1 deletion.
Empty file.
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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,
}
Original file line number Diff line number Diff line change
@@ -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"
36 changes: 36 additions & 0 deletions docs/loading-and-dumping/extended-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
========================

Expand Down
6 changes: 5 additions & 1 deletion scripts/astpath_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions src/adaptix/_internal/type_tools/type_evaler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_type_checking of {module}>", "exec")
Expand Down

0 comments on commit 441ccda

Please sign in to comment.