ruff/crates/ty_python_semantic/resources/mdtest/import/conventions.md
2025-05-03 19:49:15 +02:00

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: ...