Skip to content

Commit 9266102

Browse files
Glyphackcarljm
andauthored
[ty] Add replace-imports-with-any option (#23122)
## Summary Add config to specify set of import paths to be resolved to Any. This config will silence import errors and replace the module with typing.Any. If the module can be found, its type information will still be replaced with typing.Any. Resolves part of astral-sh/ty#2082. I haven't implmented the suggestion to use Dynamic type to have both any and the best guess of the type. I'm thinking to do it in next PR. ## Test Plan Updated mdtest. I didn't add a lot of glob tests since the allowed-unresolved-imports rule already tested that. --------- Co-authored-by: Carl Meyer <carl@astral.sh>
1 parent 2b60370 commit 9266102

9 files changed

Lines changed: 355 additions & 10 deletions

File tree

crates/ty/docs/configuration.md

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty_project/src/metadata/options.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,31 @@ pub struct AnalysisOptions {
13291329
"#
13301330
)]
13311331
pub allowed_unresolved_imports: Option<Vec<RangedValue<String>>>,
1332+
1333+
/// A list of module glob patterns whose imports should be replaced with `typing.Any`.
1334+
///
1335+
/// Unlike `allowed-unresolved-imports`, this setting replaces the module's type information
1336+
/// with `typing.Any` even if the module can be resolved. Import diagnostics are
1337+
/// unconditionally suppressed for matching modules.
1338+
///
1339+
/// - Prefix a pattern with `!` to exclude matching modules
1340+
///
1341+
/// When multiple patterns match, later entries take precedence.
1342+
///
1343+
/// Glob patterns can be used in combinations with each other. For example, to suppress errors for
1344+
/// any module where the first component contains the substring `test`, use `*test*.**`.
1345+
///
1346+
/// When multiple patterns match, later entries take precedence.
1347+
#[serde(skip_serializing_if = "Option::is_none")]
1348+
#[option(
1349+
default = r#"[]"#,
1350+
value_type = "list[str]",
1351+
example = r#"
1352+
# Replace all pandas and numpy imports with Any
1353+
replace-imports-with-any = ["pandas.**", "numpy.**"]
1354+
"#
1355+
)]
1356+
pub replace_imports_with_any: Option<Vec<RangedValue<String>>>,
13321357
}
13331358

