Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion crates/ty/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `./<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`

Expand Down
84 changes: 83 additions & 1 deletion crates/ty_project/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
28 changes: 25 additions & 3 deletions crates/ty_project/src/metadata/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand All @@ -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()]
}
};
Expand Down Expand Up @@ -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 `./<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")]
#[option(
default = r#"[".", "./src"]"#,
default = r#"null"#,
value_type = "str",
example = r#"
root = "./app"
Expand Down
2 changes: 1 addition & 1 deletion ty.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,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 `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path * otherwise, default to `.` (flat layout)",
"type": [
"string",
"null"
Expand Down
Loading