Default `src.root` to `['.', '<project_name>']` if the directory exists (#18141)

This commit is contained in:
Micha Reiser 2025-05-19 18:11:27 +02:00 committed by GitHub
parent 97058e8093
commit 55a410a885
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 116 additions and 6 deletions

View File

@ -164,7 +164,13 @@ typeshed = "/path/to/custom/typeshed"
The root of the project, used for finding first-party modules. The root of the project, used for finding first-party modules.
**Default value**: `[".", "./src"]` 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)
**Default value**: `null`
**Type**: `str` **Type**: `str`

View File

@ -244,7 +244,8 @@ impl ProjectMetadata {
} }
pub fn to_program_settings(&self, system: &dyn System) -> ProgramSettings { pub fn to_program_settings(&self, system: &dyn System) -> ProgramSettings {
self.options.to_program_settings(self.root(), system) self.options
.to_program_settings(self.root(), self.name(), system)
} }
/// Combine the project options with the CLI options where the CLI options take precedence. /// Combine the project options with the CLI options where the CLI options take precedence.
@ -947,6 +948,87 @@ expected `.`, `]`
Ok(()) Ok(())
} }
#[test]
fn no_src_root_src_layout() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file_all(
root.join("src/main.py"),
r#"
print("Hello, world!")
"#,
)
.context("Failed to write file")?;
let metadata = ProjectMetadata::discover(&root, &system)?;
let settings = metadata
.options
.to_program_settings(&root, "my_package", &system);
assert_eq!(
settings.search_paths.src_roots,
vec![root.clone(), root.join("src")]
);
Ok(())
}
#[test]
fn no_src_root_package_layout() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file_all(
root.join("psycopg/psycopg/main.py"),
r#"
print("Hello, world!")
"#,
)
.context("Failed to write file")?;
let metadata = ProjectMetadata::discover(&root, &system)?;
let settings = metadata
.options
.to_program_settings(&root, "psycopg", &system);
assert_eq!(
settings.search_paths.src_roots,
vec![root.clone(), root.join("psycopg")]
);
Ok(())
}
#[test]
fn no_src_root_flat_layout() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file_all(
root.join("my_package/main.py"),
r#"
print("Hello, world!")
"#,
)
.context("Failed to write file")?;
let metadata = ProjectMetadata::discover(&root, &system)?;
let settings = metadata
.options
.to_program_settings(&root, "my_package", &system);
assert_eq!(settings.search_paths.src_roots, vec![root]);
Ok(())
}
#[track_caller] #[track_caller]
fn assert_error_eq(error: &ProjectDiscoveryError, message: &str) { fn assert_error_eq(error: &ProjectDiscoveryError, message: &str) {
assert_eq!(error.to_string().replace('\\', "/"), message); assert_eq!(error.to_string().replace('\\', "/"), message);

View File

@ -87,6 +87,7 @@ impl Options {
pub(crate) fn to_program_settings( pub(crate) fn to_program_settings(
&self, &self,
project_root: &SystemPath, project_root: &SystemPath,
project_name: &str,
system: &dyn System, system: &dyn System,
) -> ProgramSettings { ) -> ProgramSettings {
let python_version = self let python_version = self
@ -106,13 +107,14 @@ impl Options {
ProgramSettings { ProgramSettings {
python_version, python_version,
python_platform, python_platform,
search_paths: self.to_search_path_settings(project_root, system), search_paths: self.to_search_path_settings(project_root, project_name, system),
} }
} }
fn to_search_path_settings( fn to_search_path_settings(
&self, &self,
project_root: &SystemPath, project_root: &SystemPath,
project_name: &str,
system: &dyn System, system: &dyn System,
) -> SearchPathSettings { ) -> SearchPathSettings {
let src_roots = if let Some(src_root) = self.src.as_ref().and_then(|src| src.root.as_ref()) let src_roots = if let Some(src_root) = self.src.as_ref().and_then(|src| src.root.as_ref())
@ -121,10 +123,24 @@ impl Options {
} else { } else {
let src = project_root.join("src"); let src = project_root.join("src");
// Default to `src` and the project root if `src` exists and the root hasn't been specified.
if system.is_directory(&src) { if system.is_directory(&src) {
// Default to `src` and the project root if `src` exists and the root hasn't been specified.
// This corresponds to the `src-layout`
tracing::debug!(
"Including `./src` in `src.root` because a `./src` directory exists"
);
vec![project_root.to_path_buf(), src] vec![project_root.to_path_buf(), src]
} else if system.is_directory(&project_root.join(project_name).join(project_name)) {
// `src-layout` but when the folder isn't called `src` but has the same name as the project.
// For example, the "src" folder for `psycopg` is called `psycopg` and the python files are in `psycopg/psycopg/_adapters_map.py`
tracing::debug!(
"Including `./{project_name}` in `src.root` because a `./{project_name}/{project_name}` directory exists"
);
vec![project_root.to_path_buf(), project_root.join(project_name)]
} else { } else {
// Default to a [flat project structure](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/).
tracing::debug!("Defaulting `src.root` to `.`");
vec![project_root.to_path_buf()] vec![project_root.to_path_buf()]
} }
}; };
@ -353,9 +369,15 @@ pub struct EnvironmentOptions {
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SrcOptions { pub struct SrcOptions {
/// The root of the project, used for finding first-party modules. /// 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)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[option( #[option(
default = r#"[".", "./src"]"#, default = r#"null"#,
value_type = "str", value_type = "str",
example = r#" example = r#"
root = "./app" root = "./app"

2
ty.schema.json generated
View File

@ -849,7 +849,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"root": { "root": {
"description": "The root of the project, used for finding first-party modules.", "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)",
"type": [ "type": [
"string", "string",
"null" "null"