13341359
impl AnalysisOptions {
@@ -1340,11 +1365,13 @@ impl AnalysisOptions {
13401365
let Self {
13411366
respect_type_ignore_comments,
13421367
allowed_unresolved_imports,
1368+
replace_imports_with_any,
13431369
} = self;
13441370

13451371
let AnalysisSettings {
13461372
respect_type_ignore_comments: respect_type_ignore_default,
13471373
allowed_unresolved_imports: allowed_unresolved_imports_default,
1374+
replace_imports_with_any: replace_imports_with_any_default,
13481375
} = AnalysisSettings::default();
13491376

13501377
let allowed_unresolved_imports =
@@ -1358,10 +1385,22 @@ impl AnalysisOptions {
13581385
allowed_unresolved_imports_default
13591386
};
13601387

1388+
let replace_imports_with_any =
1389+
if let Some(replace_imports_with_any) = replace_imports_with_any {
1390+
build_module_glob_set(db, replace_imports_with_any, "replace_imports_with_any")
1391+
.unwrap_or_else(|error| {
1392+
diagnostics.push(*error);
1393+
ModuleGlobSet::empty()
1394+
})
1395+
} else {
1396+
replace_imports_with_any_default
1397+
};
1398+
13611399
AnalysisSettings {
13621400
respect_type_ignore_comments: respect_type_ignore_comments
13631401
.unwrap_or(respect_type_ignore_default),
13641402
allowed_unresolved_imports,
1403+
replace_imports_with_any,
13651404
}
13661405
}
13671406
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Replace imports with Any
2+
3+
When a module cannot be found and matches the pattern, the import is replaced with `Any` and no
4+
diagnostic is emitted.
5+
6+
The syntax uses globe patterns. See `allowed-unresolved-imports` for syntax.
7+
8+
## Unresolvable module is replaced with Any
9+
10+
```toml
11+
[analysis]
12+
replace-imports-with-any = ["foo.**"]
13+
```
14+
15+
```py
16+
import foo
17+
from foo import bar
18+
from foo.sub import baz
19+
20+
reveal_type(foo) # revealed: Any
21+
reveal_type(bar) # revealed: Any
22+
reveal_type(baz) # revealed: Any
23+
```
24+
25+
## Resolvable module is also replaced with Any
26+
27+
Even when the module exists and has type information, its types are replaced with `Any`.
28+
29+
```toml
30+
[analysis]
31+
replace-imports-with-any = ["pkg.**"]
32+
```
33+
34+
`pkg/__init__.py`:
35+
36+
```py
37+
x: int = 1
38+
```
39+
40+
`pkg/sub.py`:
41+
42+
```py
43+
y: str = "hello"
44+
```
45+
46+
`main.py`:
47+
48+
```py
49+
from pkg import x
50+
from pkg.sub import y
51+
import pkg
52+
53+
reveal_type(x) # revealed: Any
54+
reveal_type(y) # revealed: Any
55+
reveal_type(pkg) # revealed: Any
56+
```
57+
58+
## Glob Pattern
59+
60+
```toml
61+
[analysis]
62+
replace-imports-with-any = ["aws*.**"]
63+
```
64+
65+
```py
66+
import aws
67+
import awscli
68+
import awscli.customizations
69+
70+
reveal_type(aws) # revealed: Any
71+
reveal_type(awscli) # revealed: Any
72+
reveal_type(awscli.customizations) # revealed: Any
73+
```
74+
75+
## Negative pattern
76+
77+
```toml
78+
[analysis]
79+
replace-imports-with-any = ["pkg.**", "!pkg.keep"]
80+
```
81+
82+
`pkg/__init__.py`:
83+
84+
```py
85+
```
86+
87+
`pkg/keep.py`:
88+
89+
```py
90+
value: int = 1
91+
```
92+
93+
`main.py`:
94+
95+
```py
96+
from pkg.keep import value
97+
from pkg.skip import other
98+
99+
reveal_type(value) # revealed: int
100+
reveal_type(other) # revealed: Any
101+
```
102+
103+
## Relative Imports
104+
105+
The match happens on the module absolute path. If the absolute path that a relative import is
106+
pointing to matches the condition it will be applied.
107+
108+
```toml
109+
[analysis]
110+
replace-imports-with-any = ["**.foo", "bar"]
111+
```
112+
113+
`package/__init__.py`:
114+
115+
```py
116+
```
117+
118+
`package/foo.py`:
119+
120+
```py
121+
val = 1
122+
```
123+
124+
`package/main.py`:
125+
126+
```py
127+
from .foo import val
128+
129+
# .bar would not match "bar" rule because the absolute import is package.bar
130+
from .bar import val2 # error: [unresolved-import]
131+
132+
reveal_type(val) # revealed: Any
133+
```
134+
135+
## Non-matching modules are unaffected
136+
137+
```toml
138+
[analysis]
139+
replace-imports-with-any = ["skipped.**"]
140+
```
141+
142+
`real_module.py`:
143+
144+
```py
145+
value: int = 42
146+
```
147+
148+
`main.py`:
149+
150+
```py
151+
from real_module import value
152+
153+
reveal_type(value) # revealed: int
154+
```

crates/ty_python_semantic/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,16 @@ pub struct AnalysisSettings {
8989
pub respect_type_ignore_comments: bool,
9090

9191
pub allowed_unresolved_imports: ModuleGlobSet,
92+
93+
pub replace_imports_with_any: ModuleGlobSet,
9294
}
9395

9496
impl Default for AnalysisSettings {
9597
fn default() -> Self {
9698
Self {
9799
respect_type_ignore_comments: true,
98100
allowed_unresolved_imports: ModuleGlobSet::empty(),
101+
replace_imports_with_any: ModuleGlobSet::empty(),
99102
}
100103
}
101104
}

0 commit comments

Comments
 (0)