[ty] Rename `src.root` setting to `environment.root` (#18760)

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Micha Reiser 2025-06-24 14:40:44 +02:00 committed by GitHub
parent 2c4c015f74
commit 0194452928
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 281 additions and 50 deletions

View File

@ -735,6 +735,9 @@ pub enum DiagnosticId {
/// # no `[overrides.rules]`
/// ```
UselessOverridesSection,
/// Use of a deprecated setting.
DeprecatedSetting,
}
impl DiagnosticId {
@ -773,6 +776,7 @@ impl DiagnosticId {
DiagnosticId::EmptyInclude => "empty-include",
DiagnosticId::UnnecessaryOverridesSection => "unnecessary-overrides-section",
DiagnosticId::UselessOverridesSection => "useless-overrides-section",
DiagnosticId::DeprecatedSetting => "deprecated-setting",
}
}

View File

@ -20,6 +20,7 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenS
Ok(quote! {
#[automatically_derived]
impl crate::combine::Combine for #ident {
#[allow(deprecated)]
fn combine_with(&mut self, other: Self) {
#(
#output

View File

@ -132,6 +132,32 @@ python-version = "3.12"
---
#### `root`
The root of the project, used for finding first-party modules.
If left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly:
* if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat)
* if a `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path
* otherwise, default to `.` (flat layout)
Besides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file),
it will also be included in the first party search path.
**Default value**: `null`
**Type**: `str`
**Example usage** (`pyproject.toml`):
```toml
[tool.ty.environment]
root = "./app"
```
---
#### `typeshed`
Optional path to a "typeshed" directory on disk for us to use for standard-library types.
@ -388,6 +414,9 @@ respect-ignore-files = false
#### `root`
> [!WARN] "Deprecated"
> This option has been deprecated. Use `environment.root` instead.
The root of the project, used for finding first-party modules.
If left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly:

View File

@ -193,6 +193,7 @@ impl CheckCommand {
.map(RelativePathBuf::cli)
.collect()
}),
..EnvironmentOptions::default()
}),
terminal: Some(TerminalOptions {
output_format: self

View File

@ -929,3 +929,123 @@ fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn src_root_deprecation_warning() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.src]
root = "./src"
"#,
),
("src/test.py", ""),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: true
exit_code: 0
----- stdout -----
warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead.
--> pyproject.toml:3:8
|
2 | [tool.ty.src]
3 | root = "./src"
| ^^^^^^^
|
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(())
}
#[test]
fn src_root_deprecation_warning_with_environment_root() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.src]
root = "./src"
[tool.ty.environment]
root = "./app"
"#,
),
("app/test.py", ""),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: true
exit_code: 0
----- stdout -----
warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead.
--> pyproject.toml:3:8
|
2 | [tool.ty.src]
3 | root = "./src"
| ^^^^^^^
4 |
5 | [tool.ty.environment]
|
info: The `src.root` setting was ignored in favor of the `environment.root` setting
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(())
}
#[test]
fn environment_root_takes_precedence_over_src_root() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.src]
root = "./src"
[tool.ty.environment]
root = "./app"
"#,
),
("src/test.py", "import my_module"),
(
"app/my_module.py",
"# This module exists in app/ but not src/",
),
])?;
// The test should pass because environment.root points to ./app where my_module.py exists
// If src.root took precedence, it would fail because my_module.py doesn't exist in ./src
assert_cmd_snapshot!(case.command(), @r#"
success: true
exit_code: 0
----- stdout -----
warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead.
--> pyproject.toml:3:8
|
2 | [tool.ty.src]
3 | root = "./src"
| ^^^^^^^
4 |
5 | [tool.ty.environment]
|
info: The `src.root` setting was ignored in favor of the `environment.root` setting
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(())
}

View File

@ -105,10 +105,12 @@ impl Options {
project_name: &str,
system: &dyn System,
) -> ProgramSettings {
let python_version = self
.environment
let environment = self.environment.or_default();
let python_version =
environment
.python_version
.as_ref()
.and_then(|env| env.python_version.as_ref())
.map(|ranged_version| PythonVersionWithSource {
version: **ranged_version,
source: match ranged_version.source() {
@ -118,10 +120,10 @@ impl Options {
),
},
});
let python_platform = self
.environment
.as_ref()
.and_then(|env| env.python_platform.as_deref().cloned())
let python_platform = environment
.python_platform
.as_deref()
.cloned()
.unwrap_or_else(|| {
let default = PythonPlatform::default();
tracing::info!("Defaulting to python-platform `{default}`");
@ -140,7 +142,15 @@ impl Options {
project_name: &str,
system: &dyn System,
) -> SearchPathSettings {
let src_roots = if let Some(src_root) = self.src.as_ref().and_then(|src| src.root.as_ref())
let environment = self.environment.or_default();
let src = self.src.or_default();
#[allow(deprecated)]
let src_roots = if let Some(src_root) = self
.environment
.as_ref()
.and_then(|environment| environment.root.as_ref())
.or_else(|| src.root.as_ref())
{
vec![src_root.absolute(project_root, system)]
} else {
@ -185,27 +195,22 @@ impl Options {
roots
};
let (extra_paths, python, typeshed) = self
.environment
.as_ref()
.map(|env| {
(
env.extra_paths.clone(),
env.python.clone(),
env.typeshed.clone(),
)
})
.unwrap_or_default();
SearchPathSettings {
extra_paths: extra_paths
extra_paths: environment
.extra_paths
.as_deref()
.unwrap_or_default()
.into_iter()
.iter()
.map(|path| path.absolute(project_root, system))
.collect(),
src_roots,
custom_typeshed: typeshed.map(|path| path.absolute(project_root, system)),
python_path: python
custom_typeshed: environment
.typeshed
.as_ref()
.map(|path| path.absolute(project_root, system)),
python_path: environment
.python
.as_ref()
.map(|python_path| {
let origin = match python_path.source() {
ValueSource::Cli => SysPrefixPathOrigin::PythonCliFlag,
@ -253,11 +258,35 @@ impl Options {
error_on_warning: terminal_options.error_on_warning.unwrap_or_default(),
};
let src_options = if let Some(src) = self.src.as_ref() {
Cow::Borrowed(src)
} else {
Cow::Owned(SrcOptions::default())
};
let src_options = self.src.or_default();
#[allow(deprecated)]
if let Some(src_root) = src_options.root.as_ref() {
let mut diagnostic = OptionDiagnostic::new(
DiagnosticId::DeprecatedSetting,
"The `src.root` setting is deprecated. Use `environment.root` instead.".to_string(),
Severity::Warning,
);
if let Some(file) = src_root
.source()
.file()
.and_then(|path| system_path_to_file(db.upcast(), path).ok())
{
diagnostic = diagnostic.with_annotation(Some(Annotation::primary(
Span::from(file).with_optional_range(src_root.range()),
)));
}
if self.environment.or_default().root.is_some() {
diagnostic = diagnostic.sub(SubDiagnostic::new(
Severity::Info,
"The `src.root` setting was ignored in favor of the `environment.root` setting",
));
}
diagnostics.push(diagnostic);
}
let src = src_options
.to_settings(db, project_root, &mut diagnostics)
@ -291,11 +320,7 @@ impl Options {
db: &dyn Db,
diagnostics: &mut Vec<OptionDiagnostic>,
) -> RuleSelection {
if let Some(rules) = self.rules.as_ref() {
rules.to_rule_selection(db, diagnostics)
} else {
RuleSelection::from_registry(db.lint_registry())
}
self.rules.or_default().to_rule_selection(db, diagnostics)
}
fn to_overrides_settings(
@ -327,6 +352,26 @@ impl Options {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct EnvironmentOptions {
/// The root of the project, used for finding first-party modules.
///
/// If left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly:
///
/// * if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat)
/// * if a `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path
/// * otherwise, default to `.` (flat layout)
///
/// Besides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file),
/// it will also be included in the first party search path.
#[serde(skip_serializing_if = "Option::is_none")]
#[option(
default = r#"null"#,
value_type = "str",
example = r#"
root = "./app"
"#
)]
pub root: Option<RelativePathBuf>,
/// Specifies the version of Python that will be used to analyze the source code.
/// The version should be specified as a string in the format `M.m` where `M` is the major version
/// and `m` is the minor (e.g. `"3.0"` or `"3.6"`).
@ -443,6 +488,7 @@ pub struct SrcOptions {
root = "./app"
"#
)]
#[deprecated(note = "Use `environment.root` instead.")]
pub root: Option<RelativePathBuf>,
/// Whether to automatically exclude files that are ignored by `.ignore`,
@ -1056,8 +1102,10 @@ impl RangedValue<OverrideOptions> {
global_rules: Option<&Rules>,
diagnostics: &mut Vec<OptionDiagnostic>,
) -> Result<Option<Override>, Box<OptionDiagnostic>> {
let rules = self.rules.or_default();
// First, warn about incorrect or useless overrides.
if self.rules.as_ref().is_none_or(Rules::is_empty) {
if rules.is_empty() {
let mut diagnostic = OptionDiagnostic::new(
DiagnosticId::UselessOverridesSection,
"Useless `overrides` section".to_string(),
@ -1174,11 +1222,11 @@ impl RangedValue<OverrideOptions> {
let files = IncludeExcludeFilter::new(include, exclude);
// Merge global rules with override rules, with override rules taking precedence
let merged_rules = self
.rules
.clone()
.combine(global_rules.cloned())
.expect("method to have early returned if rules is None");
let mut merged_rules = rules.into_owned();
if let Some(global_rules) = global_rules {
merged_rules = merged_rules.combine(global_rules.clone());
}
// Convert merged rules to rule selection
let rule_selection = merged_rules.to_rule_selection(db, diagnostics);
@ -1391,3 +1439,23 @@ impl ProjectOptionsOverrides {
}
}
}
trait OrDefault {
type Target: ToOwned;
fn or_default(&self) -> Cow<'_, Self::Target>;
}
impl<T> OrDefault for Option<T>
where
T: Default + ToOwned<Owned = T>,
{
type Target = T;
fn or_default(&self) -> Cow<'_, Self::Target> {
match self {
Some(value) => Cow::Borrowed(value),
None => Cow::Owned(T::default()),
}
}
}

8
ty.schema.json generated
View File

@ -119,6 +119,13 @@
}
]
},
"root": {
"description": "The root of the project, used for finding first-party modules.\n\nIf left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly:\n\n* if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) * if a `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path * otherwise, default to `.` (flat layout)\n\nBesides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), it will also be included in the first party search path.",
"type": [
"string",
"null"
]
},
"typeshed": {
"description": "Optional path to a \"typeshed\" directory on disk for us to use for standard-library types. If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib, bundled as a zip file in the binary",
"type": [
@ -958,6 +965,7 @@
},
"root": {
"description": "The root of the project, used for finding first-party modules.\n\nIf left unspecified, ty will try to detect common project layouts and initialize `src.root` accordingly:\n\n* if a `./src` directory exists, include `.` and `./src` in the first party search path (src layout or flat) * if a `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path * otherwise, default to `.` (flat layout)\n\nBesides, if a `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` file), it will also be included in the first party search path.",
"deprecated": true,
"type": [
"string",
"null"