crates/ty_python_semantic/resources/mdtest/import/conventions.md
This document describes the conventions for importing symbols.
Reference:
When looking up for a name, ty will fallback to using the builtins scope if the name is not found in
the global scope. The builtins.pyi file, that will be used to resolve any symbol in the builtins
scope, contains multiple symbols from other modules (e.g., typing) but those are not re-exported.
# These symbols are being imported in `builtins.pyi` but shouldn't be considered as being
# available in the builtins scope.
# error: "Name `Literal` used when not defined"
reveal_type(Literal) # revealed: Unknown
# error: "Name `sys` used when not defined"
reveal_type(sys) # revealed: Unknown
Similarly, trying to import the symbols from the builtins module which aren't re-exported should also raise an error.
# error: "Module `builtins` has no member `Literal`"
# error: "Module `builtins` has no member `sys`"
from builtins import Literal, sys
reveal_type(Literal) # revealed: Unknown
reveal_type(sys) # revealed: Unknown
# error: "Module `math` has no member `Iterable`"
from math import Iterable
reveal_type(Iterable) # revealed: Unknown
When a symbol is re-exported, importing it should not raise an error. This tests both import ...
and from ... import ... forms.
Note: Submodule imports in import ... form doesn't work because it's a syntax error. For example,
in import os.path as os.path the os.path is not a valid identifier.
from b import Any, Literal, foo
reveal_type(Any) # revealed: <special-form 'typing.Any'>
reveal_type(Literal) # revealed: <special-form 'typing.Literal'>
reveal_type(foo) # revealed: <module 'foo'>
b.pyi:
import foo as foo
from typing import Any as Any, Literal as Literal
foo.py:
Here, none of the symbols are being re-exported in the stub file.
# error: 15 [unresolved-import] "Module `b` has no member `foo`"
# error: 20 [unresolved-import] "Module `b` has no member `Any`"
# error: 25 [unresolved-import] "Module `b` has no member `Literal`"
from b import foo, Any, Literal
reveal_type(Any) # revealed: Unknown
reveal_type(Literal) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
b.pyi:
import foo
from typing import Any, Literal
foo.pyi:
Here, a chain of modules all don't re-export an import.
# error: "Module `a` has no member `Any`"
from a import Any
reveal_type(Any) # revealed: Unknown
a.pyi:
# error: "Module `b` has no member `Any`"
from b import Any
reveal_type(Any) # revealed: Unknown
b.pyi:
# error: "Module `c` has no member `Any`"
from c import Any
reveal_type(Any) # revealed: Unknown
c.pyi:
from typing import Any
reveal_type(Any) # revealed: <special-form 'typing.Any'>
But, if the symbol is being re-exported explicitly in one of the modules in the chain, it should not raise an error at that step in the chain.
# error: "Module `a` has no member `Any`"
from a import Any
reveal_type(Any) # revealed: Unknown
a.pyi:
from b import Any
reveal_type(Any) # revealed: Unknown
b.pyi:
# error: "Module `c` has no member `Any`"
from c import Any as Any
reveal_type(Any) # revealed: Unknown
c.pyi:
from typing import Any
reveal_type(Any) # revealed: <special-form 'typing.Any'>
The re-export convention only works when the aliased name is exactly the same as the original name.
# error: "Module `a` has no member `Foo`"
from a import Foo
reveal_type(Foo) # revealed: Unknown
a.pyi:
from b import AnyFoo as Foo
reveal_type(Foo) # revealed: <class 'AnyFoo'>
b.pyi:
class AnyFoo: ...
__all__Here, the symbol is re-exported using the __all__ variable.
from a import Foo
reveal_type(Foo) # revealed: <class 'Foo'>
a.pyi:
from b import Foo
__all__ = ["Foo"]
b.pyi:
class Foo: ...
__all__If a symbol is re-exported via redundant alias but is not included in __all__, it shouldn't raise
an error when using named import.
named_import.py:
from a import Foo
reveal_type(Foo) # revealed: <class 'Foo'>
a.pyi:
from b import Foo as Foo
__all__ = []
b.pyi:
class Foo: ...
However, a star import would raise an error.
star_import.py:
from a import *
# error: [unresolved-reference] "Name `Foo` used when not defined"
reveal_type(Foo) # revealed: Unknown
__init__.pyiSimilarly, for an __init__.pyi (stub) file, importing a non-exported name should raise an error
but the inference would be Unknown.
# error: 15 "Module `a` has no member `Foo`"
# error: 20 "Module `a` has no member `c`"
from a import Foo, c, foo
reveal_type(Foo) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(foo) # revealed: <module 'a.foo'>
a/__init__.pyi:
from .b import c
from .foo import Foo
a/foo.pyi:
class Foo: ...
a/b/__init__.pyi:
a/b/c.pyi:
The following scenarios are when a re-export happens conditionally in a stub file.
# error: "Member `Foo` of module `a` may be missing"
from a import Foo
reveal_type(Foo) # revealed: str
a.pyi:
from b import Foo
def coinflip() -> bool: ...
if coinflip():
Foo: str = ...
reveal_type(Foo) # revealed: <class 'Foo'> | str
b.pyi:
class Foo: ...
Here, both the branches of the condition are import statements where one of them re-exports while the other does not.
# error: "Member `Foo` of module `a` may be missing"
from a import Foo
reveal_type(Foo) # revealed: <class 'Foo'>
a.pyi:
def coinflip() -> bool: ...
if coinflip():
from b import Foo
else:
from b import Foo as Foo
reveal_type(Foo) # revealed: <class 'Foo'>
b.pyi:
class Foo: ...
# error: "Member `Foo` of module `a` may be missing"
from a import Foo
reveal_type(Foo) # revealed: <class 'Foo'>
a.pyi:
def coinflip() -> bool: ...
if coinflip():
from b import Foo as Foo
b.pyi:
class Foo: ...
# error: "Module `a` has no member `Foo`"
from a import Foo
reveal_type(Foo) # revealed: Unknown
a.pyi:
def coinflip() -> bool: ...
if coinflip():
from b import Foo
b.pyi:
class Foo: ...