mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:44:56 +00:00
[ty] Add --config-file CLI arg (#18083)
This commit is contained in:
parent
6453ac9ea1
commit
8d5655a7ba
12 changed files with 300 additions and 63 deletions
|
@ -78,7 +78,7 @@ fn setup_tomllib_case() -> Case {
|
|||
|
||||
let src_root = SystemPath::new("/src");
|
||||
let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap();
|
||||
metadata.apply_cli_options(Options {
|
||||
metadata.apply_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
|
||||
..EnvironmentOptions::default()
|
||||
|
@ -224,7 +224,7 @@ fn setup_micro_case(code: &str) -> Case {
|
|||
|
||||
let src_root = SystemPath::new("/src");
|
||||
let mut metadata = ProjectMetadata::discover(src_root, &system).unwrap();
|
||||
metadata.apply_cli_options(Options {
|
||||
metadata.apply_options(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
|
||||
..EnvironmentOptions::default()
|
||||
|
|
|
@ -22,7 +22,7 @@ ty_server = { workspace = true }
|
|||
|
||||
anyhow = { workspace = true }
|
||||
argfile = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help", "string"] }
|
||||
clap = { workspace = true, features = ["wrap_help", "string", "env"] }
|
||||
clap_complete_command = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
countme = { workspace = true, features = ["enable"] }
|
||||
|
|
4
crates/ty/docs/cli.md
generated
4
crates/ty/docs/cli.md
generated
|
@ -47,7 +47,9 @@ ty check [OPTIONS] [PATH]...
|
|||
overriding a specific configuration option.</p>
|
||||
<p>Overrides of individual settings using this option always take precedence
|
||||
over all configuration files.</p>
|
||||
</dd><dt id="ty-check--error"><a href="#ty-check--error"><code>--error</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'error'. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--config-file"><a href="#ty-check--config-file"><code>--config-file</code></a> <i>path</i></dt><dd><p>The path to a <code>ty.toml</code> file to use for configuration.</p>
|
||||
<p>While ty configuration can be included in a <code>pyproject.toml</code> file, it is not allowed in this context.</p>
|
||||
<p>May also be set with the <code>TY_CONFIG_FILE</code> environment variable.</p></dd><dt id="ty-check--error"><a href="#ty-check--error"><code>--error</code></a> <i>rule</i></dt><dd><p>Treat the given rule as having severity 'error'. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--error-on-warning"><a href="#ty-check--error-on-warning"><code>--error-on-warning</code></a></dt><dd><p>Use exit code 1 if there are any warning-level diagnostics</p>
|
||||
</dd><dt id="ty-check--exit-zero"><a href="#ty-check--exit-zero"><code>--exit-zero</code></a></dt><dd><p>Always use exit code 0, even when there are error-level diagnostics</p>
|
||||
</dd><dt id="ty-check--extra-search-path"><a href="#ty-check--extra-search-path"><code>--extra-search-path</code></a> <i>path</i></dt><dd><p>Additional path to use as a module-resolution source (can be passed multiple times)</p>
|
||||
|
|
|
@ -107,6 +107,12 @@ pub(crate) struct CheckCommand {
|
|||
#[clap(flatten)]
|
||||
pub(crate) config: ConfigsArg,
|
||||
|
||||
/// The path to a `ty.toml` file to use for configuration.
|
||||
///
|
||||
/// While ty configuration can be included in a `pyproject.toml` file, it is not allowed in this context.
|
||||
#[arg(long, env = "TY_CONFIG_FILE", value_name = "PATH")]
|
||||
pub(crate) config_file: Option<SystemPathBuf>,
|
||||
|
||||
/// The format to use for printing diagnostic messages.
|
||||
#[arg(long)]
|
||||
pub(crate) output_format: Option<OutputFormat>,
|
||||
|
|
|
@ -23,7 +23,7 @@ use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
|
|||
use ruff_db::max_parallelism;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use ty_project::metadata::options::Options;
|
||||
use ty_project::metadata::options::ProjectOptionsOverrides;
|
||||
use ty_project::watch::ProjectWatcher;
|
||||
use ty_project::{Db, DummyReporter, Reporter, watch};
|
||||
use ty_project::{ProjectDatabase, ProjectMetadata};
|
||||
|
@ -102,13 +102,21 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
|||
.map(|path| SystemPath::absolute(path, &cwd))
|
||||
.collect();
|
||||
|
||||
let system = OsSystem::new(cwd);
|
||||
let system = OsSystem::new(&cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
let config_file = args
|
||||
.config_file
|
||||
.as_ref()
|
||||
.map(|path| SystemPath::absolute(path, &cwd));
|
||||
|
||||
let cli_options = args.into_options();
|
||||
let mut project_metadata = ProjectMetadata::discover(&project_path, &system)?;
|
||||
project_metadata.apply_cli_options(cli_options.clone());
|
||||
let mut project_metadata = match &config_file {
|
||||
Some(config_file) => ProjectMetadata::from_config_file(config_file.clone(), &system)?,
|
||||
None => ProjectMetadata::discover(&project_path, &system)?,
|
||||
};
|
||||
|
||||
let options = args.into_options();
|
||||
project_metadata.apply_options(options.clone());
|
||||
project_metadata.apply_configuration_files(&system)?;
|
||||
|
||||
let mut db = ProjectDatabase::new(project_metadata, system)?;
|
||||
|
@ -117,7 +125,8 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
|||
db.project().set_included_paths(&mut db, check_paths);
|
||||
}
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
|
||||
let project_options_overrides = ProjectOptionsOverrides::new(config_file, options);
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(project_options_overrides);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
|
@ -178,11 +187,13 @@ struct MainLoop {
|
|||
/// The file system watcher, if running in watch mode.
|
||||
watcher: Option<ProjectWatcher>,
|
||||
|
||||
cli_options: Options,
|
||||
project_options_overrides: ProjectOptionsOverrides,
|
||||
}
|
||||
|
||||
impl MainLoop {
|
||||
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
|
||||
fn new(
|
||||
project_options_overrides: ProjectOptionsOverrides,
|
||||
) -> (Self, MainLoopCancellationToken) {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(10);
|
||||
|
||||
(
|
||||
|
@ -190,7 +201,7 @@ impl MainLoop {
|
|||
sender: sender.clone(),
|
||||
receiver,
|
||||
watcher: None,
|
||||
cli_options,
|
||||
project_options_overrides,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
|
@ -340,7 +351,7 @@ impl MainLoop {
|
|||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
revision += 1;
|
||||
// Automatically cancels any pending queries and waits for them to complete.
|
||||
db.apply_changes(changes, Some(&self.cli_options));
|
||||
db.apply_changes(changes, Some(&self.project_options_overrides));
|
||||
if let Some(watcher) = self.watcher.as_mut() {
|
||||
watcher.update(db);
|
||||
}
|
||||
|
|
|
@ -1700,6 +1700,63 @@ fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> {
|
|||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_file_override() -> anyhow::Result<()> {
|
||||
// Set `error-on-warning` to true in the configuration file
|
||||
// Explicitly set `--warn unresolved-reference` to ensure the rule warns instead of errors
|
||||
let case = TestCase::with_files(vec![
|
||||
("test.py", r"print(x) # [unresolved-reference]"),
|
||||
(
|
||||
"ty-override.toml",
|
||||
r#"
|
||||
[terminal]
|
||||
error-on-warning = true
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
// Ensure flag works via CLI arg
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config-file").arg("ty-override.toml"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
// Ensure the flag works via an environment variable
|
||||
assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").env("TY_CONFIG_FILE", "ty-override.toml"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unresolved-reference]: Name `x` used when not defined
|
||||
--> test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| ^
|
||||
|
|
||||
info: rule `unresolved-reference` was selected on the command line
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ use ruff_db::system::{
|
|||
};
|
||||
use ruff_db::{Db as _, Upcast};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ty_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use ty_project::metadata::options::{EnvironmentOptions, Options, ProjectOptionsOverrides};
|
||||
use ty_project::metadata::pyproject::{PyProject, Tool};
|
||||
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher};
|
||||
|
@ -164,8 +164,12 @@ impl TestCase {
|
|||
Ok(all_events)
|
||||
}
|
||||
|
||||
fn apply_changes(&mut self, changes: Vec<ChangeEvent>) {
|
||||
self.db.apply_changes(changes, None);
|
||||
fn apply_changes(
|
||||
&mut self,
|
||||
changes: Vec<ChangeEvent>,
|
||||
project_options_overrides: Option<&ProjectOptionsOverrides>,
|
||||
) {
|
||||
self.db.apply_changes(changes, project_options_overrides);
|
||||
}
|
||||
|
||||
fn update_options(&mut self, options: Options) -> anyhow::Result<()> {
|
||||
|
@ -180,7 +184,7 @@ impl TestCase {
|
|||
.context("Failed to write configuration")?;
|
||||
|
||||
let changes = self.take_watch_changes(event_for_file("pyproject.toml"));
|
||||
self.apply_changes(changes);
|
||||
self.apply_changes(changes, None);
|
||||
|
||||
if let Some(watcher) = &mut self.watcher {
|
||||
watcher.update(&self.db);
|
||||
|
@ -476,7 +480,7 @@ fn new_file() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
let foo = case.system_file(&foo_path).expect("foo.py to exist.");
|
||||
|
||||
|
@ -499,7 +503,7 @@ fn new_ignored_file() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(case.system_file(&foo_path).is_ok());
|
||||
case.assert_indexed_project_files([bar_file]);
|
||||
|
@ -535,7 +539,7 @@ fn new_non_project_file() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("black.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(case.system_file(&black_path).is_ok());
|
||||
|
||||
|
@ -576,7 +580,7 @@ fn new_files_with_explicit_included_paths() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("test2.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
let sub_a_file = case.system_file(&sub_a_path).expect("sub/a.py to exist");
|
||||
|
||||
|
@ -621,7 +625,7 @@ fn new_file_in_included_out_of_project_directory() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("script2.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
let src_a_file = case.system_file(&src_a).unwrap();
|
||||
let outside_b_file = case.system_file(&outside_b_path).unwrap();
|
||||
|
@ -648,7 +652,7 @@ fn changed_file() -> anyhow::Result<()> {
|
|||
|
||||
assert!(!changes.is_empty());
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
|
||||
case.assert_indexed_project_files([foo]);
|
||||
|
@ -671,7 +675,7 @@ fn deleted_file() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
case.assert_indexed_project_files([]);
|
||||
|
@ -703,7 +707,7 @@ fn move_file_to_trash() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
case.assert_indexed_project_files([]);
|
||||
|
@ -730,7 +734,7 @@ fn move_file_to_project() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
let foo_in_project = case.system_file(&foo_in_project)?;
|
||||
|
||||
|
@ -755,7 +759,7 @@ fn rename_file() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("bar.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
|
||||
|
@ -796,7 +800,7 @@ fn directory_moved_to_project() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
let init_file = case
|
||||
.system_file(sub_new_path.join("__init__.py"))
|
||||
|
@ -853,7 +857,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(
|
||||
|
@ -916,7 +920,7 @@ fn directory_renamed() -> anyhow::Result<()> {
|
|||
// Linux and windows only emit an event for the newly created root directory, but not for every new component.
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(
|
||||
|
@ -989,7 +993,7 @@ fn directory_deleted() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("sub"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(
|
||||
|
@ -1035,7 +1039,7 @@ fn search_path() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("a.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
case.assert_indexed_project_files([case.system_file(case.project_path("bar.py")).unwrap()]);
|
||||
|
@ -1066,7 +1070,7 @@ fn add_search_path() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("a.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), &ModuleName::new_static("a").unwrap()).is_some());
|
||||
|
||||
|
@ -1213,7 +1217,7 @@ fn changed_versions_file() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("VERSIONS"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert!(resolve_module(case.db(), &ModuleName::new("os").unwrap()).is_some());
|
||||
|
||||
|
@ -1267,7 +1271,7 @@ fn hard_links_in_project() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
|
||||
|
||||
|
@ -1338,7 +1342,7 @@ fn hard_links_to_target_outside_project() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(ChangeEvent::is_changed);
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 2')");
|
||||
|
||||
|
@ -1377,7 +1381,7 @@ mod unix {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("foo.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
|
@ -1460,7 +1464,7 @@ mod unix {
|
|||
|
||||
let changes = case.take_watch_changes(event_for_file("baz.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_file).as_str(),
|
||||
|
@ -1473,7 +1477,7 @@ mod unix {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("baz.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_file).as_str(),
|
||||
|
@ -1544,7 +1548,7 @@ mod unix {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("baz.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
// The file watcher is guaranteed to emit one event for the changed file, but it isn't specified
|
||||
// if the event is emitted for the "original" or linked path because both paths are watched.
|
||||
|
@ -1658,7 +1662,7 @@ mod unix {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("baz.py"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_original_file).as_str(),
|
||||
|
@ -1715,7 +1719,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(ChangeEvent::is_deleted);
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
// It should now pick up the outer project.
|
||||
assert_eq!(case.db().project().root(case.db()), case.root_path());
|
||||
|
@ -1781,7 +1785,73 @@ fn changes_to_user_configuration() -> anyhow::Result<()> {
|
|||
|
||||
let changes = case.stop_watch(event_for_file("ty.toml"));
|
||||
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
let diagnostics = case.db().check_file(foo);
|
||||
|
||||
assert!(
|
||||
diagnostics.len() == 1,
|
||||
"Expected exactly one diagnostic but got: {diagnostics:#?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changes_to_config_file_override() -> anyhow::Result<()> {
|
||||
let mut case = setup(|context: &mut SetupContext| {
|
||||
std::fs::write(
|
||||
context.join_project_path("pyproject.toml").as_std_path(),
|
||||
r#"
|
||||
[project]
|
||||
name = "test"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
std::fs::write(
|
||||
context.join_project_path("foo.py").as_std_path(),
|
||||
"a = 10 / 0",
|
||||
)?;
|
||||
|
||||
std::fs::write(
|
||||
context.join_project_path("ty-override.toml").as_std_path(),
|
||||
r#"
|
||||
[rules]
|
||||
division-by-zero = "ignore"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let foo = case
|
||||
.system_file(case.project_path("foo.py"))
|
||||
.expect("foo.py to exist");
|
||||
let diagnostics = case.db().check_file(foo);
|
||||
|
||||
assert!(
|
||||
diagnostics.is_empty(),
|
||||
"Expected no diagnostics but got: {diagnostics:#?}"
|
||||
);
|
||||
|
||||
// Enable division-by-zero in the explicitly specified configuration with warning severity
|
||||
update_file(
|
||||
case.project_path("ty-override.toml"),
|
||||
r#"
|
||||
[rules]
|
||||
division-by-zero = "warn"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let changes = case.stop_watch(event_for_file("ty-override.toml"));
|
||||
|
||||
case.apply_changes(
|
||||
changes,
|
||||
Some(&ProjectOptionsOverrides::new(
|
||||
Some(case.project_path("ty-override.toml")),
|
||||
Options::default(),
|
||||
)),
|
||||
);
|
||||
|
||||
let diagnostics = case.db().check_file(foo);
|
||||
|
||||
|
@ -1855,7 +1925,7 @@ fn rename_files_casing_only() -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
let changes = case.stop_watch(event_for_file("Lib.py"));
|
||||
case.apply_changes(changes);
|
||||
case.apply_changes(changes, None);
|
||||
|
||||
// Resolving `lib` should now fail but `Lib` should now succeed
|
||||
assert_eq!(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::db::{Db, ProjectDatabase};
|
||||
use crate::metadata::options::Options;
|
||||
use crate::metadata::options::ProjectOptionsOverrides;
|
||||
use crate::watch::{ChangeEvent, CreatedKind, DeletedKind};
|
||||
use crate::{Project, ProjectMetadata};
|
||||
use std::collections::BTreeSet;
|
||||
|
@ -12,10 +12,18 @@ use rustc_hash::FxHashSet;
|
|||
use ty_python_semantic::Program;
|
||||
|
||||
impl ProjectDatabase {
|
||||
#[tracing::instrument(level = "debug", skip(self, changes, cli_options))]
|
||||
pub fn apply_changes(&mut self, changes: Vec<ChangeEvent>, cli_options: Option<&Options>) {
|
||||
#[tracing::instrument(level = "debug", skip(self, changes, project_options_overrides))]
|
||||
pub fn apply_changes(
|
||||
&mut self,
|
||||
changes: Vec<ChangeEvent>,
|
||||
project_options_overrides: Option<&ProjectOptionsOverrides>,
|
||||
) {
|
||||
let mut project = self.project();
|
||||
let project_root = project.root(self).to_path_buf();
|
||||
let config_file_override =
|
||||
project_options_overrides.and_then(|options| options.config_file_override.clone());
|
||||
let options =
|
||||
project_options_overrides.map(|project_options| project_options.options.clone());
|
||||
let program = Program::get(self);
|
||||
let custom_stdlib_versions_path = program
|
||||
.custom_stdlib_search_path(self)
|
||||
|
@ -42,6 +50,14 @@ impl ProjectDatabase {
|
|||
tracing::trace!("Handle change: {:?}", change);
|
||||
|
||||
if let Some(path) = change.system_path() {
|
||||
if let Some(config_file) = &config_file_override {
|
||||
if config_file.as_path() == path {
|
||||
project_changed = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
path.file_name(),
|
||||
Some(".gitignore" | ".ignore" | "ty.toml" | "pyproject.toml")
|
||||
|
@ -170,10 +186,14 @@ impl ProjectDatabase {
|
|||
}
|
||||
|
||||
if project_changed {
|
||||
match ProjectMetadata::discover(&project_root, self.system()) {
|
||||
let new_project_metadata = match config_file_override {
|
||||
Some(config_file) => ProjectMetadata::from_config_file(config_file, self.system()),
|
||||
None => ProjectMetadata::discover(&project_root, self.system()),
|
||||
};
|
||||
match new_project_metadata {
|
||||
Ok(mut metadata) => {
|
||||
if let Some(cli_options) = cli_options {
|
||||
metadata.apply_cli_options(cli_options.clone());
|
||||
if let Some(cli_options) = options {
|
||||
metadata.apply_options(cli_options);
|
||||
}
|
||||
|
||||
if let Err(error) = metadata.apply_configuration_files(self.system()) {
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::walk::{ProjectFilesFilter, ProjectFilesWalker};
|
|||
pub use db::{Db, ProjectDatabase};
|
||||
use files::{Index, Indexed, IndexedFiles};
|
||||
use metadata::settings::Settings;
|
||||
pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
|
||||
pub use metadata::{ProjectMetadata, ProjectMetadataError};
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic, create_parse_diagnostic,
|
||||
create_unsupported_syntax_diagnostic,
|
||||
|
|
|
@ -48,6 +48,29 @@ impl ProjectMetadata {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_config_file(
|
||||
path: SystemPathBuf,
|
||||
system: &dyn System,
|
||||
) -> Result<Self, ProjectMetadataError> {
|
||||
tracing::debug!("Using overridden configuration file at '{path}'");
|
||||
|
||||
let config_file = ConfigurationFile::from_path(path.clone(), system).map_err(|error| {
|
||||
ProjectMetadataError::ConfigurationFileError {
|
||||
source: Box::new(error),
|
||||
path: path.clone(),
|
||||
}
|
||||
})?;
|
||||
|
||||
let options = config_file.into_options();
|
||||
|
||||
Ok(Self {
|
||||
name: Name::new(system.current_directory().file_name().unwrap_or("root")),
|
||||
root: system.current_directory().to_path_buf(),
|
||||
options,
|
||||
extra_configuration_paths: vec![path],
|
||||
})
|
||||
}
|
||||
|
||||
/// Loads a project from a `pyproject.toml` file.
|
||||
pub(crate) fn from_pyproject(
|
||||
pyproject: PyProject,
|
||||
|
@ -106,11 +129,11 @@ impl ProjectMetadata {
|
|||
pub fn discover(
|
||||
path: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> Result<ProjectMetadata, ProjectDiscoveryError> {
|
||||
) -> Result<ProjectMetadata, ProjectMetadataError> {
|
||||
tracing::debug!("Searching for a project in '{path}'");
|
||||
|
||||
if !system.is_directory(path) {
|
||||
return Err(ProjectDiscoveryError::NotADirectory(path.to_path_buf()));
|
||||
return Err(ProjectMetadataError::NotADirectory(path.to_path_buf()));
|
||||
}
|
||||
|
||||
let mut closest_project: Option<ProjectMetadata> = None;
|
||||
|
@ -125,7 +148,7 @@ impl ProjectMetadata {
|
|||
) {
|
||||
Ok(pyproject) => Some(pyproject),
|
||||
Err(error) => {
|
||||
return Err(ProjectDiscoveryError::InvalidPyProject {
|
||||
return Err(ProjectMetadataError::InvalidPyProject {
|
||||
path: pyproject_path,
|
||||
source: Box::new(error),
|
||||
});
|
||||
|
@ -144,7 +167,7 @@ impl ProjectMetadata {
|
|||
) {
|
||||
Ok(options) => options,
|
||||
Err(error) => {
|
||||
return Err(ProjectDiscoveryError::InvalidTyToml {
|
||||
return Err(ProjectMetadataError::InvalidTyToml {
|
||||
path: ty_toml_path,
|
||||
source: Box::new(error),
|
||||
});
|
||||
|
@ -171,7 +194,7 @@ impl ProjectMetadata {
|
|||
.and_then(|pyproject| pyproject.project.as_ref()),
|
||||
)
|
||||
.map_err(|err| {
|
||||
ProjectDiscoveryError::InvalidRequiresPythonConstraint {
|
||||
ProjectMetadataError::InvalidRequiresPythonConstraint {
|
||||
source: err,
|
||||
path: pyproject_path,
|
||||
}
|
||||
|
@ -185,7 +208,7 @@ impl ProjectMetadata {
|
|||
let metadata =
|
||||
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf())
|
||||
.map_err(
|
||||
|err| ProjectDiscoveryError::InvalidRequiresPythonConstraint {
|
||||
|err| ProjectMetadataError::InvalidRequiresPythonConstraint {
|
||||
source: err,
|
||||
path: pyproject_path,
|
||||
},
|
||||
|
@ -249,7 +272,7 @@ impl ProjectMetadata {
|
|||
}
|
||||
|
||||
/// Combine the project options with the CLI options where the CLI options take precedence.
|
||||
pub fn apply_cli_options(&mut self, options: Options) {
|
||||
pub fn apply_options(&mut self, options: Options) {
|
||||
self.options = options.combine(std::mem::take(&mut self.options));
|
||||
}
|
||||
|
||||
|
@ -282,7 +305,7 @@ impl ProjectMetadata {
|
|||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ProjectDiscoveryError {
|
||||
pub enum ProjectMetadataError {
|
||||
#[error("project path '{0}' is not a directory")]
|
||||
NotADirectory(SystemPathBuf),
|
||||
|
||||
|
@ -303,6 +326,12 @@ pub enum ProjectDiscoveryError {
|
|||
source: ResolveRequiresPythonError,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
|
||||
#[error("Error loading configuration file at {path}: {source}")]
|
||||
ConfigurationFileError {
|
||||
source: Box<ConfigurationFileError>,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -314,7 +343,7 @@ mod tests {
|
|||
use ruff_db::system::{SystemPathBuf, TestSystem};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
|
||||
use crate::{ProjectDiscoveryError, ProjectMetadata};
|
||||
use crate::{ProjectMetadata, ProjectMetadataError};
|
||||
|
||||
#[test]
|
||||
fn project_without_pyproject() -> anyhow::Result<()> {
|
||||
|
@ -1076,7 +1105,7 @@ expected `.`, `]`
|
|||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_error_eq(error: &ProjectDiscoveryError, message: &str) {
|
||||
fn assert_error_eq(error: &ProjectMetadataError, message: &str) {
|
||||
assert_eq!(error.to_string().replace('\\', "/"), message);
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,25 @@ pub(crate) struct ConfigurationFile {
|
|||
}
|
||||
|
||||
impl ConfigurationFile {
|
||||
pub(crate) fn from_path(
|
||||
path: SystemPathBuf,
|
||||
system: &dyn System,
|
||||
) -> Result<Self, ConfigurationFileError> {
|
||||
let ty_toml_str = system.read_to_string(&path).map_err(|source| {
|
||||
ConfigurationFileError::FileReadError {
|
||||
source,
|
||||
path: path.clone(),
|
||||
}
|
||||
})?;
|
||||
|
||||
match Options::from_toml_str(&ty_toml_str, ValueSource::File(Arc::new(path.clone()))) {
|
||||
Ok(options) => Ok(Self { path, options }),
|
||||
Err(error) => Err(ConfigurationFileError::InvalidTyToml {
|
||||
source: Box::new(error),
|
||||
path,
|
||||
}),
|
||||
}
|
||||
}
|
||||
/// Loads the user-level configuration file if it exists.
|
||||
///
|
||||
/// Returns `None` if the file does not exist or if the concept of user-level configurations
|
||||
|
@ -66,4 +85,10 @@ pub enum ConfigurationFileError {
|
|||
source: Box<TyTomlError>,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
#[error("Failed to read `{path}`: {source}")]
|
||||
FileReadError {
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::Db;
|
|||
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_macros::{Combine, OptionsMetadata};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
@ -575,3 +575,20 @@ impl OptionDiagnostic {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is a wrapper for options that actually get loaded from configuration files
|
||||
/// and the CLI, which also includes a `config_file_override` option that overrides
|
||||
/// default configuration discovery with an explicitly-provided path to a configuration file
|
||||
pub struct ProjectOptionsOverrides {
|
||||
pub config_file_override: Option<SystemPathBuf>,
|
||||
pub options: Options,
|
||||
}
|
||||
|
||||
impl ProjectOptionsOverrides {
|
||||
pub fn new(config_file_override: Option<SystemPathBuf>, options: Options) -> Self {
|
||||
Self {
|
||||
config_file_override,
|
||||
options,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue