From cf66e905fe557f882063495619b3223d1ca0a681 Mon Sep 17 00:00:00 2001 From: Andrey Vokin Date: Tue, 2 Jun 2026 10:56:50 +0200 Subject: [PATCH] typeshed_path: fully replace Pyrefly's bundled stubs, settable over LSP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A configured `typeshed_path` only partially redirected resolution, so a user who pointed Pyrefly at their own typeshed still got results — and the declaration URIs returned over LSP/TSP — out of Pyrefly's bundled stubs: - third-party imports always resolved against the bundled stubs; - the bundled stdlib was used as a fallback when a module was missing from `/stdlib`; - most importantly, the `Stdlib` object (which supplies the class definitions for `int`, `str`, `list`, ... used throughout checking) was *always* loaded via a hardcoded bundled-typeshed config, so builtins were bundled even when imports resolved to the custom typeshed; - and `typeshed_path` could only be set from a `pyrefly.toml`, leaving editors no way to point Pyrefly at their typeshed without writing a file. Treat `typeshed_path` as a complete replacement for the bundled stubs. When it is set: - third-party stubs resolve only from `/stubs` (each distribution subdirectory is its own search root, mirroring typeshed's layout), and the bundled stdlib branch is skipped — a miss falls through to the remaining stages (site-packages, ...) and never reaches the bundled stubs; - the stdlib loader config carries `typeshed_path`, so `compute_stdlib` builds the `Stdlib` from `/stdlib` too. `invalidate_config` drops the cached stdlib so a changed `typeshed_path` is picked up. Because the setting now suffices on its own, no separate opt-out knob is needed. Also surface `typeshed_path` over the LSP `initializationOptions` / `workspace/configuration` channel (as `typeshedPath`), deferring to an explicit value from a user-owned config file. --- pyrefly/lib/lsp/non_wasm/workspace.rs | 79 +++++++++++++++ pyrefly/lib/module/finder.rs | 137 +++++++++++++++++++++++++- pyrefly/lib/module/typeshed.rs | 46 +++++++-- pyrefly/lib/state/state.rs | 25 ++++- pyrefly/lib/test/state.rs | 80 +++++---------- 5 files changed, 297 insertions(+), 70 deletions(-) diff --git a/pyrefly/lib/lsp/non_wasm/workspace.rs b/pyrefly/lib/lsp/non_wasm/workspace.rs index dfc7e01e73..37562a6549 100644 --- a/pyrefly/lib/lsp/non_wasm/workspace.rs +++ b/pyrefly/lib/lsp/non_wasm/workspace.rs @@ -85,6 +85,11 @@ pub struct Workspace { pub stream_diagnostics: Option, pub diagnostic_mode: Option, pub workspace_config: Option, + /// Custom typeshed override supplied over LSP. Equivalent to the + /// `typeshed_path` field of a `pyrefly.toml`, but settable via + /// `initializationOptions`/`workspace/configuration` so editor integrations + /// can point Pyrefly at their own typeshed without writing a config file. + pub typeshed_path: Option, } impl Workspace { @@ -172,6 +177,13 @@ impl ConfigConfigurer for WorkspaceConfigConfigurer { // skip interpreter query because we already have the interpreter from the workspace config.interpreters.skip_interpreter_query = true; } + // Workspace-level `typeshed_path` from LSP overrides the bundled typeshed + // but defers to an explicit value already set by a config file the user owns. + if let Some(typeshed_path) = w.typeshed_path.clone() + && config.typeshed_path.is_none() + { + config.typeshed_path = Some(typeshed_path); + } }) }; @@ -252,6 +264,10 @@ struct PyreflyClientConfig { disabled_language_services: Option, stream_diagnostics: Option, config_path: Option, + /// Override the bundled typeshed with a custom path. Equivalent to the + /// `typeshed_path` field of a `pyrefly.toml`, but settable over the LSP + /// `initializationOptions`/`workspace/configuration` channel. + typeshed_path: Option, } #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)] @@ -544,6 +560,7 @@ impl Workspaces { // so a partial `did_change_configuration` touching some other // key won't pay for a needless recheck. self.update_display_type_errors(modified, scope_uri, pyrefly.display_type_errors); + self.update_typeshed_path(modified, scope_uri, pyrefly.typeshed_path); self.update_type_checking_mode( modified, scope_uri, @@ -656,6 +673,37 @@ impl Workspaces { } /// Update displayTypeErrors setting for scope_uri, None if default workspace + /// Update [`Workspace::typeshed_path`] for `scope_uri`, or the default workspace + /// when `scope_uri` is `None`. `modified` is set only when the value actually + /// changes, so a partial payload re-stating the current value doesn't trigger a + /// spurious recheck. + fn update_typeshed_path( + &self, + modified: &mut bool, + scope_uri: &Option, + typeshed_path: Option, + ) { + let mut workspaces = self.workspaces.write(); + match scope_uri { + Some(scope_uri) => { + if let Ok(path) = scope_uri.to_file_path() + && let Some(workspace) = workspaces.get_mut(&path) + && workspace.typeshed_path != typeshed_path + { + *modified = true; + workspace.typeshed_path = typeshed_path; + } + } + None => { + let mut default = self.default.write(); + if default.typeshed_path != typeshed_path { + *modified = true; + default.typeshed_path = typeshed_path; + } + } + } + } + fn update_display_type_errors( &self, modified: &mut bool, @@ -1093,6 +1141,37 @@ mod tests { assert!(config.pyrefly.is_some()); } + #[test] + fn test_pyrefly_typeshed_path_parsed_from_lsp_config() { + let lsp_config: LspConfig = serde_json::from_value(json!({ + "pyrefly": { "typeshedPath": "/path/to/custom/typeshed" } + })) + .expect("LspConfig should parse"); + assert_eq!( + lsp_config.pyrefly.expect("pyrefly section").typeshed_path, + Some(PathBuf::from("/path/to/custom/typeshed")) + ); + } + + #[test] + fn test_pyrefly_typeshed_path_applied_to_default_workspace() { + let workspaces = Workspaces::new(Workspace::new(), &[]); + let mut modified = false; + workspaces.apply_client_configuration( + &mut modified, + &None, + json!({ "pyrefly": { "typeshedPath": "/path/to/custom/typeshed" } }), + ); + assert!( + modified, + "applying typeshed_path should mark workspace modified" + ); + assert_eq!( + workspaces.default.read().typeshed_path, + Some(PathBuf::from("/path/to/custom/typeshed")) + ); + } + /// Legacy `displayTypeErrors` maps onto the two new axes: /// `force-on` sets the typeCheckingMode to Default and is a no-op /// on the kill switch; `force-off` sets the kill switch and is a diff --git a/pyrefly/lib/module/finder.rs b/pyrefly/lib/module/finder.rs index e9df6580af..52838cb1df 100644 --- a/pyrefly/lib/module/finder.rs +++ b/pyrefly/lib/module/finder.rs @@ -5,6 +5,7 @@ * LICENSE file in the root directory of this source tree. */ +use std::collections::HashMap; use std::ffi::OsString; use std::fmt::Debug; use std::io::Read; @@ -20,6 +21,7 @@ use pyrefly_python::COMPILED_FILE_SUFFIXES; use pyrefly_python::module_name::ModuleName; use pyrefly_python::module_path::ModulePath; use pyrefly_python::module_path::ModuleStyle; +use pyrefly_util::lock::Mutex; use pyrefly_util::locked_map::LockedMap; use pyrefly_util::suggest::best_suggestion; use regex::Regex; @@ -1115,14 +1117,79 @@ fn find_extra_extension_module<'a>( None } +/// Per-typeshed cache of the immediate subdirectories of `/stubs`. Each +/// subdirectory is a typeshed-style stub distribution (`stubs/Markdown/`, +/// `stubs/pandas-stubs/`, ...), and we treat each of them as its own search root, +/// mirroring typeshed's third-party layout. +static CUSTOM_TYPESHED_STUB_PKG_DIRS: LazyLock>>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +fn custom_typeshed_stub_pkg_dirs(typeshed_root: &Path) -> Arc> { + let stubs_root = typeshed_root.join("stubs"); + let mut cache = CUSTOM_TYPESHED_STUB_PKG_DIRS.lock(); + if let Some(cached) = cache.get(&stubs_root) { + return cached.clone(); + } + let pkgs: Vec = std::fs::read_dir(&stubs_root) + .ok() + .into_iter() + .flatten() + .filter_map(Result::ok) + .filter_map(|entry| { + let path = entry.path(); + if path.is_dir() { Some(path) } else { None } + }) + .collect(); + let arc = Arc::new(pkgs); + cache.insert(stubs_root, arc.clone()); + arc +} + +fn find_in_custom_typeshed_stubs( + typeshed_root: &Path, + module: ModuleName, + style_filter: Option, +) -> Option> { + let pkg_dirs = custom_typeshed_stub_pkg_dirs(typeshed_root); + if pkg_dirs.is_empty() { + return None; + } + // Namespaces and phantom-paths discovered while scanning typeshed stubs are + // discarded: typeshed stub packages aren't expected to form namespace packages + // and they are not surfaced to the outer `find_import_internal` loop anyway. + let mut namespaces_found = Vec::new(); + let mut phantom_paths: Option<&mut Vec> = None; + let dir_cache = DirEntryCache::new(false); + find_module( + module, + pkg_dirs.iter(), + &mut namespaces_found, + style_filter, + None, + false, + &mut phantom_paths, + &dir_cache, + None, + ) +} + /// This function will find either third party typeshed stubs or other third party stubs /// Here a decision is being made to prioritize typeshed stubs over other third party stubs that are bundled. /// Since we run the typeshed update script with a more regular cadence, it is more likely that /// these stubs will be more up to date. fn find_third_party_stub( + config: &ConfigFile, module: ModuleName, style_filter: Option, ) -> Option> { + // A configured `typeshed_path` fully replaces Pyrefly's bundled stubs: third-party + // stubs are resolved only from `/stubs`, and a miss returns `None` + // so that later `find_import_internal` stages (site-packages, ...) take over. We + // never fall back to the bundled typeshed/Meta stubs. + if let Some(custom_typeshed_path) = &config.typeshed_path { + return find_in_custom_typeshed_stubs(custom_typeshed_path, module, style_filter); + } + let third_party_typeshed_stub = if matches!(style_filter, Some(ModuleStyle::Interface) | None) { typeshed_third_party().map_or_else( |err| { @@ -1187,7 +1254,7 @@ pub fn find_import_internal( ) -> FindingOrError { let mut namespaces_found = vec![]; let origin = origin.map(|p| p.as_path()); - let typeshed_third_party_result = find_third_party_stub(module, style_filter); + let typeshed_third_party_result = find_third_party_stub(config, module, style_filter); let typeshed_third_party_stub = typeshed_third_party_result.clone(); let from_real_config_file = config.from_real_config_file(); @@ -1237,7 +1304,8 @@ pub fn find_import_internal( ) { path - } else if matches!(style_filter, Some(ModuleStyle::Interface) | None) + } else if config.typeshed_path.is_none() + && matches!(style_filter, Some(ModuleStyle::Interface) | None) && let Some(path) = typeshed().map_or_else( |err| { Some(FindingOrError::Error(FindError::missing_import( @@ -1247,6 +1315,9 @@ pub fn find_import_internal( |ts| ts.find(module).map(FindingOrError::new_finding), ) { + // Skip the bundled typeshed stdlib entirely when the user pointed us at their + // own typeshed: a configured `typeshed_path` is consulted above and must never + // fall back to Pyrefly's bundled stubs. path } else if !config.disable_search_path_heuristics && let Some(path) = find_module( @@ -4262,7 +4333,8 @@ mod tests { fn test_find_third_party_stub_prioritizes_typeshed_over_bundled() { // 'requests' exists in typeshed third party stubs, so it should // return BundledTypeshedThirdParty (typeshed is prioritized over other bundled stubs) - let result = find_third_party_stub(ModuleName::from_str("requests"), None); + let config = get_config(ConfigSource::Synthetic); + let result = find_third_party_stub(&config, ModuleName::from_str("requests"), None); assert!(result.is_some(), "Should find 'requests' stub"); if let Some(FindingOrError::Finding(finding)) = result { @@ -4283,7 +4355,8 @@ mod tests { fn test_find_third_party_stub_returns_bundled_when_not_in_typeshed() { // 'conans' exists only in BundledThirdParty (from third_party/stubs/conans-stubs), // not in typeshed, so it should return BundledThirdParty - let result = find_third_party_stub(ModuleName::from_str("conans"), None); + let config = get_config(ConfigSource::Synthetic); + let result = find_third_party_stub(&config, ModuleName::from_str("conans"), None); assert!(result.is_some(), "Should find 'conans' stub"); if let Some(FindingOrError::Finding(finding)) = result { @@ -4300,6 +4373,62 @@ mod tests { } } + #[test] + fn test_find_third_party_stub_skips_bundled_when_typeshed_path_set() { + // `conans` (Meta-bundled) and `requests` (bundled typeshed) both resolve via + // the bundled stubs by default. Once `typeshed_path` points at an empty custom + // typeshed, neither bundled source is consulted, so both must miss. + let mut config = get_config(ConfigSource::Synthetic); + assert!( + find_third_party_stub(&config, ModuleName::from_str("conans"), None).is_some(), + "Sanity check: 'conans' resolves via bundled by default" + ); + assert!( + find_third_party_stub(&config, ModuleName::from_str("requests"), None).is_some(), + "Sanity check: 'requests' resolves via bundled typeshed by default" + ); + + let tmp = tempfile::tempdir().expect("create tempdir"); + std::fs::create_dir_all(tmp.path().join("stubs")).expect("create empty stubs dir"); + config.typeshed_path = Some(tmp.path().to_path_buf()); + assert!( + find_third_party_stub(&config, ModuleName::from_str("conans"), None).is_none(), + "Bundled Meta stubs must be skipped when typeshed_path is set" + ); + assert!( + find_third_party_stub(&config, ModuleName::from_str("requests"), None).is_none(), + "Bundled typeshed third-party stubs must also be skipped when typeshed_path is set" + ); + } + + #[test] + fn test_find_third_party_stub_prefers_custom_typeshed() { + // Build a typeshed-style directory on disk with one stub package and verify + // that `config.typeshed_path` makes Pyrefly resolve modules out of it instead + // of falling through to the bundled typeshed/third-party stubs. + let tmp = tempfile::tempdir().expect("create tempdir"); + let stubs_root = tmp.path().join("stubs"); + let pkg = stubs_root.join("custom_pkg"); + std::fs::create_dir_all(pkg.join("custom_pkg")).expect("create pkg dirs"); + std::fs::write(pkg.join("custom_pkg/__init__.pyi"), "X: int = 0\n").expect("write stub"); + + let mut config = get_config(ConfigSource::Synthetic); + config.typeshed_path = Some(tmp.path().to_path_buf()); + + let result = find_third_party_stub(&config, ModuleName::from_str("custom_pkg"), None) + .expect("custom typeshed lookup hits"); + match result { + FindingOrError::Finding(finding) => { + assert!( + matches!(finding.finding.details(), ModulePathDetails::FileSystem(_)), + "Expected FileSystem path for custom typeshed, got: {:?}", + finding.finding.details() + ); + } + other => panic!("Expected Finding from custom typeshed, got: {:?}", other), + } + } + #[test] fn test_suggest_stdlib_import() { // Test that we suggest 'math' for 'mathh' (one character typo) diff --git a/pyrefly/lib/module/typeshed.rs b/pyrefly/lib/module/typeshed.rs index 4f5d884805..98cfcd1b27 100644 --- a/pyrefly/lib/module/typeshed.rs +++ b/pyrefly/lib/module/typeshed.rs @@ -78,21 +78,49 @@ impl BundledStub for BundledTypeshedStdlib { Some(path) => vec![path], None => Vec::new(), }; - let error_overrides = HashMap::from([ - // The stdlib is full of deliberately incorrect overrides, so ignore them - (ErrorKind::BadOverride, Severity::Ignore), - (ErrorKind::BadOverrideParamName, Severity::Ignore), - // The stdlib has variance violations in typing.pyi, so ignore them - (ErrorKind::InvalidVariance, Severity::Ignore), - ]); - let config_file = - create_bundled_stub_config(Some(search_paths), Some(error_overrides), Some(true)); + let config_file = create_bundled_stub_config( + Some(search_paths), + Some(stdlib_error_overrides()), + Some(true), + ); ArcId::new(config_file) }); CONFIG.dupe() } } +/// The stdlib is full of deliberately incorrect overrides and variance violations +/// (e.g. in `typing.pyi`), so silence those error kinds when type-checking it. +fn stdlib_error_overrides() -> HashMap { + HashMap::from([ + (ErrorKind::BadOverride, Severity::Ignore), + (ErrorKind::BadOverrideParamName, Severity::Ignore), + (ErrorKind::InvalidVariance, Severity::Ignore), + ]) +} + +/// Loader config used to resolve and type-check the stdlib. +/// +/// With no `typeshed_path`, the bundled stdlib snapshot is used. When a +/// `typeshed_path` is configured, the returned config carries it so that the +/// normal finder branch resolves stdlib modules out of `/stdlib` +/// and never falls back to the bundled snapshot — making a configured typeshed a +/// complete replacement for Pyrefly's bundled stdlib, just like third-party stubs. +pub fn stdlib_config(typeshed_path: Option<&Path>) -> ArcId { + match typeshed_path { + None => BundledTypeshedStdlib::config(), + Some(typeshed_path) => { + // `typeshed_path` is already absolute (read from an + // already-configured `ConfigFile`), so it needs no further + // absolutization after `create_bundled_stub_config` configures. + let mut config_file = + create_bundled_stub_config(None, Some(stdlib_error_overrides()), Some(true)); + config_file.typeshed_path = Some(typeshed_path.to_path_buf()); + ArcId::new(config_file) + } + } +} + static BUNDLED_TYPESHED: LazyLock> = LazyLock::new(BundledTypeshedStdlib::new); diff --git a/pyrefly/lib/state/state.rs b/pyrefly/lib/state/state.rs index 281bcb8378..370cd71bcb 100644 --- a/pyrefly/lib/state/state.rs +++ b/pyrefly/lib/state/state.rs @@ -115,6 +115,7 @@ use crate::export::special::SpecialExport; use crate::module::bundled::BundledStub; use crate::module::finder::find_import_prefixes; use crate::module::typeshed::BundledTypeshedStdlib; +use crate::module::typeshed::stdlib_config; use crate::solver::solver::VarRecurser; use crate::state::epoch::Epoch; use crate::state::errors::Errors; @@ -1886,7 +1887,11 @@ impl<'a> Transaction<'a> { /// redundant single-threaded work on rechecks and multi-epoch runs. /// /// Returns `true` if all entries were already cached (no work done). - fn compute_stdlib(&mut self, sys_infos: SmallSet) -> bool { + fn compute_stdlib( + &mut self, + sys_infos: SmallSet, + typeshed_path: Option<&Path>, + ) -> bool { // Filter out SysInfos that already have a computed stdlib. let missing: SmallSet = sys_infos .into_iter() @@ -1895,7 +1900,9 @@ impl<'a> Transaction<'a> { if missing.is_empty() { return true; } - let loader = self.get_cached_loader(&BundledTypeshedStdlib::config()); + // The stdlib is loaded from the configured `typeshed_path` when one is set, + // so a custom typeshed fully replaces the bundled stdlib (see `stdlib_config`). + let loader = self.get_cached_loader(&stdlib_config(typeshed_path)); // Use defaults (disabled) for stdlib - depth limiting is for user code let thread_state = ThreadState::new(None); for k in missing.into_iter_hashed() { @@ -2042,8 +2049,14 @@ impl<'a> Transaction<'a> { .iter() .map(|x| x.sys_info().dupe()) .collect::>(); + // The stdlib loader honors `typeshed_path` so a custom typeshed replaces the + // bundled stdlib. typeshed_path is effectively uniform across a project, so we + // use the first handle that has one configured. + let typeshed_path = handles + .iter() + .find_map(|h| self.data.state.get_config(h).typeshed_path.clone()); let stdlib_start = Instant::now(); - let stdlib_cached = self.compute_stdlib(sys_infos); + let stdlib_cached = self.compute_stdlib(sys_infos, typeshed_path.as_deref()); let compute_stdlib_time = stdlib_start.elapsed(); { let mut stats = self.stats.lock(); @@ -2264,6 +2277,12 @@ impl<'a> Transaction<'a> { // This is reasonable, because we will cache the result on ModuleData. self.data.state.config_finder.clear(); + // The stdlib loader config depends on `typeshed_path`, which this config + // change may have altered. Drop the cached stdlib so `compute_stdlib` + // rebuilds it against the new config rather than serving a stale (possibly + // bundled) snapshot. + self.data.stdlib.clear(); + // Wipe the copy of ConfigFile on each module that has changed. // If they change, set find to dirty. let mut dirty_set = self.data.dirty.lock(); diff --git a/pyrefly/lib/test/state.rs b/pyrefly/lib/test/state.rs index 547db241ff..72b990409c 100644 --- a/pyrefly/lib/test/state.rs +++ b/pyrefly/lib/test/state.rs @@ -913,54 +913,39 @@ fn test_search_exports_cancellation() { ); } +// Symlinking the materialized bundled stdlib into `/stdlib` is the simplest +// way to get a *complete* custom typeshed (the `Stdlib` bootstrap needs the full set +// of stdlib classes); symlink is unix-only. +#[cfg(unix)] #[test] -fn test_compute_stdlib_uses_bundled_typeshed_even_with_custom_path() { +fn test_compute_stdlib_uses_custom_typeshed_path() { use std::fs; use tempfile::TempDir; + use crate::module::bundled::BundledStub; + use crate::module::typeshed::typeshed; + + // Build a custom typeshed whose `stdlib` points at the (complete) bundled stdlib, + // but at a *different* on-disk location than the bundled snapshot Pyrefly uses by + // default. Previously the `Stdlib` object (which supplies the class definitions for + // `int`, `str`, ... used during checking) was always loaded from the bundled + // typeshed, while the `int` annotation resolved from `typeshed_path`. Those were + // different classes, so `x: int = 1` spuriously failed with a Literal[1]/int + // mismatch. With the stdlib also loaded from `typeshed_path`, both are the same + // class and there is no error. + let bundled_stdlib = typeshed().unwrap().materialized_path_on_disk().unwrap(); let temp_dir = TempDir::new().unwrap(); let typeshed_path = temp_dir.path().join("custom_typeshed"); - let stdlib_path = typeshed_path.join("stdlib"); - fs::create_dir_all(&stdlib_path).unwrap(); - - // Create a minimal builtins.pyi that defines int as a class, but very minimally. - // The key insight: Stdlib object loads built-in types (int, str, etc.) and their - // class definitions for type-checking. Since our custom typeshed's int is used - // for imports but bundled typeshed's int is used for Stdlib, we get a type mismatch. - let builtins_content = r#" -class object: ... -class type: ... -class int: - # This is a minimal int that doesn't have many methods - # The bundled typeshed has a full implementation - pass -class str: ... -class bool(int): ... -class float: ... -class list: ... -class dict: ... -class tuple: ... -class None: ... -"#; - fs::write(stdlib_path.join("builtins.pyi"), builtins_content).unwrap(); - - fs::write(stdlib_path.join("VERSIONS"), "builtins: 3.0-\n").unwrap(); + fs::create_dir_all(&typeshed_path).unwrap(); + std::os::unix::fs::symlink(&bundled_stdlib, typeshed_path.join("stdlib")).unwrap(); let mut config = ConfigFile::default(); config.python_environment.set_empty_to_default(); config.typeshed_path = Some(typeshed_path.clone()); let sys_info = config.get_sys_info(); - // Create a test module. The bug manifests as follows: - // - The `int` annotation is resolved from the custom typeshed - // - The Stdlib's int (used for Literal[1]) comes from bundled typeshed - // - These are different types, causing "Literal[1] is not assignable to int" let test_code = r#" -# This simple assignment demonstrates the bug: -# - The `int` type annotation is resolved from custom typeshed (builtins.int@4:7-10) -# - The Literal[1] type comes from bundled Stdlib (builtins.int@420:7-10) -# - Since these are different types, we get a type error x: int = 1 "#; let module_name = ModuleName::from_str("test_module"); @@ -986,32 +971,19 @@ x: int = 1 transaction.run(&[handle.dupe()], Require::Everything, None); let errors = transaction.get_errors([&handle]).collect_errors(); - - assert!( - config.typeshed_path.is_some(), - "Test setup error: typeshed_path should be set in config" - ); - assert_eq!( - config.typeshed_path.as_ref().unwrap(), - &typeshed_path, - "Test setup error: typeshed_path should match the custom path" - ); - - // Verify the specific error is the expected type mismatch. This is specific - // error is an indication that we are not using the typeshed bundled with Pyrefly - // and not the typeshed provided through the config. let error_messages: Vec = errors .ordinary .iter() .map(|e| e.msg().to_string()) .collect(); - let has_literal_int_error = error_messages - .iter() - .any(|msg| msg.contains("Literal[1]") && msg.contains("int")); + + // The stdlib now comes from the custom typeshed, so the annotation's `int` and + // `Literal[1]`'s `int` are the same class: `x: int = 1` checks cleanly. Under the + // old behavior the stdlib `int` was the bundled class, yielding a Literal[1]/int + // mismatch here. assert!( - has_literal_int_error, - "Expected error about Literal[1] not being assignable to int, but got: {:?}. \ - This error demonstrates the mismatch between bundled Stdlib int and custom typeshed int.", + error_messages.is_empty(), + "Expected no errors with the stdlib loaded from the custom typeshed, got: {:?}", error_messages ); }