Skip to content

Commit 93d17a1

Browse files
committed
[ty] Add --config-file
217
1 parent 3d55a16 commit 93d17a1

File tree

8 files changed

+121
-25
lines changed

8 files changed

+121
-25
lines changed

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: 14 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)
@@ -170,10 +173,16 @@ impl ProjectDatabase {
170173
}
171174

172175
if project_changed {
173-
match ProjectMetadata::discover(&project_root, self.system()) {
176+
let new_project_metadata = match config_file_override {
177+
Some(config_file) => {
178+
ProjectMetadata::from_config_file(config_file, project_root, self.system())
179+
}
180+
None => ProjectMetadata::discover(&project_root, self.system()),
181+
};
182+
match new_project_metadata {
174183
Ok(mut metadata) => {
175184
if let Some(cli_options) = cli_options {
176-
metadata.apply_cli_options(cli_options.clone());
185+
metadata.apply_cli_options(cli_options);
177186
}
178187

179188
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: 43 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,33 @@ 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+
let options = config_file.into_options();
69+
Ok(Self {
70+
name: Name::new(root.file_name().unwrap_or("root")),
71+
root,
72+
options,
73+
extra_configuration_paths: Vec::new(),
74+
config_file_override: Some(path),
75+
})
76+
}
77+
5178
/// Loads a project from a `pyproject.toml` file.
5279
pub(crate) fn from_pyproject(
5380
pyproject: PyProject,
@@ -92,6 +119,7 @@ impl ProjectMetadata {
92119
root,
93120
options,
94121
extra_configuration_paths: Vec::new(),
122+
config_file_override: None,
95123
})
96124
}
97125

@@ -106,11 +134,11 @@ impl ProjectMetadata {
106134
pub fn discover(
107135
path: &SystemPath,
108136
system: &dyn System,
109-
) -> Result<ProjectMetadata, ProjectDiscoveryError> {
137+
) -> Result<ProjectMetadata, ProjectMetadataError> {
110138
tracing::debug!("Searching for a project in '{path}'");
111139

112140
if !system.is_directory(path) {
113-
return Err(ProjectDiscoveryError::NotADirectory(path.to_path_buf()));
141+
return Err(ProjectMetadataError::NotADirectory(path.to_path_buf()));
114142
}
115143

116144
let mut closest_project: Option<ProjectMetadata> = None;
@@ -125,7 +153,7 @@ impl ProjectMetadata {
125153
) {
126154
Ok(pyproject) => Some(pyproject),
127155
Err(error) => {
128-
return Err(ProjectDiscoveryError::InvalidPyProject {
156+
return Err(ProjectMetadataError::InvalidPyProject {
129157
path: pyproject_path,
130158
source: Box::new(error),
131159
});
@@ -144,7 +172,7 @@ impl ProjectMetadata {
144172
) {
145173
Ok(options) => options,
146174
Err(error) => {
147-
return Err(ProjectDiscoveryError::InvalidTyToml {
175+
return Err(ProjectMetadataError::InvalidTyToml {
148176
path: ty_toml_path,
149177
source: Box::new(error),
150178
});
@@ -171,7 +199,7 @@ impl ProjectMetadata {
171199
.and_then(|pyproject| pyproject.project.as_ref()),
172200
)
173201
.map_err(|err| {
174-
ProjectDiscoveryError::InvalidRequiresPythonConstraint {
202+
ProjectMetadataError::InvalidRequiresPythonConstraint {
175203
source: err,
176204
path: pyproject_path,
177205
}
@@ -185,7 +213,7 @@ impl ProjectMetadata {
185213
let metadata =
186214
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf())
187215
.map_err(
188-
|err| ProjectDiscoveryError::InvalidRequiresPythonConstraint {
216+
|err| ProjectMetadataError::InvalidRequiresPythonConstraint {
189217
source: err,
190218
path: pyproject_path,
191219
},
@@ -281,7 +309,7 @@ impl ProjectMetadata {
281309
}
282310

283311
#[derive(Debug, Error)]
284-
pub enum ProjectDiscoveryError {
312+
pub enum ProjectMetadataError {
285313
#[error("project path '{0}' is not a directory")]
286314
NotADirectory(SystemPathBuf),
287315

@@ -302,6 +330,12 @@ pub enum ProjectDiscoveryError {
302330
source: ResolveRequiresPythonError,
303331
path: SystemPathBuf,
304332
},
333+
334+
#[error("Error loading configuration file at {path}: {source}")]
335+
ConfigurationFileError {
336+
source: Box<ConfigurationFileError>,
337+
path: SystemPathBuf,
338+
},
305339
}
306340

307341
#[cfg(test)]
@@ -313,7 +347,7 @@ mod tests {
313347
use ruff_db::system::{SystemPathBuf, TestSystem};
314348
use ruff_python_ast::PythonVersion;
315349

316-
use crate::{ProjectDiscoveryError, ProjectMetadata};
350+
use crate::{ProjectMetadata, ProjectMetadataError};
317351

318352
#[test]
319353
fn project_without_pyproject() -> anyhow::Result<()> {
@@ -948,7 +982,7 @@ expected `.`, `]`
948982
}
949983

950984
#[track_caller]
951-
fn assert_error_eq(error: &ProjectDiscoveryError, message: &str) {
985+
fn assert_error_eq(error: &ProjectMetadataError, message: &str) {
952986
assert_eq!(error.to_string().replace('\\', "/"), message);
953987
}
954988

crates/ty_project/src/metadata/configuration_file.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@ pub(crate) struct ConfigurationFile {
1414
}
1515

1616
impl ConfigurationFile {
17+
pub(crate) fn from_path(
18+
path: SystemPathBuf,
19+
system: &dyn System,
20+
) -> Result<Self, ConfigurationFileError> {
21+
let ty_toml_str = system.read_to_string(&path).map_err(|source| {
22+
ConfigurationFileError::FileReadError {
23+
source,
24+
path: path.clone(),
25+
}
26+
})?;
27+
match Options::from_toml_str(&ty_toml_str, ValueSource::File(Arc::new(path.clone()))) {
28+
Ok(options) => Ok(Self { path, options }),
29+
Err(error) => Err(ConfigurationFileError::InvalidTyToml {
30+
source: Box::new(error),
31+
path,
32+
}),
33+
}
34+
}
1735
/// Loads the user-level configuration file if it exists.
1836
///
1937
/// Returns `None` if the file does not exist or if the concept of user-level configurations
@@ -66,4 +84,10 @@ pub enum ConfigurationFileError {
6684
source: Box<TyTomlError>,
6785
path: SystemPathBuf,
6886
},
87+
#[error("Failed to read `{path}`: {source}")]
88+
FileReadError {
89+
#[source]
90+
source: std::io::Error,
91+
path: SystemPathBuf,
92+
},
6993
}

crates/ty_project/src/metadata/options.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::Db;
22
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
33
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, Severity, Span};
44
use ruff_db::files::system_path_to_file;
5-
use ruff_db::system::{System, SystemPath};
5+
use ruff_db::system::{System, SystemPath, SystemPathBuf};
66
use ruff_macros::{Combine, OptionsMetadata};
77
use ruff_python_ast::PythonVersion;
88
use rustc_hash::FxHashMap;
@@ -516,3 +516,17 @@ impl OptionDiagnostic {
516516
}
517517
}
518518
}
519+
520+
pub struct MetaOptions {
521+
pub config_file_override: Option<SystemPathBuf>,
522+
pub cli_options: Options,
523+
}
524+
525+
impl MetaOptions {
526+
pub fn new(config_file_override: Option<SystemPathBuf>, cli_options: Options) -> Self {
527+
Self {
528+
config_file_override,
529+
cli_options,
530+
}
531+
}
532+
}

0 commit comments

Comments
 (0)