Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 136 additions & 2 deletions crates/pyrefly_config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<String> {
// 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())
}
Expand Down Expand Up @@ -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;
Expand All @@ -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::<Vec<_>>(),
["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#"
Expand Down
11 changes: 11 additions & 0 deletions pyrefly/lib/commands/dump_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -91,6 +94,7 @@ impl DumpConfigArgs {
self.args.files,
self.args.config_override,
self.max_files,
self.full,
wrapper,
)
}
Expand All @@ -100,6 +104,7 @@ fn dump_config(
files: FilesArgs,
config_override: ConfigOverrideArgs,
max_files: MaxFiles,
full: bool,
wrapper: Option<ConfigConfigurerWrapper>,
) -> anyhow::Result<CommandExitStatus> {
config_override.validate()?;
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 37 additions & 0 deletions test/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,43 @@ Default configuration
[0]
```

## Dump config with --full

<!-- Default settings are matched with globs so that new config fields do not
require updating this test. -->

```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}
Expand Down
6 changes: 6 additions & 0 deletions website/docs/import-resolution.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading