From af780a9afd55094ba439cbefeb0abc32e08ce91f Mon Sep 17 00:00:00 2001 From: ravivats Date: Sun, 30 Aug 2026 12:52:58 +0800 Subject: [PATCH 1/4] Add a failing test for `dump-config --full` `dump-config` reports only the interpreter and the paths imports resolve from, so there is no way to see what any other setting resolved to. Someone debugging a config that appears to be ignored has nothing to inspect. This adds the end-to-end test for a `--full` flag that prints the rest of the resolved settings, ahead of implementing it. The config under test sets a path-scoped override, two error severities written out of alphabetical order, and a misspelled key, so the test pins down the sort order and the reporting of unrecognized keys as well as the overall shape of the output. --- test/config.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/config.md b/test/config.md index 8c92c322d1..add7dc4cae 100644 --- a/test/config.md +++ b/test/config.md @@ -50,6 +50,58 @@ Default configuration [0] ``` +## Dump config with --full + + + +```scrut {output_stream: stdout} +$ mkdir -p $TMPDIR/full/src && touch $TMPDIR/full/src/a.py && \ +> printf 'project-includes = ["src/**/*.py"]\npython-version = "3.12.0"\nuse-ignore-files = false\nnot-a-real-key = 1\n\n[errors]\nmissing-import = "warn"\nbad-assignment = "error"\n\n[[sub-config]]\nmatches = "src/**"\ncheck-unannotated-defs = false\n' > $TMPDIR/full/pyrefly.toml && \ +> $PYREFLY dump-config --full $TMPDIR/full/src/a.py +Configuration at `*/full/pyrefly.toml` (glob) + Using interpreter: * (glob) + Required version: (unset) + Project files: + Includes: [*/full/src/**/*.py] (glob) + Excludes: * (glob) + Excludes heuristics disabled: false + Respecting ignore files: false + Extra file extensions: [] + Python environment: + Version: 3.12.0 + Platform: * (glob) + Import settings: + Search path heuristics disabled: false + Fallback search path enabled: false + Typeshed path: (unset) + Reporting: + Output format: (unset) + Minimum severity: (unset) + Baseline: (unset) + Baseline error level: (unset) + Skip LSP config indexing: false + Type checking: +* (glob+) + Error severity overrides: + bad-assignment: error + missing-import: warn + Path-scoped overrides: + Matching `*/full/src/**/*`: (glob) + check-unannotated-defs: false + Coverage overrides: + Includes: (unset) + Excludes: (unset) + Build system: + (none) + Unrecognized config keys: [not-a-real-key] + Covered files: + */full/src/a.py (glob) + Resolving imports from: +* (glob+) +[0] +``` + ## Specify both files and config ```scrut {output_stream: stderr} From ce07683e0e4d44c835255bfdefa97af0f450edf8 Mon Sep 17 00:00:00 2001 From: ravivats Date: Sun, 30 Aug 2026 12:52:59 +0800 Subject: [PATCH 2/4] Print every resolved setting under `dump-config --full` Users are pointed at `dump-config` to work out why a config is not behaving the way they expect, but it only reports the interpreter and the paths imports resolve from. Every other setting is invisible, and a key Pyrefly did not recognize is dropped silently, which is exactly the case the command is reached for. `--full` prints the rest of the resolved configuration in the indented, prose-labelled style the command already uses. Default output is untouched, so the existing behaviour and its tests still hold. The rendering lives in `pyrefly_config` next to `structured_import_lookup_path` because several `ConfigBase` settings are crate-private and unreadable from the command, which only prints the lines it is handed. Settings the import lookup path already covers are left out so that `--full` adds information rather than repeating it, and the error severities are sorted on the way out because they are stored in a `HashMap`. --- crates/pyrefly_config/src/config.rs | 250 +++++++++++++++++++ crates/pyrefly_config/src/module_wildcard.rs | 6 + pyrefly/lib/commands/dump_config.rs | 11 + 3 files changed, 267 insertions(+) diff --git a/crates/pyrefly_config/src/config.rs b/crates/pyrefly_config/src/config.rs index a408810b2a..47a234d173 100644 --- a/crates/pyrefly_config/src/config.rs +++ b/crates/pyrefly_config/src/config.rs @@ -984,6 +984,256 @@ impl ConfigFile { result } + /// Render every user-facing setting as `label: value` lines for + /// `dump-config --full`, indented relative to the caller's own indentation. + /// + /// This omits what `dump-config` already prints — the config source, the + /// interpreter, the covered files, and everything reported by + /// `structured_import_lookup_path` — so that `--full` adds information + /// rather than repeating it. + pub fn structured_settings(&self) -> Vec { + fn value(value: &Option) -> Option { + value.as_ref().map(T::to_string) + } + + fn path(value: &Option) -> Option { + value.as_ref().map(|p| p.display().to_string()) + } + + // Render an enum by its config-file spelling (`skip-and-infer-return-any`) + // rather than its Rust variant name, so that it matches what a user writes. + fn variant(value: &Option) -> Option { + value.as_ref().map(|v| { + v.to_possible_value() + .expect("config enums do not skip variants") + .get_name() + .to_owned() + }) + } + + fn list(values: impl IntoIterator) -> String { + format!("[{}]", values.into_iter().join(", ")) + } + + fn or_unset(value: Option) -> String { + value.unwrap_or_else(|| "(unset)".to_owned()) + } + + // Every setting a `ConfigBase` carries, labelled by its config-file key so + // that it can be matched against what the user wrote. A `None` value means + // the setting is unspecified, which for a sub-config means "not overridden". + fn base_settings(base: &ConfigBase) -> Vec<(&'static str, Option)> { + vec![ + ( + "check-unannotated-defs", + value(&base.check_unannotated_defs), + ), + ("infer-return-types", variant(&base.infer_return_types)), + ("infer-with-first-use", value(&base.infer_with_first_use)), + ("check-all-matches", value(&base.check_all_matches)), + ( + "strict-callable-subtyping", + value(&base.strict_callable_subtyping), + ), + ( + "strict-partial-subtyping", + value(&base.strict_partial_subtyping), + ), + ( + "spec-compliant-overloads", + value(&base.spec_compliant_overloads), + ), + ( + "legacy-overload-expansion", + value(&base.legacy_overload_expansion), + ), + ( + "treat-all-caps-as-final", + value(&base.treat_all_caps_as_final), + ), + ( + "disable-type-errors-in-ide", + value(&base.disable_type_errors_in_ide), + ), + ( + "ignore-errors-in-generated-code", + value(&base.ignore_errors_in_generated_code), + ), + ("permissive-ignores", value(&base.permissive_ignores)), + ( + "enabled-ignores", + base.enabled_ignores.as_ref().map(|tools| { + list(tools.iter().map(|t| { + t.to_possible_value() + .expect("Tool does not skip variants") + .get_name() + .to_owned() + })) + }), + ), + ( + "replace-imports-with-any", + base.replace_imports_with_any.as_ref().map(list), + ), + ( + "ignore-missing-imports", + base.ignore_missing_imports.as_ref().map(list), + ), + ("recursion-depth-limit", value(&base.recursion_depth_limit)), + ( + "recursion-overflow-handler", + variant(&base.recursion_overflow_handler), + ), + ( + "untyped-def-behavior (deprecated)", + variant(&base.untyped_def_behavior), + ), + ( + "pytorch-efficiency-lints (deprecated)", + value(&base.pytorch_efficiency_lints), + ), + ] + } + + let mut lines = vec![ + format!( + "Required version: {}", + or_unset(value(&self.required_version)) + ), + "Project files:".to_owned(), + format!(" Includes: {}", self.project_includes), + format!(" Excludes: {}", self.project_excludes), + format!( + " Excludes heuristics disabled: {}", + self.disable_project_excludes_heuristics + ), + format!(" Respecting ignore files: {}", self.use_ignore_files), + format!( + " Extra file extensions: {}", + list(&self.extra_file_extensions) + ), + "Python environment:".to_owned(), + format!( + " Version: {}", + or_unset(value(&self.python_environment.python_version)) + ), + format!( + " Platform: {}", + or_unset(value(&self.python_environment.python_platform)) + ), + "Import settings:".to_owned(), + format!( + " Search path heuristics disabled: {}", + self.disable_search_path_heuristics + ), + format!( + " Fallback search path enabled: {}", + self.enable_fallback_search_path + ), + format!(" Typeshed path: {}", or_unset(path(&self.typeshed_path))), + "Reporting:".to_owned(), + format!( + " Output format: {}", + or_unset(variant(&self.output_format)) + ), + format!( + " Minimum severity: {}", + or_unset(variant(&self.min_severity)) + ), + format!(" Baseline: {}", or_unset(path(&self.baseline))), + format!( + " Baseline error level: {}", + or_unset(variant(&self.baseline_error_level)) + ), + format!( + " Skip LSP config indexing: {}", + self.skip_lsp_config_indexing + ), + "Type checking:".to_owned(), + format!(" Preset: {}", or_unset(variant(&self.preset))), + ]; + lines.extend( + base_settings(&self.root) + .into_iter() + .map(|(key, setting)| format!(" {key}: {}", or_unset(setting))), + ); + + // `ErrorDisplayConfig` is backed by a `HashMap`, so sort to keep the output + // stable from run to run. + let errors = self + .root + .errors + .iter() + .flat_map(|errors| errors.iter()) + .map(|(kind, severity)| (kind.to_name(), severity)) + .sorted(); + lines.push("Error severity overrides:".to_owned()); + lines.extend(errors.map(|(kind, severity)| { + format!( + " {kind}: {}", + severity + .to_possible_value() + .expect("Severity does not skip variants") + .get_name() + ) + })); + + lines.push("Path-scoped overrides:".to_owned()); + for sub in &self.sub_configs { + lines.push(format!(" Matching `{}`:", sub.matches)); + lines.extend( + base_settings(&sub.settings) + .into_iter() + .filter_map(|(key, setting)| { + setting.map(|setting| format!(" {key}: {setting}")) + }), + ); + } + + lines.push("Coverage overrides:".to_owned()); + lines.push(format!( + " Includes: {}", + or_unset(value(&self.coverage.includes)) + )); + lines.push(format!( + " Excludes: {}", + or_unset(value(&self.coverage.excludes)) + )); + + lines.push("Build system:".to_owned()); + match &self.build_system { + None => lines.push(" (none)".to_owned()), + Some(build_system) => { + lines.push(format!(" Args: {}", build_system.args)); + lines.push(format!( + " Ignore if missing: {}", + build_system.ignore_if_build_system_missing + )); + lines.push(format!( + " Search path prefix: {}", + list(build_system.search_path_prefix.iter().map(|p| p.display())) + )); + lines.push(format!( + " Catch-all targets: {}", + list(&build_system.catch_all_targets) + )); + lines.push(format!( + " Catch-all targets only: {}", + build_system.catch_all_targets_only + )); + } + } + + // Unrecognized keys are silently ignored when a config is parsed, so + // surfacing them here is often the fastest way to spot a typo. + lines.push(format!( + "Unrecognized config keys: {}", + list(self.root.extras.0.keys()) + )); + + lines + } + pub fn get_sys_info(&self) -> SysInfo { SysInfo::new(self.python_version(), self.python_platform().clone()) } diff --git a/crates/pyrefly_config/src/module_wildcard.rs b/crates/pyrefly_config/src/module_wildcard.rs index 14cfb3cae9..49fa7e5776 100644 --- a/crates/pyrefly_config/src/module_wildcard.rs +++ b/crates/pyrefly_config/src/module_wildcard.rs @@ -109,6 +109,12 @@ impl<'de> Deserialize<'de> for ModuleWildcard { } } +impl fmt::Display for ModuleWildcard { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.origin) + } +} + impl Serialize for ModuleWildcard { fn serialize(&self, serializer: S) -> Result where diff --git a/pyrefly/lib/commands/dump_config.rs b/pyrefly/lib/commands/dump_config.rs index a33fb817cb..794b4903b6 100644 --- a/pyrefly/lib/commands/dump_config.rs +++ b/pyrefly/lib/commands/dump_config.rs @@ -78,6 +78,10 @@ pub struct DumpConfigArgs { /// config(s) to print. Pass "all" to output all files. #[arg(long, default_value_t = MaxFiles::Count(10))] max_files: MaxFiles, + /// Print every resolved configuration setting, not just the interpreter + /// and the paths imports are resolved from. + #[arg(long)] + full: bool, #[command(flatten)] args: FullCheckArgs, } @@ -91,6 +95,7 @@ impl DumpConfigArgs { self.args.files, self.args.config_override, self.max_files, + self.full, wrapper, ) } @@ -100,6 +105,7 @@ fn dump_config( files: FilesArgs, config_override: ConfigOverrideArgs, max_files: MaxFiles, + full: bool, wrapper: Option, ) -> anyhow::Result { config_override.validate()?; @@ -147,6 +153,11 @@ fn dump_config( } } println!(" Using interpreter: {}", config.interpreters); + if full { + for line in config.structured_settings() { + println!(" {line}"); + } + } println!(" Covered files:"); for (i, fi) in files.iter().enumerate() { if max_files.should_print(i) { From 22b5b3a9633d7ffb500a82e8a7070c7195aa5703 Mon Sep 17 00:00:00 2001 From: ravivats Date: Sun, 30 Aug 2026 12:52:59 +0800 Subject: [PATCH 3/4] Document `dump-config --full` The import resolution guide is where users are sent to debug configuration problems, so the new flag is described alongside the command it belongs to. --- website/docs/import-resolution.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/website/docs/import-resolution.mdx b/website/docs/import-resolution.mdx index 73e2250e19..4185d6e96c 100644 --- a/website/docs/import-resolution.mdx +++ b/website/docs/import-resolution.mdx @@ -214,3 +214,8 @@ It will only use import hooks if you set Pyrefly has a `dump-config` command that dumps the import-related config options it is using for each file it is checking. To use it, simply replace `check` with `dump-config` in your command-line invocation. + +Pass `--full` to also print every other resolved configuration setting, including error +severity overrides, path-scoped `sub-config` overrides, and any config keys Pyrefly did not +recognize. This is useful when a setting does not appear to take effect and you need to see +the value Pyrefly actually resolved. From 983f826e6217e7261fab5ffdf9ca2999cf8b6f4c Mon Sep 17 00:00:00 2001 From: ravivats Date: Sun, 1 Nov 2026 02:31:30 +0800 Subject: [PATCH 4/4] Serialize full configuration output as TOML Use the config's serializer so dump-config --full picks up new settings without maintaining a separate list. Omit settings already shown by the command and sort nested tables for stable output. --- crates/pyrefly_config/src/config.rs | 376 +++++++------------ crates/pyrefly_config/src/module_wildcard.rs | 6 - pyrefly/lib/commands/dump_config.rs | 8 +- test/config.md | 59 ++- website/docs/import-resolution.mdx | 7 +- 5 files changed, 160 insertions(+), 296 deletions(-) diff --git a/crates/pyrefly_config/src/config.rs b/crates/pyrefly_config/src/config.rs index 47a234d173..48772f2afb 100644 --- a/crates/pyrefly_config/src/config.rs +++ b/crates/pyrefly_config/src/config.rs @@ -59,6 +59,8 @@ use serde::de::DeserializeOwned; use serde_with::skip_serializing_none; use starlark_map::small_map::SmallMap; use starlark_map::small_set::SmallSet; +use toml::Table; +use toml::Value; use tracing::debug; use tracing::error; @@ -984,254 +986,39 @@ impl ConfigFile { result } - /// Render every user-facing setting as `label: value` lines for - /// `dump-config --full`, indented relative to the caller's own indentation. - /// - /// This omits what `dump-config` already prints — the config source, the - /// interpreter, the covered files, and everything reported by - /// `structured_import_lookup_path` — so that `--full` adds information - /// rather than repeating it. - pub fn structured_settings(&self) -> Vec { - fn value(value: &Option) -> Option { - value.as_ref().map(T::to_string) - } - - fn path(value: &Option) -> Option { - value.as_ref().map(|p| p.display().to_string()) - } - - // Render an enum by its config-file spelling (`skip-and-infer-return-any`) - // rather than its Rust variant name, so that it matches what a user writes. - fn variant(value: &Option) -> Option { - value.as_ref().map(|v| { - v.to_possible_value() - .expect("config enums do not skip variants") - .get_name() - .to_owned() - }) - } - - fn list(values: impl IntoIterator) -> String { - format!("[{}]", values.into_iter().join(", ")) - } - - fn or_unset(value: Option) -> String { - value.unwrap_or_else(|| "(unset)".to_owned()) - } - - // Every setting a `ConfigBase` carries, labelled by its config-file key so - // that it can be matched against what the user wrote. A `None` value means - // the setting is unspecified, which for a sub-config means "not overridden". - fn base_settings(base: &ConfigBase) -> Vec<(&'static str, Option)> { - vec![ - ( - "check-unannotated-defs", - value(&base.check_unannotated_defs), - ), - ("infer-return-types", variant(&base.infer_return_types)), - ("infer-with-first-use", value(&base.infer_with_first_use)), - ("check-all-matches", value(&base.check_all_matches)), - ( - "strict-callable-subtyping", - value(&base.strict_callable_subtyping), - ), - ( - "strict-partial-subtyping", - value(&base.strict_partial_subtyping), - ), - ( - "spec-compliant-overloads", - value(&base.spec_compliant_overloads), - ), - ( - "legacy-overload-expansion", - value(&base.legacy_overload_expansion), - ), - ( - "treat-all-caps-as-final", - value(&base.treat_all_caps_as_final), - ), - ( - "disable-type-errors-in-ide", - value(&base.disable_type_errors_in_ide), - ), - ( - "ignore-errors-in-generated-code", - value(&base.ignore_errors_in_generated_code), - ), - ("permissive-ignores", value(&base.permissive_ignores)), - ( - "enabled-ignores", - base.enabled_ignores.as_ref().map(|tools| { - list(tools.iter().map(|t| { - t.to_possible_value() - .expect("Tool does not skip variants") - .get_name() - .to_owned() - })) - }), - ), - ( - "replace-imports-with-any", - base.replace_imports_with_any.as_ref().map(list), - ), - ( - "ignore-missing-imports", - base.ignore_missing_imports.as_ref().map(list), - ), - ("recursion-depth-limit", value(&base.recursion_depth_limit)), - ( - "recursion-overflow-handler", - variant(&base.recursion_overflow_handler), - ), - ( - "untyped-def-behavior (deprecated)", - variant(&base.untyped_def_behavior), - ), - ( - "pytorch-efficiency-lints (deprecated)", - value(&base.pytorch_efficiency_lints), + /// Serialize settings not covered by the interpreter and import-path output. + /// Unset options and defaults omitted by config serialization are not included. + pub fn remaining_settings(&self) -> Result { + // Error severities use a HashMap, including inside sub-configs. Sort all + // tables for stable output while preserving the order of array elements. + fn sort_tables(value: Value) -> Value { + match value { + Value::Table(table) => Value::Table( + table + .into_iter() + .sorted_by(|(a, _), (b, _)| a.cmp(b)) + .map(|(key, value)| (key, sort_tables(value))) + .collect(), ), - ] - } - - let mut lines = vec![ - format!( - "Required version: {}", - or_unset(value(&self.required_version)) - ), - "Project files:".to_owned(), - format!(" Includes: {}", self.project_includes), - format!(" Excludes: {}", self.project_excludes), - format!( - " Excludes heuristics disabled: {}", - self.disable_project_excludes_heuristics - ), - format!(" Respecting ignore files: {}", self.use_ignore_files), - format!( - " Extra file extensions: {}", - list(&self.extra_file_extensions) - ), - "Python environment:".to_owned(), - format!( - " Version: {}", - or_unset(value(&self.python_environment.python_version)) - ), - format!( - " Platform: {}", - or_unset(value(&self.python_environment.python_platform)) - ), - "Import settings:".to_owned(), - format!( - " Search path heuristics disabled: {}", - self.disable_search_path_heuristics - ), - format!( - " Fallback search path enabled: {}", - self.enable_fallback_search_path - ), - format!(" Typeshed path: {}", or_unset(path(&self.typeshed_path))), - "Reporting:".to_owned(), - format!( - " Output format: {}", - or_unset(variant(&self.output_format)) - ), - format!( - " Minimum severity: {}", - or_unset(variant(&self.min_severity)) - ), - format!(" Baseline: {}", or_unset(path(&self.baseline))), - format!( - " Baseline error level: {}", - or_unset(variant(&self.baseline_error_level)) - ), - format!( - " Skip LSP config indexing: {}", - self.skip_lsp_config_indexing - ), - "Type checking:".to_owned(), - format!(" Preset: {}", or_unset(variant(&self.preset))), - ]; - lines.extend( - base_settings(&self.root) - .into_iter() - .map(|(key, setting)| format!(" {key}: {}", or_unset(setting))), - ); - - // `ErrorDisplayConfig` is backed by a `HashMap`, so sort to keep the output - // stable from run to run. - let errors = self - .root - .errors - .iter() - .flat_map(|errors| errors.iter()) - .map(|(kind, severity)| (kind.to_name(), severity)) - .sorted(); - lines.push("Error severity overrides:".to_owned()); - lines.extend(errors.map(|(kind, severity)| { - format!( - " {kind}: {}", - severity - .to_possible_value() - .expect("Severity does not skip variants") - .get_name() - ) - })); - - lines.push("Path-scoped overrides:".to_owned()); - for sub in &self.sub_configs { - lines.push(format!(" Matching `{}`:", sub.matches)); - lines.extend( - base_settings(&sub.settings) - .into_iter() - .filter_map(|(key, setting)| { - setting.map(|setting| format!(" {key}: {setting}")) - }), - ); - } - - lines.push("Coverage overrides:".to_owned()); - lines.push(format!( - " Includes: {}", - or_unset(value(&self.coverage.includes)) - )); - lines.push(format!( - " Excludes: {}", - or_unset(value(&self.coverage.excludes)) - )); - - lines.push("Build system:".to_owned()); - match &self.build_system { - None => lines.push(" (none)".to_owned()), - Some(build_system) => { - lines.push(format!(" Args: {}", build_system.args)); - lines.push(format!( - " Ignore if missing: {}", - build_system.ignore_if_build_system_missing - )); - lines.push(format!( - " Search path prefix: {}", - list(build_system.search_path_prefix.iter().map(|p| p.display())) - )); - lines.push(format!( - " Catch-all targets: {}", - list(&build_system.catch_all_targets) - )); - lines.push(format!( - " Catch-all targets only: {}", - build_system.catch_all_targets_only - )); + Value::Array(values) => Value::Array(values.into_iter().map(sort_tables).collect()), + value => value, } } - // Unrecognized keys are silently ignored when a config is parsed, so - // surfacing them here is often the fastest way to spot a typo. - lines.push(format!( - "Unrecognized config keys: {}", - list(self.root.extras.0.keys()) - )); - - lines + let mut settings = Table::try_from(self).context("Failed to serialize config to TOML")?; + for key in [ + "python-interpreter-path", + "fallback-python-interpreter-name", + "python-interpreter-find-command", + "conda-environment", + "skip-interpreter-query", + "search-path", + "site-package-path", + ] { + settings.remove(key); + } + toml::to_string_pretty(&sort_tables(Value::Table(settings))) + .context("Failed to serialize remaining config settings to TOML") } pub fn get_sys_info(&self) -> SysInfo { @@ -2229,8 +2016,6 @@ mod tests { use pyrefly_util::includes::Includes; use pyrefly_util::test_path::TestPath; use tempfile::TempDir; - use toml::Table; - use toml::Value; use super::*; use crate::base::ExtraConfigs; @@ -2241,6 +2026,105 @@ mod tests { use crate::module_wildcard::ModuleWildcard; use crate::util::ConfigOrigin; + #[test] + fn test_remaining_settings() { + let config: ConfigFile = toml::from_str( + r#" + python-interpreter-path = "python" + fallback-python-interpreter-name = "python3" + python-interpreter-find-command = ["find-python"] + conda-environment = "test" + skip-interpreter-query = true + search-path = ["src"] + site-package-path = ["packages"] + use-ignore-files = false + replace-imports-with-any = ["vendor.*", "!vendor.safe"] + not-a-real-key = "quoted \"value\"\nnext line" + + [errors] + missing-import = "warn" + bad-assignment = "error" + + [coverage] + includes = ["src/**"] + + [[sub-config]] + matches = "src/**" + check-unannotated-defs = false + + [sub-config.errors] + missing-import = "ignore" + bad-assignment = "warn" + + [[sub-config]] + matches = "tests/**" + check-unannotated-defs = true + "#, + ) + .unwrap(); + let settings: Table = toml::from_str(&config.remaining_settings().unwrap()).unwrap(); + for key in [ + "python-interpreter-path", + "fallback-python-interpreter-name", + "python-interpreter-find-command", + "conda-environment", + "skip-interpreter-query", + "search-path", + "site-package-path", + ] { + assert!(!settings.contains_key(key), "{key} is already printed"); + } + assert_eq!(settings["use-ignore-files"].as_bool(), Some(false)); + assert_eq!( + settings["not-a-real-key"].as_str(), + Some("quoted \"value\"\nnext line") + ); + assert_eq!( + settings["replace-imports-with-any"], + Value::try_from(["vendor.*", "!vendor.safe"]).unwrap() + ); + assert_eq!( + settings["coverage"]["includes"], + Value::try_from(["src/**/*"]).unwrap() + ); + let overrides = settings["sub-config"].as_array().unwrap(); + assert_eq!(overrides.len(), 2); + assert_eq!(overrides[0]["matches"].as_str(), Some("src/**/*")); + assert_eq!(overrides[1]["matches"].as_str(), Some("tests/**/*")); + assert_eq!( + overrides[0]["check-unannotated-defs"].as_bool(), + Some(false) + ); + assert_eq!(overrides[1]["check-unannotated-defs"].as_bool(), Some(true)); + for (errors, severities) in [ + (&settings["errors"], ["error", "warn"]), + (&overrides[0]["errors"], ["warn", "ignore"]), + ] { + assert_eq!( + errors.as_table().unwrap().keys().collect::>(), + ["bad-assignment", "missing-import"] + ); + assert_eq!(errors["bad-assignment"].as_str(), Some(severities[0])); + assert_eq!(errors["missing-import"].as_str(), Some(severities[1])); + } + } + + #[test] + fn test_remaining_settings_resolved_defaults() { + let mut config = ConfigFile::default(); + config.interpreters.skip_interpreter_query = true; + config.root.check_unannotated_defs = Some(false); + assert!(config.configure().is_empty()); + + let settings: Table = toml::from_str(&config.remaining_settings().unwrap()).unwrap(); + assert_eq!(settings["check-unannotated-defs"].as_bool(), Some(false)); + assert_eq!(settings["infer-return-types"].as_str(), Some("checked")); + assert!(settings.contains_key("python-version")); + assert!(!settings.contains_key("required-version")); + assert!(!settings.contains_key("disable-search-path-heuristics")); + assert!(!settings.contains_key("use-ignore-files")); + } + #[test] fn deserialize_pyrefly_config() { let config_str = r#" diff --git a/crates/pyrefly_config/src/module_wildcard.rs b/crates/pyrefly_config/src/module_wildcard.rs index 49fa7e5776..14cfb3cae9 100644 --- a/crates/pyrefly_config/src/module_wildcard.rs +++ b/crates/pyrefly_config/src/module_wildcard.rs @@ -109,12 +109,6 @@ impl<'de> Deserialize<'de> for ModuleWildcard { } } -impl fmt::Display for ModuleWildcard { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.origin) - } -} - impl Serialize for ModuleWildcard { fn serialize(&self, serializer: S) -> Result where diff --git a/pyrefly/lib/commands/dump_config.rs b/pyrefly/lib/commands/dump_config.rs index 794b4903b6..ac292a96c3 100644 --- a/pyrefly/lib/commands/dump_config.rs +++ b/pyrefly/lib/commands/dump_config.rs @@ -78,8 +78,7 @@ pub struct DumpConfigArgs { /// config(s) to print. Pass "all" to output all files. #[arg(long, default_value_t = MaxFiles::Count(10))] max_files: MaxFiles, - /// Print every resolved configuration setting, not just the interpreter - /// and the paths imports are resolved from. + /// Print additional configuration settings as TOML. #[arg(long)] full: bool, #[command(flatten)] @@ -154,8 +153,9 @@ fn dump_config( } println!(" Using interpreter: {}", config.interpreters); if full { - for line in config.structured_settings() { - println!(" {line}"); + println!(" Configuration settings (TOML):"); + for line in config.remaining_settings()?.lines() { + println!(" {line}"); } } println!(" Covered files:"); diff --git a/test/config.md b/test/config.md index add7dc4cae..4e17b18600 100644 --- a/test/config.md +++ b/test/config.md @@ -52,49 +52,34 @@ Default configuration ## Dump config with --full - + ```scrut {output_stream: stdout} $ mkdir -p $TMPDIR/full/src && touch $TMPDIR/full/src/a.py && \ -> printf 'project-includes = ["src/**/*.py"]\npython-version = "3.12.0"\nuse-ignore-files = false\nnot-a-real-key = 1\n\n[errors]\nmissing-import = "warn"\nbad-assignment = "error"\n\n[[sub-config]]\nmatches = "src/**"\ncheck-unannotated-defs = false\n' > $TMPDIR/full/pyrefly.toml && \ -> $PYREFLY dump-config --full $TMPDIR/full/src/a.py +> printf 'project-includes = ["src/**/*.py"]\npython-version = "3.12.0"\nuse-ignore-files = false\nnot-a-real-key = 1\n\n[errors]\nmissing-import = "warn"\nbad-assignment = "error"\n\n[[sub-config]]\nmatches = "src/**"\ncheck-unannotated-defs = false\n\n[sub-config.errors]\nmissing-import = "ignore"\nbad-assignment = "warn"\n' > $TMPDIR/full/pyrefly.toml && \ +> $PYREFLY dump-config --full --python-version 3.11.0 $TMPDIR/full/src/a.py Configuration at `*/full/pyrefly.toml` (glob) Using interpreter: * (glob) - Required version: (unset) - Project files: - Includes: [*/full/src/**/*.py] (glob) - Excludes: * (glob) - Excludes heuristics disabled: false - Respecting ignore files: false - Extra file extensions: [] - Python environment: - Version: 3.12.0 - Platform: * (glob) - Import settings: - Search path heuristics disabled: false - Fallback search path enabled: false - Typeshed path: (unset) - Reporting: - Output format: (unset) - Minimum severity: (unset) - Baseline: (unset) - Baseline error level: (unset) - Skip LSP config indexing: false - Type checking: + Configuration settings (TOML): * (glob+) - Error severity overrides: - bad-assignment: error - missing-import: warn - Path-scoped overrides: - Matching `*/full/src/**/*`: (glob) - check-unannotated-defs: false - Coverage overrides: - Includes: (unset) - Excludes: (unset) - Build system: - (none) - Unrecognized config keys: [not-a-real-key] + not-a-real-key = 1 +* (glob+) + python-version = "3.11.0" +* (glob+) + use-ignore-files = false +* (glob+) + [errors] + bad-assignment = "error" + missing-import = "warn" +* (glob+) + [[sub-config]] + check-unannotated-defs = false + matches = "*/full/src/**/*" (glob) +* (glob+) + [sub-config.errors] + bad-assignment = "warn" + missing-import = "ignore" Covered files: */full/src/a.py (glob) Resolving imports from: diff --git a/website/docs/import-resolution.mdx b/website/docs/import-resolution.mdx index 4185d6e96c..d0f748cb24 100644 --- a/website/docs/import-resolution.mdx +++ b/website/docs/import-resolution.mdx @@ -215,7 +215,8 @@ Pyrefly has a `dump-config` command that dumps the import-related config options each file it is checking. To use it, simply replace `check` with `dump-config` in your command-line invocation. -Pass `--full` to also print every other resolved configuration setting, including error +Pass `--full` to also print the remaining resolved configuration as TOML, including error severity overrides, path-scoped `sub-config` overrides, and any config keys Pyrefly did not -recognize. This is useful when a setting does not appear to take effect and you need to see -the value Pyrefly actually resolved. +recognize. Unset options and some settings at their default values are omitted. This is +useful when a setting does not appear to take effect and you need to see the value Pyrefly +actually resolved.