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
250 changes: 250 additions & 0 deletions crates/pyrefly_config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
fn value<T: Display>(value: &Option<T>) -> Option<String> {
value.as_ref().map(T::to_string)
}

fn path(value: &Option<PathBuf>) -> Option<String> {
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<T: ValueEnum>(value: &Option<T>) -> Option<String> {
value.as_ref().map(|v| {
v.to_possible_value()
.expect("config enums do not skip variants")
.get_name()
.to_owned()
})
}

fn list<T: Display>(values: impl IntoIterator<Item = T>) -> String {
format!("[{}]", values.into_iter().join(", "))
}

fn or_unset(value: Option<String>) -> 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<String>)> {
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())
}
Expand Down
6 changes: 6 additions & 0 deletions crates/pyrefly_config/src/module_wildcard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
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,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,
}
Expand All @@ -91,6 +95,7 @@ impl DumpConfigArgs {
self.args.files,
self.args.config_override,
self.max_files,
self.full,
wrapper,
)
}
Expand All @@ -100,6 +105,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 +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) {
Expand Down
52 changes: 52 additions & 0 deletions test/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,58 @@ Default configuration
[0]
```

## Dump config with --full

<!-- The `Type checking:` settings are matched with a glob so that adding a new
`ConfigBase` setting does not break 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' > $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}
Expand Down
5 changes: 5 additions & 0 deletions website/docs/import-resolution.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading