[ty] Ensure first-party search paths always appear in a sensible order (#20629)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This PR ensures that we always put `./src` before `.` in our list of
first-party search paths. This better emulates the fact that at runtime,
the module name of a file `src/foo.py` would almost certainly be `foo`
rather than `src.foo`.

I wondered if fixing this might fix
https://github.com/astral-sh/ruff/pull/20603#issuecomment-3345317444. It
seems like that's not the case, but it also seems like it leads to
better diagnostics because we report much more intuitive module names to
the user in our error messages -- so, it's probably a good change
anyway.
This commit is contained in:
Alex Waygood 2025-09-29 21:19:13 +01:00 committed by GitHub
parent 0092794302
commit 8664842d00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 55 additions and 17 deletions

View file

@ -188,6 +188,40 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any
Ok(())
}
/// If `.` and `./src` are both registered as first-party search paths,
/// the `./src` directory should take precedence for module resolution,
/// because it is relative to `.`.
#[test]
fn src_subdirectory_takes_precedence_over_repo_root() -> anyhow::Result<()> {
let case = CliTest::with_files([(
"src/package/__init__.py",
"from . import nonexistent_submodule",
)])?;
// If `./src` didn't take priority over `.` here, we would report
// "Module `src.package` has no member `nonexistent_submodule`"
// instead of "Module `package` has no member `nonexistent_submodule`".
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package` has no member `nonexistent_submodule`
--> src/package/__init__.py:1:15
|
1 | from . import nonexistent_submodule
| ^^^^^^^^^^^^^^^^^^^^^
|
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(())
}
/// This tests that, even if no Python *version* has been specified on the CLI or in a config file,
/// ty is still able to infer the Python version from a `--python` argument on the CLI,
/// *even if* the `--python` argument points to a system installation.
@ -1738,8 +1772,8 @@ fn default_root_tests_package() -> anyhow::Result<()> {
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: 1. <temp_dir>/src (first-party code)
info: 2. <temp_dir>/ (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
@ -1814,8 +1848,8 @@ fn default_root_python_package() -> anyhow::Result<()> {
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: 1. <temp_dir>/src (first-party code)
info: 2. <temp_dir>/ (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
@ -1861,8 +1895,8 @@ fn default_root_python_package_pyi() -> anyhow::Result<()> {
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: 1. <temp_dir>/src (first-party code)
info: 2. <temp_dir>/ (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
@ -1902,8 +1936,8 @@ fn pythonpath_is_respected() -> anyhow::Result<()> {
3 | print(f"{baz.it}")
|
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: 1. <temp_dir>/src (first-party code)
info: 2. <temp_dir>/ (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
@ -1959,8 +1993,8 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> {
3 | import foo
|
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: 1. <temp_dir>/src (first-party code)
info: 2. <temp_dir>/ (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
@ -1975,8 +2009,8 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> {
5 | print(f"{baz.it}")
|
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: 1. <temp_dir>/src (first-party code)
info: 2. <temp_dir>/ (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

View file

@ -237,15 +237,16 @@ impl Options {
.map(|root| root.absolute(project_root, system))
.collect()
} else {
let mut roots = vec![];
let src = project_root.join("src");
let mut roots = 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 `.` and `./src` in `environment.root` because a `./src` directory exists"
);
vec![project_root.to_path_buf(), src]
roots.push(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`
@ -253,12 +254,11 @@ impl Options {
"Including `.` and `/{project_name}` in `environment.root` because a `./{project_name}/{project_name}` directory exists"
);
vec![project_root.to_path_buf(), project_root.join(project_name)]
roots.push(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!("Including `.` in `environment.root`");
vec![project_root.to_path_buf()]
};
}
let python = project_root.join("python");
if system.is_directory(&python)
@ -293,6 +293,10 @@ impl Options {
roots.push(tests_dir);
}
// The project root should always be included, and should always come
// after any subdirectories such as `./src`, `./tests` and/or `./python`.
roots.push(project_root.to_path_buf());
roots
};