[ty] Include python folder in environment.root if it exists (#20263)

## Summary

I felt it was safer to add the `python` folder *in addition* to a
possibly-existing `src` folder, even though the `src` folder only
contains Rust code for `maturin`-based projects. There might be
non-maturin projects where a `python` folder exists for other reasons,
next to a normal `src` layout.

closes https://github.com/astral-sh/ty/issues/1120

## Test Plan

Tested locally on the egglog-python project.
This commit is contained in:
David Peter 2025-09-05 13:53:48 +02:00 committed by GitHub
parent 8ade6c4eaf
commit 7ee863b6d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 144 additions and 3 deletions

View file

@ -144,7 +144,7 @@ If left unspecified, ty will try to detect common project layouts and initialize
* if a `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path * if a `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path
* otherwise, default to `.` (flat layout) * 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), Besides, if a `./python` or `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` or `__init__.pyi` file),
it will also be included in the first party search path. it will also be included in the first party search path.
**Default value**: `null` **Default value**: `null`

View file

@ -1752,3 +1752,126 @@ fn default_root_tests_package() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[test]
fn default_root_python_folder() -> anyhow::Result<()> {
let case = CliTest::with_files([
("src/foo.py", "foo = 10"),
("python/bar.py", "bar = 20"),
(
"python/test_bar.py",
r#"
from foo import foo
from bar import bar
print(f"{foo} {bar}")
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
");
Ok(())
}
/// If `python/__init__.py` is present, it is considered a package and `python` is not added to search paths.
#[test]
fn default_root_python_package() -> anyhow::Result<()> {
let case = CliTest::with_files([
("src/foo.py", "foo = 10"),
("python/__init__.py", ""),
("python/bar.py", "bar = 20"),
(
"python/test_bar.py",
r#"
from foo import foo
from bar import bar # expected unresolved import
print(f"{foo} {bar}")
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `bar`
--> python/test_bar.py:3:6
|
2 | from foo import foo
3 | from bar import bar # expected unresolved import
| ^^^
4 |
5 | print(f"{foo} {bar}")
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. <temp_dir>/src (first-party code)
info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
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(())
}
/// Similarly, if `python/__init__.pyi` is present, it is considered a package and `python` is not added to search paths.
#[test]
fn default_root_python_package_pyi() -> anyhow::Result<()> {
let case = CliTest::with_files([
("src/foo.py", "foo = 10"),
("python/__init__.pyi", ""),
("python/bar.py", "bar = 20"),
(
"python/test_bar.py",
r#"
from foo import foo
from bar import bar # expected unresolved import
print(f"{foo} {bar}")
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `bar`
--> python/test_bar.py:3:6
|
2 | from foo import foo
3 | from bar import bar # expected unresolved import
| ^^^
4 |
5 | print(f"{foo} {bar}")
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. <temp_dir>/src (first-party code)
info: 3. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
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

@ -259,11 +259,29 @@ impl Options {
vec![project_root.to_path_buf()] vec![project_root.to_path_buf()]
}; };
let python = project_root.join("python");
if system.is_directory(&python)
&& !system.is_file(&python.join("__init__.py"))
&& !system.is_file(&python.join("__init__.pyi"))
&& !roots.contains(&python)
{
// If a `./python` directory exists, include it as a source root. This is the recommended layout
// for maturin-based rust/python projects [1].
//
// https://github.com/PyO3/maturin/blob/979fe1db42bb9e58bc150fa6fc45360b377288bf/README.md?plain=1#L88-L99
tracing::debug!(
"Including `./python` in `environment.root` because a `./python` directory exists"
);
roots.push(python);
}
// Considering pytest test discovery conventions, // Considering pytest test discovery conventions,
// we also include the `tests` directory if it exists and is not a package. // we also include the `tests` directory if it exists and is not a package.
let tests_dir = project_root.join("tests"); let tests_dir = project_root.join("tests");
if system.is_directory(&tests_dir) if system.is_directory(&tests_dir)
&& !system.is_file(&tests_dir.join("__init__.py")) && !system.is_file(&tests_dir.join("__init__.py"))
&& !system.is_file(&tests_dir.join("__init__.pyi"))
&& !roots.contains(&tests_dir) && !roots.contains(&tests_dir)
{ {
// If the `tests` directory exists and is not a package, include it as a source root. // If the `tests` directory exists and is not a package, include it as a source root.
@ -428,7 +446,7 @@ pub struct EnvironmentOptions {
/// * if a `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path /// * if a `./<project-name>/<project-name>` directory exists, include `.` and `./<project-name>` in the first party search path
/// * otherwise, default to `.` (flat layout) /// * 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), /// Besides, if a `./python` or `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` or `__init__.pyi` file),
/// it will also be included in the first party search path. /// it will also be included in the first party search path.
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
#[option( #[option(

2
ty.schema.json generated
View file

@ -101,7 +101,7 @@
] ]
}, },
"root": { "root": {
"description": "The root paths of the project, used for finding first-party modules.\n\nAccepts a list of directory paths searched in priority order (first has highest priority).\n\nIf left unspecified, ty will try to detect common project layouts and initialize `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.", "description": "The root paths of the project, used for finding first-party modules.\n\nAccepts a list of directory paths searched in priority order (first has highest priority).\n\nIf left unspecified, ty will try to detect common project layouts and initialize `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 `./python` or `./tests` directory exists and is not a package (i.e. it does not contain an `__init__.py` or `__init__.pyi` file), it will also be included in the first party search path.",
"type": [ "type": [
"array", "array",
"null" "null"