Skip to content

Commit f46ed8d

Browse files
authored
[ty] Add --config CLI arg (#17697)
1 parent 6c177e2 commit f46ed8d

File tree

3 files changed

+186
-4
lines changed

3 files changed

+186
-4
lines changed

crates/ty/src/args.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use crate::logging::Verbosity;
22
use crate::python_version::PythonVersion;
3+
use clap::error::ErrorKind;
34
use clap::{ArgAction, ArgMatches, Error, Parser};
45
use ruff_db::system::SystemPathBuf;
6+
use ty_project::combine::Combine;
57
use ty_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions};
6-
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
8+
use ty_project::metadata::value::{RangedValue, RelativePathBuf, ValueSource};
79
use ty_python_semantic::lint;
810

911
#[derive(Debug, Parser)]
@@ -14,6 +16,7 @@ pub(crate) struct Args {
1416
pub(crate) command: Command,
1517
}
1618

19+
#[allow(clippy::large_enum_variant)]
1720
#[derive(Debug, clap::Subcommand)]
1821
pub(crate) enum Command {
1922
/// Check a project for type errors.
@@ -86,6 +89,9 @@ pub(crate) struct CheckCommand {
8689
#[clap(flatten)]
8790
pub(crate) rules: RulesArg,
8891

92+
#[clap(flatten)]
93+
pub(crate) config: ConfigsArg,
94+
8995
/// The format to use for printing diagnostic messages.
9096
#[arg(long)]
9197
pub(crate) output_format: Option<OutputFormat>,
@@ -140,7 +146,7 @@ impl CheckCommand {
140146
.no_respect_ignore_files
141147
.then_some(false)
142148
.or(self.respect_ignore_files);
143-
Options {
149+
let options = Options {
144150
environment: Some(EnvironmentOptions {
145151
python_version: self
146152
.python_version
@@ -166,7 +172,9 @@ impl CheckCommand {
166172
rules,
167173
respect_ignore_files,
168174
..Default::default()
169-
}
175+
};
176+
// Merge with options passed in via --config
177+
options.combine(self.config.into_options().unwrap_or_default())
170178
}
171179
}
172180

@@ -299,3 +307,55 @@ pub(crate) enum TerminalColor {
299307
/// Never display colors.
300308
Never,
301309
}
310+
/// A TOML `<KEY> = <VALUE>` pair
311+
/// (such as you might find in a `ty.toml` configuration file)
312+
/// overriding a specific configuration option.
313+
/// Overrides of individual settings using this option always take precedence
314+
/// over all configuration files.
315+
#[derive(Debug, Clone)]
316+
pub(crate) struct ConfigsArg(Option<Options>);
317+
318+
impl clap::FromArgMatches for ConfigsArg {
319+
fn from_arg_matches(matches: &ArgMatches) -> Result<Self, Error> {
320+
let combined = matches
321+
.get_many::<String>("config")
322+
.into_iter()
323+
.flatten()
324+
.map(|s| {
325+
Options::from_toml_str(s, ValueSource::Cli)
326+
.map_err(|err| Error::raw(ErrorKind::InvalidValue, err.to_string()))
327+
})
328+
.collect::<Result<Vec<_>, _>>()?
329+
.into_iter()
330+
.reduce(|acc, item| item.combine(acc));
331+
Ok(Self(combined))
332+
}
333+
334+
fn update_from_arg_matches(&mut self, matches: &ArgMatches) -> Result<(), Error> {
335+
self.0 = Self::from_arg_matches(matches)?.0;
336+
Ok(())
337+
}
338+
}
339+
340+
impl clap::Args for ConfigsArg {
341+
fn augment_args(cmd: clap::Command) -> clap::Command {
342+
cmd.arg(
343+
clap::Arg::new("config")
344+
.short('c')
345+
.long("config")
346+
.value_name("CONFIG_OPTION")
347+
.help("A TOML `<KEY> = <VALUE>` pair")
348+
.action(ArgAction::Append),
349+
)
350+
}
351+
352+
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
353+
Self::augment_args(cmd)
354+
}
355+
}
356+
357+
impl ConfigsArg {
358+
pub(crate) fn into_options(self) -> Option<Options> {
359+
self.0
360+
}
361+
}

crates/ty/tests/cli.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,128 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
13381338
Ok(())
13391339
}
13401340

1341+
#[test]
1342+
fn cli_config_args_toml_string_basic() -> anyhow::Result<()> {
1343+
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
1344+
1345+
// Long flag
1346+
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true"), @r"
1347+
success: false
1348+
exit_code: 1
1349+
----- stdout -----
1350+
warning: lint:unresolved-reference: Name `x` used when not defined
1351+
--> test.py:1:7
1352+
|
1353+
1 | print(x) # [unresolved-reference]
1354+
| ^
1355+
|
1356+
info: `lint:unresolved-reference` was selected on the command line
1357+
1358+
Found 1 diagnostic
1359+
1360+
----- stderr -----
1361+
");
1362+
1363+
// Short flag
1364+
assert_cmd_snapshot!(case.command().arg("-c").arg("terminal.error-on-warning=true"), @r"
1365+
success: false
1366+
exit_code: 1
1367+
----- stdout -----
1368+
error: lint:unresolved-reference: Name `x` used when not defined
1369+
--> test.py:1:7
1370+
|
1371+
1 | print(x) # [unresolved-reference]
1372+
| ^
1373+
|
1374+
info: `lint:unresolved-reference` is enabled by default
1375+
1376+
Found 1 diagnostic
1377+
1378+
----- stderr -----
1379+
");
1380+
1381+
Ok(())
1382+
}
1383+
1384+
#[test]
1385+
fn cli_config_args_overrides_knot_toml() -> anyhow::Result<()> {
1386+
let case = TestCase::with_files(vec![
1387+
(
1388+
"knot.toml",
1389+
r#"
1390+
[terminal]
1391+
error-on-warning = true
1392+
"#,
1393+
),
1394+
("test.py", r"print(x) # [unresolved-reference]"),
1395+
])?;
1396+
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=false"), @r"
1397+
success: true
1398+
exit_code: 0
1399+
----- stdout -----
1400+
warning: lint:unresolved-reference: Name `x` used when not defined
1401+
--> test.py:1:7
1402+
|
1403+
1 | print(x) # [unresolved-reference]
1404+
| ^
1405+
|
1406+
info: `lint:unresolved-reference` was selected on the command line
1407+
1408+
Found 1 diagnostic
1409+
1410+
----- stderr -----
1411+
");
1412+
1413+
Ok(())
1414+
}
1415+
1416+
#[test]
1417+
fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> {
1418+
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
1419+
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"
1420+
success: true
1421+
exit_code: 0
1422+
----- stdout -----
1423+
warning: lint:unresolved-reference: Name `x` used when not defined
1424+
--> test.py:1:7
1425+
|
1426+
1 | print(x) # [unresolved-reference]
1427+
| ^
1428+
|
1429+
info: `lint:unresolved-reference` was selected on the command line
1430+
1431+
Found 1 diagnostic
1432+
1433+
----- stderr -----
1434+
");
1435+
1436+
Ok(())
1437+
}
1438+
1439+
#[test]
1440+
fn cli_config_args_invalid_option() -> anyhow::Result<()> {
1441+
let case = TestCase::with_file("test.py", r"print(1)")?;
1442+
assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r"
1443+
success: false
1444+
exit_code: 2
1445+
----- stdout -----
1446+
1447+
----- stderr -----
1448+
error: TOML parse error at line 1, column 1
1449+
|
1450+
1 | bad-option=true
1451+
| ^^^^^^^^^^
1452+
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `respect-ignore-files`
1453+
1454+
1455+
Usage: ty <COMMAND>
1456+
1457+
For more information, try '--help'.
1458+
");
1459+
1460+
Ok(())
1461+
}
1462+
13411463
struct TestCase {
13421464
_temp_dir: TempDir,
13431465
_settings_scope: SettingsBindDropGuard,

crates/ty_project/src/metadata/options.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub struct Options {
3838
}
3939

4040
impl Options {
41-
pub(crate) fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, TyTomlError> {
41+
pub fn from_toml_str(content: &str, source: ValueSource) -> Result<Self, TyTomlError> {
4242
let _guard = ValueSourceGuard::new(source, true);
4343
let options = toml::from_str(content)?;
4444
Ok(options)

0 commit comments

Comments
 (0)