diff --git a/crates/pyrefly_config/src/config.rs b/crates/pyrefly_config/src/config.rs index a408810b2a..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,6 +986,41 @@ impl ConfigFile { result } + /// 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(), + ), + Value::Array(values) => Value::Array(values.into_iter().map(sort_tables).collect()), + value => value, + } + } + + 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 { SysInfo::new(self.python_version(), self.python_platform().clone()) } @@ -1979,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; @@ -1991,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/pyrefly/lib/commands/dump_config.rs b/pyrefly/lib/commands/dump_config.rs index a33fb817cb..ac292a96c3 100644 --- a/pyrefly/lib/commands/dump_config.rs +++ b/pyrefly/lib/commands/dump_config.rs @@ -78,6 +78,9 @@ 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 additional configuration settings as TOML. + #[arg(long)] + full: bool, #[command(flatten)] args: FullCheckArgs, } @@ -91,6 +94,7 @@ impl DumpConfigArgs { self.args.files, self.args.config_override, self.max_files, + self.full, wrapper, ) } @@ -100,6 +104,7 @@ fn dump_config( files: FilesArgs, config_override: ConfigOverrideArgs, max_files: MaxFiles, + full: bool, wrapper: Option, ) -> anyhow::Result { config_override.validate()?; @@ -147,6 +152,12 @@ fn dump_config( } } println!(" Using interpreter: {}", config.interpreters); + if full { + println!(" Configuration settings (TOML):"); + for line in config.remaining_settings()?.lines() { + println!(" {line}"); + } + } println!(" Covered files:"); for (i, fi) in files.iter().enumerate() { if max_files.should_print(i) { diff --git a/test/config.md b/test/config.md index 8c92c322d1..4e17b18600 100644 --- a/test/config.md +++ b/test/config.md @@ -50,6 +50,43 @@ 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\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) + Configuration settings (TOML): +* (glob+) + 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: +* (glob+) +[0] +``` + ## Specify both files and config ```scrut {output_stream: stderr} diff --git a/website/docs/import-resolution.mdx b/website/docs/import-resolution.mdx index 73e2250e19..d0f748cb24 100644 --- a/website/docs/import-resolution.mdx +++ b/website/docs/import-resolution.mdx @@ -214,3 +214,9 @@ 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 the remaining resolved configuration as TOML, including error +severity overrides, path-scoped `sub-config` overrides, and any config keys Pyrefly did not +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.