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

Add documentation for exec_type_checking #333

Merged
merged 1 commit into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading