6.2 KiB
Import conventions
This document describes the conventions for importing symbols.
Reference:
Builtins scope
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
Builtins import
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
Re-exported symbols in stub files
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: typing.Any
reveal_type(Literal) # revealed: 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
:
Non-exported symbols in stub files
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
:
Nested non-exports
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: typing.Any
Nested mixed re-export and not
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: typing.Any
Exported as different name
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: Literal[AnyFoo]
b.pyi
:
class AnyFoo: ...
Exported using __all__
Here, the symbol is re-exported using the __all__
variable.
# TODO: This should *not* be an error but we don't understand `__all__` yet.
# error: "Module `a` has no member `Foo`"
from a import Foo
a.pyi
:
from b import Foo
__all__ = ['Foo']
b.pyi
:
class Foo: ...
Re-exports in __init__.pyi
Similarly, 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
:
Conditional re-export in stub file
The following scenarios are when a re-export happens conditionally in a stub file.
Global import
# error: "Member `Foo` of module `a` is possibly unbound"
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: Literal[Foo] | str
b.pyi
:
class Foo: ...
Both branch is an import
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` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: Literal[Foo]
a.pyi
:
def coinflip() -> bool: ...
if coinflip():
from b import Foo
else:
from b import Foo as Foo
reveal_type(Foo) # revealed: Literal[Foo]
b.pyi
:
class Foo: ...
Re-export in one branch
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: Literal[Foo]
a.pyi
:
def coinflip() -> bool: ...
if coinflip():
from b import Foo as Foo
b.pyi
:
class Foo: ...
Non-export in one branch
# 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: ...