Skip to content

Commit f452fa0

Browse files
committed
[ty] Add --config-file
217
1 parent 660375d commit f452fa0

File tree

10 files changed

+149
-25
lines changed

10 files changed

+149
-25
lines changed

crates/ty/docs/cli.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ ty check [OPTIONS] [PATH]...
4242
<li><code>always</code>: Always display colors</li>
4343
<li><code>never</code>: Never display colors</li>
4444
</ul></dd><dt id="ty-check--config"><a href="#ty-check--config"><code>--config</code></a>, <code>-c</code> <i>config-option</i></dt><dd><p>A TOML <code>&lt;KEY&gt; = &lt;VALUE&gt;</code> pair</p>
45+
</dd><dt id="ty-check--config-file"><a href="#ty-check--config-file"><code>--config-file</code></a> <i>config-file</i></dt><dd><p>A path to a <code>ty.toml</code> configuration file</p>
46+
<p><code>pyproject.toml</code> files are not accepted. When provided, this file will be used in place of any discovered configuration (including user-level configuration). ty will skip project discovery and default to the current working directory. Paths are anchored at the current working directory.</p>
4547
</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>
4648
</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>
4749
</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>

crates/ty/src/args.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ pub(crate) struct CheckCommand {
107107
#[clap(flatten)]
108108
pub(crate) config: ConfigsArg,
109109

110+
/// A path to a `ty.toml` configuration file
111+
///
112+
/// `pyproject.toml` files are not accepted.
113+
/// When provided, this file will be used in place of any discovered configuration (including user-level configuration).
114+
/// ty will skip project discovery and default to the current working directory.
115+
/// Paths are anchored at the current working directory.
116+
#[arg(long)]
117+
pub(crate) config_file: Option<SystemPathBuf>,
118+
110119
/// The format to use for printing diagnostic messages.
111120
#[arg(long)]
112121
pub(crate) output_format: Option<OutputFormat>,

crates/ty/src/lib.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
2323
use ruff_db::max_parallelism;
2424
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
2525
use salsa::plumbing::ZalsaDatabase;
26-
use ty_project::metadata::options::Options;
26+
use ty_project::metadata::options::MetaOptions;
2727
use ty_project::watch::ProjectWatcher;
2828
use ty_project::{Db, DummyReporter, Reporter, watch};
2929
use ty_project::{ProjectDatabase, ProjectMetadata};
@@ -102,12 +102,17 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
102102
.map(|path| SystemPath::absolute(path, &cwd))
103103
.collect();
104104

105-
let system = OsSystem::new(cwd);
105+
let system = OsSystem::new(cwd.clone());
106106
let watch = args.watch;
107107
let exit_zero = args.exit_zero;
108+
let config_file = args.config_file.clone();
109+
110+
let mut project_metadata = match &config_file {
111+
Some(config_file) => ProjectMetadata::from_config_file(config_file.clone(), cwd, &system)?,
112+
None => ProjectMetadata::discover(&project_path, &system)?,
113+
};
108114

109115
let cli_options = args.into_options();
110-
let mut project_metadata = ProjectMetadata::discover(&project_path, &system)?;
111116
project_metadata.apply_cli_options(cli_options.clone());
112117
project_metadata.apply_configuration_files(&system)?;
113118

@@ -117,7 +122,8 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
117122
db.project().set_included_paths(&mut db, check_paths);
118123
}
119124

120-
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
125+
let meta_options = MetaOptions::new(config_file, cli_options);
126+
let (main_loop, main_loop_cancellation_token) = MainLoop::new(meta_options);
121127

122128
// Listen to Ctrl+C and abort the watch mode.
123129
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
@@ -178,19 +184,19 @@ struct MainLoop {
178184
/// The file system watcher, if running in watch mode.
179185
watcher: Option<ProjectWatcher>,
180186

181-
cli_options: Options,
187+
meta_options: MetaOptions,
182188
}
183189

