Skip to content

Commit d9fe996

Browse files
authored
[ty] Support custom builtins (#22021)
1 parent 5ea30c4 commit d9fe996

3 files changed

Lines changed: 96 additions & 21 deletions

File tree

crates/ty_python_semantic/resources/mdtest/import/builtins.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,63 @@ def reveal_type(obj, /): ...
7676
```py
7777
reveal_type(foo) # revealed: Unknown
7878
```
79+
80+
## Builtins imported from custom project-level stubs
81+
82+
The project can add or replace builtins with the `__builtins__.pyi` stub. They will take precedence
83+
over the typeshed ones.
84+
85+
```py
86+
reveal_type(foo) # revealed: int
87+
reveal_type(bar) # revealed: str
88+
reveal_type(quux(1)) # revealed: int
89+
b = baz # error: [unresolved-reference]
90+
91+
reveal_type(ord(100)) # revealed: bool
92+
a = ord("a") # error: [invalid-argument-type]
93+
94+
bar = int(123)
95+
reveal_type(bar) # revealed: int
96+
```
97+
98+
`__builtins__.pyi`:
99+
100+
```pyi
101+
foo: int = ...
102+
bar: str = ...
103+
104+
def quux(value: int) -> int: ...
105+
106+
unused: str = ...
107+
108+
def ord(x: int) -> bool: ...
109+
```
110+
111+
Builtins stubs are searched relative to the project root, not the file using them.
112+
113+
`under/some/folder.py`:
114+
115+
```py
116+
reveal_type(foo) # revealed: int
117+
reveal_type(bar) # revealed: str
118+
```
119+
120+
## Assigning custom builtins
121+
122+
```py
123+
import builtins
124+
125+
builtins.foo = 123
126+
builtins.bar = 456 # error: [unresolved-attribute]
127+
builtins.baz = 789 # error: [invalid-assignment]
128+
builtins.chr = lambda x: str(x) # error: [invalid-assignment]
129+
builtins.chr = 10
130+
```
131+
132+
`__builtins__.pyi`:
133+
134+
```pyi
135+
foo: int
136+
baz: str
137+
chr: int
138+
```

crates/ty_python_semantic/src/place.rs

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use ruff_db::files::File;
22
use ruff_python_ast::PythonVersion;
3-
use ty_module_resolver::{KnownModule, file_to_module, resolve_module_confident};
3+
use ty_module_resolver::{
4+
KnownModule, Module, ModuleName, file_to_module, resolve_module_confident,
5+
};
46

57
use crate::dunder_all::dunder_all_names;
68
use crate::semantic_index::definition::{Definition, DefinitionState};
@@ -380,25 +382,29 @@ pub(crate) fn imported_symbol<'db>(
380382
/// and should not be used when a symbol is being explicitly imported from the `builtins` module
381383
/// (e.g. `from builtins import int`).
382384
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> PlaceAndQualifiers<'db> {
383-
resolve_module_confident(db, &KnownModule::Builtins.name())
384-
.and_then(|module| {
385-
let file = module.file(db)?;
386-
Some(
387-
symbol_impl(
388-
db,
389-
global_scope(db, file),
390-
symbol,
391-
RequiresExplicitReExport::Yes,
392-
ConsideredDefinitions::EndOfScope,
393-
)
394-
.or_fall_back_to(db, || {
395-
// We're looking up in the builtins namespace and not the module, so we should
396-
// do the normal lookup in `types.ModuleType` and not the special one as in
397-
// `imported_symbol`.
398-
module_type_implicit_global_symbol(db, symbol)
399-
}),
400-
)
401-
})
385+
let resolver = |module: Module<'_>| {
386+
let file = module.file(db)?;
387+
let found_symbol = symbol_impl(
388+
db,
389+
global_scope(db, file),
390+
symbol,
391+
RequiresExplicitReExport::Yes,
392+
ConsideredDefinitions::EndOfScope,
393+
)
394+
.or_fall_back_to(db, || {
395+
// We're looking up in the builtins namespace and not the module, so we should
396+
// do the normal lookup in `types.ModuleType` and not the special one as in
397+
// `imported_symbol`.
398+
module_type_implicit_global_symbol(db, symbol)
399+
});
400+
// If this symbol is not present in project-level builtins, search in the default ones.
401+
found_symbol
402+
.ignore_possibly_undefined()
403+
.map(|_| found_symbol)
404+
};
405+
resolve_module_confident(db, &ModuleName::new_static("__builtins__").unwrap())
406+
.and_then(&resolver)
407+
.or_else(|| resolve_module_confident(db, &KnownModule::Builtins.name()).and_then(resolver))
402408
.unwrap_or_default()
403409
}
404410

crates/ty_python_semantic/src/types/infer/builder.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4963,7 +4963,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
49634963
}
49644964

49654965
Type::ModuleLiteral(module) => {
4966-
if let Place::Defined(attr_ty, _, _) = module.static_member(db, attribute).place {
4966+
let sym = if module
4967+
.module(db)
4968+
.known(db)
4969+
.is_some_and(KnownModule::is_builtins)
4970+
{
4971+
builtins_symbol(db, attribute)
4972+
} else {
4973+
module.static_member(db, attribute)
4974+
};
4975+
if let Place::Defined(attr_ty, _, _) = sym.place {
49674976
let value_ty = infer_value_ty(self, TypeContext::new(Some(attr_ty)));
49684977

49694978
let assignable = value_ty.is_assignable_to(db, attr_ty);

0 commit comments

Comments
 (0)