Skip to content

Commit

Permalink
Add support for _typeshed and update CI checks (#193)
Browse files Browse the repository at this point in the history
* typeshed

* lint

* remove python3.8

* macOS 12 -> macOS 13

* validate forward_module in type_checking_overrides
  • Loading branch information
azliu0 authored Dec 11, 2024
1 parent 181bb89 commit 0b01508
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 4 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ jobs:
strategy:
fail-fast: false # run all variants across python versions/os to completion
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
os: ["ubuntu-latest"]
include:
- os: "macos-12" # x86-64
- os: "macos-13" # x86-64
python-version: "3.10"
- os: "macos-14" # ARM64 (M1)
python-version: "3.10"
Expand Down
12 changes: 12 additions & 0 deletions src/synchronicity/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
import importlib
import logging
import sys
import typing

logger = logging.getLogger("synchronicity")

# Modules that cannot be evaluated at runtime, e.g.,
# only available under the TYPE_CHECKING guard, but can be used freely in stub files
TYPE_CHECKING_OVERRIDES = {"_typeshed"}


def evaluated_annotation(annotation, *, globals_=None, declaration_module=None):
# evaluate string annotations...
Expand All @@ -24,6 +29,13 @@ def evaluated_annotation(annotation, *, globals_=None, declaration_module=None):
# in case of unimported modules referenced in the annotation itself
# typically happens with TYPE_CHECKING guards etc.
ref_module, _ = annotation.rsplit(".", 1)
# for modules that can't be evaluated at runtime,
# return a ForwardRef with __forward_module__ set
# to the name of the module that we want to import in the stub file
if ref_module in TYPE_CHECKING_OVERRIDES:
ref = typing.ForwardRef(annotation)
ref.__forward_module__ = ref_module
return ref
# hack: import the library *into* the namespace of the supplied globals
exec(f"import {ref_module}", globals_)
return eval(annotation, globals_)
Expand Down
16 changes: 14 additions & 2 deletions src/synchronicity/type_stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

import synchronicity
from synchronicity import combined_types, overload_tracking
from synchronicity.annotations import evaluated_annotation
from synchronicity.annotations import TYPE_CHECKING_OVERRIDES, evaluated_annotation
from synchronicity.interface import Interface
from synchronicity.synchronizer import (
SYNCHRONIZER_ATTR,
Expand Down Expand Up @@ -135,6 +135,8 @@ def _get_type_vars(typ, synchronizer, home_module):
# The reason it cant be used directly is that this method should return the original type params
# and not translated values
if isinstance(typ, typing.ForwardRef): # TypeVars wrap their arguments as ForwardRefs (sometimes?)
if hasattr(typ, "__forward_module__") and typ.__forward_module__ is not None:
return ret
typ = typ.__forward_arg__
if isinstance(typ, str):
try:
Expand Down Expand Up @@ -695,7 +697,17 @@ def _formatannotation(self, annotation, base_module=None) -> str:
* ignores base_module (uses self.target_module instead)
"""
origin = getattr(annotation, "__origin__", None)
assert not isinstance(annotation, typing.ForwardRef) # Forward refs should already have been evaluated!

# For forward refs from modules that cannot be evaluated at runtime
# but can be used freely in stub files, import the module and return the
# original annotation
if isinstance(annotation, typing.ForwardRef):
if hasattr(annotation, "__forward_module__") and annotation.__forward_module__ in TYPE_CHECKING_OVERRIDES:
self.imports.add(annotation.__forward_module__)
return annotation.__forward_arg__

# The remaining forward refs should already have been evaluated
assert not isinstance(annotation, typing.ForwardRef)
args = safe_get_args(annotation)

if isinstance(annotation, typing_extensions.ParamSpecArgs):
Expand Down
15 changes: 15 additions & 0 deletions test/type_stub_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,3 +619,18 @@ def foo(fn: typing.Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> str:
"def foo(fn: typing.Callable[test.type_stub_helpers.some_mod.P, None], *args: test.type_stub_helpers.some_mod.P.args, **kwargs: test.type_stub_helpers.some_mod.P.kwargs) -> str:" # noqa
in src
) # noqa: E501


if typing.TYPE_CHECKING:
import _typeshed


def test_typeshed():
"""Test that _typeshed annotations are preserved in stubs."""

def foo() -> "_typeshed.OpenTextMode":
return "r"

src = _function_source(foo)
assert "import _typeshed" in src
assert "def foo() -> _typeshed.OpenTextMode:" in src

0 comments on commit 0b01508

Please sign in to comment.