# 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. ```py # 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. ```py # 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. ```py from b import Any, Literal, foo reveal_type(Any) # revealed: typing.Any reveal_type(Literal) # revealed: typing.Literal reveal_type(foo) # revealed: ``` `b.pyi`: ```pyi import foo as foo from typing import Any as Any, Literal as Literal ``` `foo.py`: ```py ``` ## Non-exported symbols in stub files Here, none of the symbols are being re-exported in the stub file. ```py # 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`: ```pyi import foo from typing import Any, Literal ``` `foo.pyi`: ```pyi ``` ## Nested non-exports Here, a chain of modules all don't re-export an import. ```py # error: "Module `a` has no member `Any`" from a import Any reveal_type(Any) # revealed: Unknown ``` `a.pyi`: ```pyi # error: "Module `b` has no member `Any`" from b import Any reveal_type(Any) # revealed: Unknown ``` `b.pyi`: ```pyi # error: "Module `c` has no member `Any`" from c import Any reveal_type(Any) # revealed: Unknown ``` `c.pyi`: ```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. ```py # error: "Module `a` has no member `Any`" from a import Any reveal_type(Any) # revealed: Unknown ``` `a.pyi`: ```pyi from b import Any reveal_type(Any) # revealed: Unknown ``` `b.pyi`: ```pyi # error: "Module `c` has no member `Any`" from c import Any as Any reveal_type(Any) # revealed: Unknown ``` `c.pyi`: ```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. ```py # error: "Module `a` has no member `Foo`" from a import Foo reveal_type(Foo) # revealed: Unknown ``` `a.pyi`: ```pyi from b import AnyFoo as Foo reveal_type(Foo) # revealed: ``` `b.pyi`: ```pyi class AnyFoo: ... ``` ## Exported using `__all__` Here, the symbol is re-exported using the `__all__` variable. ```py from a import Foo reveal_type(Foo) # revealed: ``` `a.pyi`: ```pyi from b import Foo __all__ = ['Foo'] ``` `b.pyi`: ```pyi class Foo: ... ``` ## Re-exports with `__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`: ```py from a import Foo reveal_type(Foo) # revealed: ``` `a.pyi`: ```pyi from b import Foo as Foo __all__ = [] ``` `b.pyi`: ```pyi class Foo: ... ``` However, a star import _would_ raise an error. `star_import.py`: ```py from a import * # error: [unresolved-reference] "Name `Foo` used when not defined" reveal_type(Foo) # revealed: Unknown ``` ## 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`. ```py # 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: ``` `a/__init__.pyi`: ```pyi from .b import c from .foo import Foo ``` `a/foo.pyi`: ```pyi class Foo: ... ``` `a/b/__init__.pyi`: ```pyi ``` `a/b/c.pyi`: ```pyi ``` ## Conditional re-export in stub file The following scenarios are when a re-export happens conditionally in a stub file. ### Global import ```py # error: "Member `Foo` of module `a` is possibly unbound" from a import Foo reveal_type(Foo) # revealed: str ``` `a.pyi`: ```pyi from b import Foo def coinflip() -> bool: ... if coinflip(): Foo: str = ... reveal_type(Foo) # revealed: | str ``` `b.pyi`: ```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. ```py # error: "Member `Foo` of module `a` is possibly unbound" from a import Foo reveal_type(Foo) # revealed: ``` `a.pyi`: ```pyi def coinflip() -> bool: ... if coinflip(): from b import Foo else: from b import Foo as Foo reveal_type(Foo) # revealed: ``` `b.pyi`: ```pyi class Foo: ... ``` ### Re-export in one branch ```py # error: "Member `Foo` of module `a` is possibly unbound" from a import Foo reveal_type(Foo) # revealed: ``` `a.pyi`: ```pyi def coinflip() -> bool: ... if coinflip(): from b import Foo as Foo ``` `b.pyi`: ```pyi class Foo: ... ``` ### Non-export in one branch ```py # error: "Module `a` has no member `Foo`" from a import Foo reveal_type(Foo) # revealed: Unknown ``` `a.pyi`: ```pyi def coinflip() -> bool: ... if coinflip(): from b import Foo ``` `b.pyi`: ```pyi class Foo: ... ```