[ty] Add --config-file CLI arg (#18083)

This commit is contained in:
justin 2025-05-26 23:00:38 -07:00 committed by GitHub
parent 6453ac9ea1
commit 8d5655a7ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 300 additions and 63 deletions

View file

@ -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()) {

View file

@ -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,

View file

@ -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);
}

View file

@ -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,
},
}

View file

@ -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,
}
}
}