diff --git a/crates/ty/docs/configuration.md b/crates/ty/docs/configuration.md index 35aa8c4853..1d18d9d093 100644 --- a/crates/ty/docs/configuration.md +++ b/crates/ty/docs/configuration.md @@ -164,7 +164,13 @@ typeshed = "/path/to/custom/typeshed" 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 `.//` directory exists, include `.` and `./` in the first party search path +* otherwise, default to `.` (flat layout) + +**Default value**: `null` **Type**: `str` diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index d07f176504..71bc12264c 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -244,7 +244,8 @@ impl ProjectMetadata { } 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. @@ -947,6 +948,87 @@ expected `.`, `]` 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] fn assert_error_eq(error: &ProjectDiscoveryError, message: &str) { assert_eq!(error.to_string().replace('\\', "/"), message); diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index a5397c75e0..f8c83ad5b4 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -87,6 +87,7 @@ impl Options { pub(crate) fn to_program_settings( &self, project_root: &SystemPath, + project_name: &str, system: &dyn System, ) -> ProgramSettings { let python_version = self @@ -106,13 +107,14 @@ impl Options { ProgramSettings { python_version, 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( &self, project_root: &SystemPath, + 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()) @@ -121,10 +123,24 @@ impl Options { } else { 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) { + // 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] + } 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 { + // 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()] } }; @@ -353,9 +369,15 @@ pub struct EnvironmentOptions { #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct SrcOptions { /// 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 `.//` directory exists, include `.` and `./` in the first party search path + /// * otherwise, default to `.` (flat layout) #[serde(skip_serializing_if = "Option::is_none")] #[option( - default = r#"[".", "./src"]"#, + default = r#"null"#, value_type = "str", example = r#" root = "./app" diff --git a/ty.schema.json b/ty.schema.json index 6dcd55fcfd..0dae961d6d 100644 --- a/ty.schema.json +++ b/ty.schema.json @@ -849,7 +849,7 @@ "type": "object", "properties": { "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 `.//` directory exists, include `.` and `./` in the first party search path * otherwise, default to `.` (flat layout)", "type": [ "string", "null"