184190
impl MainLoop {
185-
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
191+
fn new(meta_options: MetaOptions) -> (Self, MainLoopCancellationToken) {
186192
let (sender, receiver) = crossbeam_channel::bounded(10);
187193

188194
(
189195
Self {
190196
sender: sender.clone(),
191197
receiver,
192198
watcher: None,
193-
cli_options,
199+
meta_options,
194200
},
195201
MainLoopCancellationToken { sender },
196202
)
@@ -338,7 +344,7 @@ impl MainLoop {
338344
MainLoopMessage::ApplyChanges(changes) => {
339345
revision += 1;
340346
// Automatically cancels any pending queries and waits for them to complete.
341-
db.apply_changes(changes, Some(&self.cli_options));
347+
db.apply_changes(changes, Some(&self.meta_options));
342348
if let Some(watcher) = self.watcher.as_mut() {
343349
watcher.update(db);
344350
}

crates/ty/tests/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1426,7 +1426,7 @@ fn cli_config_args_toml_string_basic() -> anyhow::Result<()> {
14261426
fn cli_config_args_overrides_knot_toml() -> anyhow::Result<()> {
14271427
let case = TestCase::with_files(vec![
14281428
(
1429-
"knot.toml",
1429+
"ty.toml",
14301430
r#"
14311431
[terminal]
14321432
error-on-warning = true

crates/ty_project/src/db/changes.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::db::{Db, ProjectDatabase};
2-
use crate::metadata::options::Options;
2+
use crate::metadata::options::MetaOptions;
33
use crate::watch::{ChangeEvent, CreatedKind, DeletedKind};
44
use crate::{Project, ProjectMetadata};
55
use std::collections::BTreeSet;
@@ -12,10 +12,13 @@ use rustc_hash::FxHashSet;
1212
use ty_python_semantic::Program;
1313

1414
impl ProjectDatabase {
15-
#[tracing::instrument(level = "debug", skip(self, changes, cli_options))]
16-
pub fn apply_changes(&mut self, changes: Vec<ChangeEvent>, cli_options: Option<&Options>) {
15+
#[tracing::instrument(level = "debug", skip(self, changes, meta_options))]
16+
pub fn apply_changes(&mut self, changes: Vec<ChangeEvent>, meta_options: Option<&MetaOptions>) {
1717
let mut project = self.project();
1818
let project_root = project.root(self).to_path_buf();
19+
let config_file_override =
20+
meta_options.and_then(|options| options.config_file_override.clone());
21+
let cli_options = meta_options.map(|options| options.cli_options.clone());
1922
let program = Program::get(self);
2023
let custom_stdlib_versions_path = program
2124
.custom_stdlib_search_path(self)
@@ -42,6 +45,14 @@ impl ProjectDatabase {
4245
tracing::trace!("Handle change: {:?}", change);
4346

4447
if let Some(path) = change.system_path() {
48+
if let Some(config_file) = &config_file_override {
49+
if path.eq(config_file.as_path()) {
50+
project_changed = true;
51+
52+
continue;
53+
}
54+
}
55+
4556
if matches!(
4657
path.file_name(),
4758
Some(".gitignore" | ".ignore" | "ty.toml" | "pyproject.toml")
@@ -170,10 +181,16 @@ impl ProjectDatabase {
170181
}
171182

172183
if project_changed {
173-
match ProjectMetadata::discover(&project_root, self.system()) {
184+
let new_project_metadata = match config_file_override {
185+
Some(config_file) => {
186+
ProjectMetadata::from_config_file(config_file, project_root, self.system())
187+
}
188+
None => ProjectMetadata::discover(&project_root, self.system()),
189+
};
190+
match new_project_metadata {
174191
Ok(mut metadata) => {
175192
if let Some(cli_options) = cli_options {
176-
metadata.apply_cli_options(cli_options.clone());
193+
metadata.apply_cli_options(cli_options);
177194
}
178195

179196
if let Err(error) = metadata.apply_configuration_files(self.system()) {

crates/ty_project/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::walk::{ProjectFilesFilter, ProjectFilesWalker};
55
pub use db::{Db, ProjectDatabase};
66
use files::{Index, Indexed, IndexedFiles};
77
use metadata::settings::Settings;
8-
pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
8+
pub use metadata::{ProjectMetadata, ProjectMetadataError};
99
use ruff_db::diagnostic::{
1010
Annotation, Diagnostic, DiagnosticId, Severity, Span, SubDiagnostic, create_parse_diagnostic,
1111
create_unsupported_syntax_diagnostic,

crates/ty_project/src/metadata.rs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ pub struct ProjectMetadata {
2727
/// The raw options
2828
pub(super) options: Options,
2929

30+
/// Config file to override any discovered configuration
31+
pub(super) config_file_override: Option<SystemPathBuf>,
32+
3033
/// Paths of configurations other than the project's configuration that were combined into [`Self::options`].
3134
///
3235
/// This field stores the paths of the configuration files, mainly for
@@ -45,9 +48,35 @@ impl ProjectMetadata {
4548
root,
4649
extra_configuration_paths: Vec::default(),
4750
options: Options::default(),
51+
config_file_override: None,
4852
}
4953
}
5054

55+
pub fn from_config_file(
56+
path: SystemPathBuf,
57+
root: SystemPathBuf,
58+
system: &dyn System,
59+
) -> Result<Self, ProjectMetadataError> {
60+
tracing::debug!("Using overridden configuration file at '{path}'");
61+
62+
let config_file = ConfigurationFile::from_path(path.clone(), system).map_err(|error| {
63+
ProjectMetadataError::ConfigurationFileError {
64+
source: Box::new(error),
65+
path: path.clone(),
66+
}
67+
})?;
68+
69+
let options = config_file.into_options();
70+
71+
Ok(Self {
72+
name: Name::new(root.file_name().unwrap_or("root")),
73+
root,
74+
options,
75+
extra_configuration_paths: Vec::new(),
76+
config_file_override: Some(path),
77+
})
78+
}
79+
5180
/// Loads a project from a `pyproject.toml` file.
5281
pub(crate) fn from_pyproject(
5382
pyproject: PyProject,
@@ -92,6 +121,7 @@ impl ProjectMetadata {
92121
root,
93122
options,
94123
extra_configuration_paths: Vec::new(),
124+
config_file_override: None,
95125
})
96126
}
97127

@@ -106,11 +136,11 @@ impl ProjectMetadata {
106136
pub fn discover(
107137
path: &SystemPath,
108138
system: &dyn System,
109-
) -> Result<ProjectMetadata, ProjectDiscoveryError> {
139+
) -> Result<ProjectMetadata, ProjectMetadataError> {
110140
tracing::debug!("Searching for a project in '{path}'");
111141

112142
if !system.is_directory(path) {
113-
return Err(ProjectDiscoveryError::NotADirectory(path.to_path_buf()));
143+
return Err(ProjectMetadataError::NotADirectory(path.to_path_buf()));
114144
}
115145

116146
let mut closest_project: Option<ProjectMetadata> = None;
@@ -125,7 +155,7 @@ impl ProjectMetadata {
125155
) {
126156
Ok(pyproject) => Some(pyproject),
127157
Err(error) => {
128-
return Err(ProjectDiscoveryError::InvalidPyProject {
158+
return Err(ProjectMetadataError::InvalidPyProject {
129159
path: pyproject_path,
130160
source: Box::new(error),
131161
});
@@ -144,7 +174,7 @@ impl ProjectMetadata {
144174
) {
145175
Ok(options) => options,
146176
Err(error) => {
147-
return Err(ProjectDiscoveryError::InvalidTyToml {
177+
return Err(ProjectMetadataError::InvalidTyToml {
148178
path: ty_toml_path,
149179
source: Box::new(error),
150180
});
@@ -171,7 +201,7 @@ impl ProjectMetadata {
171201
.and_then(|pyproject| pyproject.project.as_ref()),
172202
)
173203
.map_err(|err| {
174-
ProjectDiscoveryError::InvalidRequiresPythonConstraint {
204+
ProjectMetadataError::InvalidRequiresPythonConstraint {
175205
source: err,
176206
path: pyproject_path,
177207
}
@@ -185,7 +215,7 @@ impl ProjectMetadata {
185215
let metadata =
186216
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf())
187217
.map_err(
188-
|err| ProjectDiscoveryError::InvalidRequiresPythonConstraint {
218+
|err| ProjectMetadataError::InvalidRequiresPythonConstraint {
189219
source: err,
190220
path: pyproject_path,
191221
},
@@ -281,7 +311,7 @@ impl ProjectMetadata {
281311
}
282312

283313
#[derive(Debug, Error)]
284-
pub enum ProjectDiscoveryError {
314+
pub enum ProjectMetadataError {
285315
#[error("project path '{0}' is not a directory")]
286316
NotADirectory(SystemPathBuf),
287317

@@ -302,6 +332,12 @@ pub enum ProjectDiscoveryError {
302332
source: ResolveRequiresPythonError,
303333
path: SystemPathBuf,
304334
},
335+
336+
#[error("Error loading configuration file at {path}: {source}")]
337+
ConfigurationFileError {
338+
source: Box<ConfigurationFileError>,
339+
path: SystemPathBuf,
340+
},
305341
}
306342

307343
#[cfg(test)]
@@ -313,7 +349,7 @@ mod tests {
313349
use ruff_db::system::{SystemPathBuf, TestSystem};
314350
use ruff_python_ast::PythonVersion;
315351

316-
use crate::{ProjectDiscoveryError, ProjectMetadata};
352+
use crate::{ProjectMetadata, ProjectMetadataError};
317353

318354
#[test]
319355
fn project_without_pyproject() -> anyhow::Result<()> {
@@ -336,6 +372,7 @@ mod tests {
336372
name: Name("app"),
337373
root: "/app",
338374
options: Options(),
375+
config_file_override: None,
339376
)
340377
"#);
341378
});
@@ -374,6 +411,7 @@ mod tests {
374411
name: Name("backend"),
375412
root: "/app",
376413
options: Options(),
414+
config_file_override: None,
377415
)
378416
"#);
379417
});
@@ -471,6 +509,7 @@ expected `.`, `]`
471509
root: Some("src"),
472510
)),
473511
),
512+
config_file_override: None,
474513
)
475514
"#);
476515
});
@@ -521,6 +560,7 @@ expected `.`, `]`
521560
root: Some("src"),
522561
)),
523562
),
563+
config_file_override: None,
524564
)
525565
"#);
526566
});
@@ -561,6 +601,7 @@ expected `.`, `]`
561601
name: Name("nested-project"),
562602
root: "/app/packages/a",
563603
options: Options(),
604+
config_file_override: None,
564605
)
565606
"#);
566607
});
@@ -608,6 +649,7 @@ expected `.`, `]`
608649
r#python-version: Some("3.10"),
609650
)),
610651
),
652+
config_file_override: None,
611653
)
612654
"#);
613655
});
@@ -663,6 +705,7 @@ expected `.`, `]`
663705
root: Some("src"),
664706
)),
665707
),
708+
config_file_override: None,
666709
)
667710
"#);
668711
});
@@ -948,7 +991,7 @@ expected `.`, `]`
948991
}
949992

950993
#[track_caller]
951-
fn assert_error_eq(error: &ProjectDiscoveryError, message: &str) {
994+
fn assert_error_eq(error: &ProjectMetadataError, message: &str) {
952995
assert_eq!(error.to_string().replace('\\', "/"), message);
953996
}
954997

0 commit comments

Comments
 (0)