[ty] Add --config CLI arg (#17697)

This commit is contained in:
justin 2025-05-09 02:38:37 -04:00 committed by GitHub
parent 6c177e2bbe
commit f46ed8d410
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 186 additions and 4 deletions

View file

@ -1,9 +1,11 @@
use crate::logging::Verbosity;
use crate::python_version::PythonVersion;
use clap::error::ErrorKind;
use clap::{ArgAction, ArgMatches, Error, Parser};
use ruff_db::system::SystemPathBuf;
use ty_project::combine::Combine;
use ty_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions};
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use ty_project::metadata::value::{RangedValue, RelativePathBuf, ValueSource};
use ty_python_semantic::lint;
#[derive(Debug, Parser)]
@ -14,6 +16,7 @@ pub(crate) struct Args {
pub(crate) command: Command,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, clap::Subcommand)]
pub(crate) enum Command {
/// Check a project for type errors.
@ -86,6 +89,9 @@ pub(crate) struct CheckCommand {
#[clap(flatten)]
pub(crate) rules: RulesArg,
#[clap(flatten)]
pub(crate) config: ConfigsArg,
/// The format to use for printing diagnostic messages.
#[arg(long)]
pub(crate) output_format: Option<OutputFormat>,
@ -140,7 +146,7 @@ impl CheckCommand {
.no_respect_ignore_files
.then_some(false)
.or(self.respect_ignore_files);
Options {
let options = Options {
environment: Some(EnvironmentOptions {
python_version: self
.python_version
@ -166,7 +172,9 @@ impl CheckCommand {
rules,
respect_ignore_files,
..Default::default()
}
};
// Merge with options passed in via --config
options.combine(self.config.into_options().unwrap_or_default())
}
}
@ -299,3 +307,55 @@ pub(crate) enum TerminalColor {
/// Never display colors.
Never,
}
/// A TOML `<KEY> = <VALUE>` pair
/// (such as you might find in a `ty.toml` configuration file)
/// overriding a specific configuration option.
/// Overrides of individual settings using this option always take precedence
/// over all configuration files.
#[derive(Debug, Clone)]
pub(crate) struct ConfigsArg(Option<Options>);
impl clap::FromArgMatches for ConfigsArg {
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, Error> {
let combined = matches
.get_many::<String>("config")
.into_iter()
.flatten()
.map(|s| {
Options::from_toml_str(s, ValueSource::Cli)
.map_err(|err| Error::raw(ErrorKind::InvalidValue, err.to_string()))
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.reduce(|acc, item| item.combine(acc));
Ok(Self(combined))
}
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> {
self.0 = Self::from_arg_matches(matches)?.0;
Ok(())
}
}
impl clap::Args for ConfigsArg {
fn augment_args(cmd: clap::Command) -> clap::Command {
cmd.arg(
clap::Arg::new("config")
.short('c')
.long("config")
.value_name("CONFIG_OPTION")
.help("A TOML `<KEY> = <VALUE>` pair")
.action(ArgAction::Append),
)
}
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
Self::augment_args(cmd)
}
}
impl ConfigsArg {
pub(crate) fn into_options(self) -> Option<Options> {
self.0
}
}

View file

@ -1338,6 +1338,128 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn cli_config_args_toml_string_basic() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
// Long flag
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true"), @r"
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: `lint:unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
");
// Short flag
assert_cmd_snapshot!(case.command().arg("-c").arg("terminal.error-on-warning=true"), @r"
success: false
exit_code: 1
----- stdout -----
error: lint:unresolved-reference: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: `lint:unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn cli_config_args_overrides_knot_toml() -> anyhow::Result<()> {
let case = TestCase::with_files(vec![
(
"knot.toml",
r#"
[terminal]
error-on-warning = true
"#,
),
("test.py", r"print(x) # [unresolved-reference]"),
])?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=false"), @r"
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: `lint:unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true").arg("--config").arg("terminal.error-on-warning=false"), @r"
success: true
exit_code: 0
----- stdout -----
warning: lint:unresolved-reference: Name `x` used when not defined
--> test.py:1:7
|
1 | print(x) # [unresolved-reference]
| ^
|
info: `lint:unresolved-reference` was selected on the command line
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn cli_config_args_invalid_option() -> anyhow::Result<()> {
let case = TestCase::with_file("test.py", r"print(1)")?;
assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: TOML parse error at line 1, column 1
|
1 | bad-option=true
| ^^^^^^^^^^
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `respect-ignore-files`
Usage: ty <COMMAND>
For more information, try '--help'.
");
Ok(())
}
struct TestCase {
_temp_dir: TempDir,
_settings_scope: SettingsBindDropGuard,

View file

@ -38,7 +38,7 @@ pub struct Options {
}
impl Options {
pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, TyTomlError> {
pub fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, TyTomlError> {
let _guard = ValueSourceGuard::new(source, true);
let options = toml::from_str(content)?;
Ok(options)