ruff/crates/ty/tests/cli/python_environment.rs
Brent Westbrook e64d772788
Standardize syntax error construction (#20903)
Summary
--

This PR unifies the two different ways Ruff and ty construct syntax
errors. Ruff has been storing the primary message in the diagnostic
itself, while ty attached the message to the primary annotation:

```
> ruff check try.py
invalid-syntax: name capture `x` makes remaining patterns unreachable
 --> try.py:2:10
  |
1 | match 42:
2 |     case x: ...
  |          ^
3 |     case y: ...
  |

Found 1 error.
> uvx ty check try.py
WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors.
Checking ------------------------------------------------------------ 1/1 files                                                                                                 
error[invalid-syntax]
 --> try.py:2:10
  |
1 | match 42:
2 |     case x: ...
  |          ^ name capture `x` makes remaining patterns unreachable
3 |     case y: ...
  |

Found 1 diagnostic
```

I think there are benefits to both approaches, and I do like ty's
version, but I feel like we should pick one (and it might help with
#20901 eventually). I slightly prefer Ruff's version, so I went with
that. Hopefully this isn't too controversial, but I'm happy to close
this if it is.

Note that this shouldn't change any other diagnostic formats in ty
because
[`Diagnostic::primary_message`](98d27c4128/crates/ruff_db/src/diagnostic/mod.rs (L177))
was already falling back to the primary annotation message if the
diagnostic message was empty. As a result, I think this change will
partially resolve the FIXME therein.

Test Plan
--

Existing tests with updated snapshots
2025-10-16 11:56:32 -04:00

2181 lines
62 KiB
Rust

use insta_cmd::assert_cmd_snapshot;
use ruff_python_ast::PythonVersion;
use crate::CliTest;
/// Specifying an option on the CLI should take precedence over the same setting in the
/// project's configuration. Here, this is tested for the Python version.
#[test]
fn config_override_python_version() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python-version = "3.11"
"#,
),
(
"test.py",
r#"
import sys
# Access `sys.last_exc` that was only added in Python 3.12
print(sys.last_exc)
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[unresolved-attribute]: Type `<module 'sys'>` has no attribute `last_exc`
--> test.py:5:7
|
4 | # Access `sys.last_exc` that was only added in Python 3.12
5 | print(sys.last_exc)
| ^^^^^^^^^^^^
|
info: Python 3.11 was assumed when accessing `last_exc`
--> pyproject.toml:3:18
|
2 | [tool.ty.environment]
3 | python-version = "3.11"
| ^^^^^^ Python 3.11 assumed due to this configuration setting
|
info: rule `unresolved-attribute` is enabled by default
Found 1 diagnostic
----- stderr -----
"#);
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
Ok(())
}
/// Same as above, but for the Python platform.
#[test]
fn config_override_python_platform() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python-platform = "linux"
"#,
),
(
"test.py",
r#"
import sys
from typing_extensions import reveal_type
reveal_type(sys.platform)
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
info[revealed-type]: Revealed type
--> test.py:5:13
|
3 | from typing_extensions import reveal_type
4 |
5 | reveal_type(sys.platform)
| ^^^^^^^^^^^^ `Literal["linux"]`
|
Found 1 diagnostic
----- stderr -----
"###);
assert_cmd_snapshot!(case.command().arg("--python-platform").arg("all"), @r###"
success: true
exit_code: 0
----- stdout -----
info[revealed-type]: Revealed type
--> test.py:5:13
|
3 | from typing_extensions import reveal_type
4 |
5 | reveal_type(sys.platform)
| ^^^^^^^^^^^^ `LiteralString`
|
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}
#[test]
fn config_file_annotation_showing_where_python_version_set_typing_error() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python-version = "3.8"
"#,
),
(
"test.py",
r#"
aiter
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:2:1
|
2 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.8 was assumed when resolving types
--> pyproject.toml:3:18
|
2 | [tool.ty.environment]
3 | python-version = "3.8"
| ^^^^^ Python 3.8 assumed due to this configuration setting
|
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:2:1
|
2 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.9 was assumed when resolving types because it was specified on the command line
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
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 -----
"###);
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.
///
/// We currently cannot infer the Python version from a system installation on Windows:
/// on Windows, we can only infer the Python version from a virtual environment.
/// This is because we use the layout of the Python installation to infer the Python version:
/// on Unix, the `site-packages` directory of an installation will be located at
/// `<sys.prefix>/lib/pythonX.Y/site-packages`. On Windows, however, the `site-packages`
/// directory will be located at `<sys.prefix>/Lib/site-packages`, which doesn't give us the
/// same information.
#[cfg(not(windows))]
#[test]
fn python_version_inferred_from_system_installation() -> anyhow::Result<()> {
let cpython_case = CliTest::with_files([
("pythons/Python3.8/bin/python", ""),
("pythons/Python3.8/lib/python3.8/site-packages/foo.py", ""),
("test.py", "aiter"),
])?;
assert_cmd_snapshot!(cpython_case.command().arg("--python").arg("pythons/Python3.8/bin/python"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:1:1
|
1 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.8 was assumed when resolving types because of the layout of your Python installation
info: The primary `site-packages` directory of your installation was found at `lib/python3.8/site-packages/`
info: No Python version was specified on the command line or in a configuration file
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
let pypy_case = CliTest::with_files([
("pythons/pypy3.8/bin/python", ""),
("pythons/pypy3.8/lib/pypy3.8/site-packages/foo.py", ""),
("test.py", "aiter"),
])?;
assert_cmd_snapshot!(pypy_case.command().arg("--python").arg("pythons/pypy3.8/bin/python"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:1:1
|
1 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.8 was assumed when resolving types because of the layout of your Python installation
info: The primary `site-packages` directory of your installation was found at `lib/pypy3.8/site-packages/`
info: No Python version was specified on the command line or in a configuration file
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
let free_threaded_case = CliTest::with_files([
("pythons/Python3.13t/bin/python", ""),
(
"pythons/Python3.13t/lib/python3.13t/site-packages/foo.py",
"",
),
("test.py", "import string.templatelib"),
])?;
assert_cmd_snapshot!(free_threaded_case.command().arg("--python").arg("pythons/Python3.13t/bin/python"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `string.templatelib`
--> test.py:1:8
|
1 | import string.templatelib
| ^^^^^^^^^^^^^^^^^^
|
info: The stdlib module `string.templatelib` is only available on Python 3.14+
info: Python 3.13 was assumed when resolving modules because of the layout of your Python installation
info: The primary `site-packages` directory of your installation was found at `lib/python3.13t/site-packages/`
info: No Python version was specified on the command line or in a configuration file
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}
/// On Unix systems, it's common for a Python installation at `.venv/bin/python` to only be a symlink
/// to a system Python installation. We must be careful not to resolve the symlink too soon!
/// If we do, we will incorrectly add the system installation's `site-packages` as a search path,
/// when we should be adding the virtual environment's `site-packages` directory as a search path instead.
#[cfg(unix)]
#[test]
fn python_argument_points_to_symlinked_executable() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"system-installation/lib/python3.13/site-packages/foo.py",
"",
),
("system-installation/bin/python", ""),
(
"strange-venv-location/lib/python3.13/site-packages/bar.py",
"",
),
(
"test.py",
"\
import foo
import bar",
),
])?;
case.write_symlink(
"system-installation/bin/python",
"strange-venv-location/bin/python",
)?;
assert_cmd_snapshot!(case.command().arg("--python").arg("strange-venv-location/bin/python"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `foo`
--> test.py:1:8
|
1 | import foo
| ^^^
2 | import bar
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/strange-venv-location/lib/python3.13/site-packages (site-packages)
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 -----
"###);
Ok(())
}
/// On Unix systems, a virtual environment can come with multiple `site-packages` directories:
/// one at `<sys.prefix>/lib/pythonX.Y/site-packages` and one at
/// `<sys.prefix>/lib64/pythonX.Y/site-packages`. According to [the stdlib docs], the `lib64`
/// is not *meant* to have any Python files in it (only C extensions and similar). Empirically,
/// however, it sometimes does indeed have Python files in it: popular tools such as poetry
/// appear to sometimes install Python packages into the `lib64` site-packages directory even
/// though they probably shouldn't. We therefore check for both a `lib64` and a `lib` directory,
/// and add them both as search paths if they both exist.
///
/// See:
/// - <https://github.com/astral-sh/ty/issues/1043>
/// - <https://github.com/astral-sh/ty/issues/257>.
///
/// [the stdlib docs]: https://docs.python.org/3/library/sys.html#sys.platlibdir
#[cfg(unix)]
#[test]
fn lib64_site_packages_directory_on_unix() -> anyhow::Result<()> {
let case = CliTest::with_files([
(".venv/lib/python3.13/site-packages/foo.py", ""),
(".venv/lib64/python3.13/site-packages/bar.py", ""),
("test.py", "import foo, bar, baz"),
])?;
assert_cmd_snapshot!(case.command().arg("--python").arg(".venv"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `baz`
--> test.py:1:18
|
1 | import foo, bar, baz
| ^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/ (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: 3. <temp_dir>/.venv/lib/python3.13/site-packages (site-packages)
info: 4. <temp_dir>/.venv/lib64/python3.13/site-packages (site-packages)
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 -----
"###);
Ok(())
}
#[test]
fn many_search_paths() -> anyhow::Result<()> {
let case = CliTest::with_files([
("extra1/foo1.py", ""),
("extra2/foo2.py", ""),
("extra3/foo3.py", ""),
("extra4/foo4.py", ""),
("extra5/foo5.py", ""),
("extra6/foo6.py", ""),
("test.py", "import foo1, baz"),
])?;
assert_cmd_snapshot!(
case.command()
.arg("--python-platform").arg("linux")
.arg("--extra-search-path").arg("extra1")
.arg("--extra-search-path").arg("extra2")
.arg("--extra-search-path").arg("extra3")
.arg("--extra-search-path").arg("extra4")
.arg("--extra-search-path").arg("extra5")
.arg("--extra-search-path").arg("extra6"),
@r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `baz`
--> test.py:1:14
|
1 | import foo1, baz
| ^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/extra1 (extra search path specified on the CLI or in your config file)
info: 2. <temp_dir>/extra2 (extra search path specified on the CLI or in your config file)
info: 3. <temp_dir>/extra3 (extra search path specified on the CLI or in your config file)
info: 4. <temp_dir>/extra4 (extra search path specified on the CLI or in your config file)
info: 5. <temp_dir>/extra5 (extra search path specified on the CLI or in your config file)
info: ... and 3 more paths. Run with `-v` to see all paths.
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 -----
");
// Shows all with `-v`
assert_cmd_snapshot!(
case.command()
.arg("--python-platform").arg("linux")
.arg("--extra-search-path").arg("extra1")
.arg("--extra-search-path").arg("extra2")
.arg("--extra-search-path").arg("extra3")
.arg("--extra-search-path").arg("extra4")
.arg("--extra-search-path").arg("extra5")
.arg("--extra-search-path").arg("extra6")
.arg("-v"),
@r"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `baz`
--> test.py:1:14
|
1 | import foo1, baz
| ^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/extra1 (extra search path specified on the CLI or in your config file)
info: 2. <temp_dir>/extra2 (extra search path specified on the CLI or in your config file)
info: 3. <temp_dir>/extra3 (extra search path specified on the CLI or in your config file)
info: 4. <temp_dir>/extra4 (extra search path specified on the CLI or in your config file)
info: 5. <temp_dir>/extra5 (extra search path specified on the CLI or in your config file)
info: 6. <temp_dir>/extra6 (extra search path specified on the CLI or in your config file)
info: 7. <temp_dir>/ (first-party code)
info: 8. 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 -----
INFO Python version: Python 3.14, platform: linux
INFO Indexed 7 file(s) in 0.000s
");
Ok(())
}
#[test]
fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python = "venv"
"#,
),
(
"venv/pyvenv.cfg",
r#"
version = 3.8
home = foo/bar/bin
"#,
),
if cfg!(target_os = "windows") {
("foo/bar/bin/python.exe", "")
} else {
("foo/bar/bin/python", "")
},
if cfg!(target_os = "windows") {
("venv/Lib/site-packages/foo.py", "")
} else {
("venv/lib/python3.8/site-packages/foo.py", "")
},
("test.py", "aiter"),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:1:1
|
1 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.8 was assumed when resolving types because of your virtual environment
--> venv/pyvenv.cfg:2:11
|
2 | version = 3.8
| ^^^ Python version inferred from virtual environment metadata file
3 | home = foo/bar/bin
|
info: No Python version was specified on the command line or in a configuration file
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}
#[test]
fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python = "venv"
"#,
),
(
"venv/pyvenv.cfg",
r#"home = foo/bar/bin
version = 3.8"#,
),
if cfg!(target_os = "windows") {
("foo/bar/bin/python.exe", "")
} else {
("foo/bar/bin/python", "")
},
if cfg!(target_os = "windows") {
("venv/Lib/site-packages/foo.py", "")
} else {
("venv/lib/python3.8/site-packages/foo.py", "")
},
("test.py", "aiter"),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `aiter` used when not defined
--> test.py:1:1
|
1 | aiter
| ^^^^^
|
info: `aiter` was added as a builtin in Python 3.10
info: Python 3.8 was assumed when resolving types because of your virtual environment
--> venv/pyvenv.cfg:4:23
|
4 | version = 3.8
| ^^^ Python version inferred from virtual environment metadata file
|
info: No Python version was specified on the command line or in a configuration file
info: rule `unresolved-reference` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}
#[test]
fn config_file_annotation_showing_where_python_version_set_syntax_error() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[project]
requires-python = ">=3.8"
"#,
),
(
"test.py",
r#"
match object():
case int():
pass
case _:
pass
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]: Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10)
--> test.py:2:1
|
2 | match object():
| ^^^^^
3 | case int():
4 | pass
|
info: Python 3.8 was assumed when parsing syntax
--> pyproject.toml:3:19
|
2 | [project]
3 | requires-python = ">=3.8"
| ^^^^^^^ Python 3.8 assumed due to this configuration setting
|
Found 1 diagnostic
----- stderr -----
"#);
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r"
success: false
exit_code: 1
----- stdout -----
error[invalid-syntax]: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
--> test.py:2:1
|
2 | match object():
| ^^^^^
3 | case int():
4 | pass
|
info: Python 3.9 was assumed when parsing syntax because it was specified on the command line
Found 1 diagnostic
----- stderr -----
");
Ok(())
}
#[test]
fn python_cli_argument_virtual_environment() -> anyhow::Result<()> {
let path_to_executable = if cfg!(windows) {
"my-venv/Scripts/python.exe"
} else {
"my-venv/bin/python"
};
let other_venv_path = "my-venv/foo/some_other_file.txt";
let case = CliTest::with_files([
("test.py", ""),
(
if cfg!(windows) {
"my-venv/Lib/site-packages/foo.py"
} else {
"my-venv/lib/python3.13/site-packages/foo.py"
},
"",
),
(path_to_executable, ""),
(other_venv_path, ""),
])?;
// Passing a path to the installation works
assert_cmd_snapshot!(case.command().arg("--python").arg("my-venv"), @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
// And so does passing a path to the executable inside the installation
assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
// But random other paths inside the installation are rejected
assert_cmd_snapshot!(case.command().arg("--python").arg(other_venv_path), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ty failed
Cause: Invalid `--python` argument `<temp_dir>/my-venv/foo/some_other_file.txt`: does not point to a Python executable or a directory on disk
"###);
// And so are paths that do not exist on disk
assert_cmd_snapshot!(case.command().arg("--python").arg("not-a-directory-or-executable"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ty failed
Cause: Invalid `--python` argument `<temp_dir>/not-a-directory-or-executable`: does not point to a Python executable or a directory on disk
Cause: No such file or directory (os error 2)
"###);
Ok(())
}
#[test]
fn python_cli_argument_system_installation() -> anyhow::Result<()> {
let path_to_executable = if cfg!(windows) {
"Python3.11/python.exe"
} else {
"Python3.11/bin/python"
};
let case = CliTest::with_files([
("test.py", ""),
(
if cfg!(windows) {
"Python3.11/Lib/site-packages/foo.py"
} else {
"Python3.11/lib/python3.11/site-packages/foo.py"
},
"",
),
(path_to_executable, ""),
])?;
// Passing a path to the installation works
assert_cmd_snapshot!(case.command().arg("--python").arg("Python3.11"), @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
// And so does passing a path to the executable inside the installation
assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
Ok(())
}
#[test]
fn config_file_broken_python_setting() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[project]
name = "test"
version = "0.1.0"
description = "Some description"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []
[tool.ty.environment]
python = "not-a-directory-or-executable"
"#,
),
("test.py", ""),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ty failed
Cause: Invalid `environment.python` setting
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
9 |
10 | [tool.ty.environment]
11 | python = "not-a-directory-or-executable"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not point to a Python executable or a directory on disk
|
Cause: No such file or directory (os error 2)
"###);
Ok(())
}
#[test]
fn config_file_python_setting_directory_with_no_site_packages() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python = "directory-but-no-site-packages"
"#,
),
("directory-but-no-site-packages/lib/foo.py", ""),
("test.py", ""),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ty failed
Cause: Failed to discover the site-packages directory
Cause: Invalid `environment.python` setting
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
1 |
2 | [tool.ty.environment]
3 | python = "directory-but-no-site-packages"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not find a `site-packages` directory for this Python installation/executable
|
"###);
Ok(())
}
// This error message is never emitted on Windows, because Windows installations have simpler layouts
#[cfg(not(windows))]
#[test]
fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.environment]
python = "directory-but-no-site-packages"
"#,
),
("directory-but-no-site-packages/foo.py", ""),
("test.py", ""),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ty failed
Cause: Failed to discover the site-packages directory
Cause: Failed to iterate over the contents of the `lib`/`lib64` directories of the Python installation
--> Invalid setting in configuration file `<temp_dir>/pyproject.toml`
|
1 |
2 | [tool.ty.environment]
3 | python = "directory-but-no-site-packages"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
"###);
Ok(())
}
#[test]
fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"ty.toml",
&*format!(
r#"
[environment]
python-version = "{}"
python-platform = "linux"
"#,
PythonVersion::default()
),
),
(
"main.py",
r#"
import os
os.grantpt(1) # only available on unix, Python 3.13 or newer
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[unresolved-attribute]: Type `<module 'os'>` has no attribute `grantpt`
--> main.py:4:1
|
2 | import os
3 |
4 | os.grantpt(1) # only available on unix, Python 3.13 or newer
| ^^^^^^^^^^
|
info: Python 3.10 was assumed when accessing `grantpt`
--> ty.toml:3:18
|
2 | [environment]
3 | python-version = "3.10"
| ^^^^^^ Python 3.10 assumed due to this configuration setting
4 | python-platform = "linux"
|
info: rule `unresolved-attribute` is enabled by default
Found 1 diagnostic
----- stderr -----
"#);
// Use default (which should be latest supported)
let case = CliTest::with_files([
(
"ty.toml",
r#"
[environment]
python-platform = "linux"
"#,
),
(
"main.py",
r#"
import os
os.grantpt(1) # only available on unix, Python 3.13 or newer
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
Ok(())
}
/// The `site-packages` directory is used by ty for external import.
/// Ty does the following checks to discover the `site-packages` directory in the order:
/// 1) If `VIRTUAL_ENV` environment variable is set
/// 2) If `CONDA_PREFIX` environment variable is set (and .filename == `CONDA_DEFAULT_ENV`)
/// 3) If a `.venv` directory exists at the project root
/// 4) If `CONDA_PREFIX` environment variable is set (and .filename != `CONDA_DEFAULT_ENV`)
/// or if `_CONDA_ROOT` is set (and `_CONDA_ROOT` == `CONDA_PREFIX`)
///
/// This test (and the next one) is aiming at validating the logic around these cases.
///
/// To do this we create a program that has these 4 imports:
///
/// ```python
/// from package1 import ActiveVenv
/// from package1 import ChildConda
/// from package1 import WorkingVenv
/// from package1 import BaseConda
/// ```
///
/// We then create 4 different copies of package1. Each copy defines all of these
/// classes... except the one that describes it. Therefore we know we got e.g.
/// the working venv if we get a diagnostic like this:
///
/// ```text
/// Unresolved import
/// 4 | from package1 import WorkingVenv
/// | ^^^^^^^^^^^
/// ```
///
/// This test uses a directory structure as follows:
///
/// ├── project
/// │ ├── test.py
/// │ └── .venv
/// │ ├── pyvenv.cfg
/// │ └── lib
/// │ └── python3.13
/// │ └── site-packages
/// │ └── package1
/// │ └── __init__.py
/// ├── myvenv
/// │ ├── pyvenv.cfg
/// │ └── lib
/// │ └── python3.13
/// │ └── site-packages
/// │ └── package1
/// │ └── __init__.py
/// └── conda
/// ├── lib
/// │ └── python3.13
/// │ └── site-packages
/// │ └── package1
/// │ └── __init__.py
/// └── envs
/// └── conda-env
/// └── lib
/// └── python3.13
/// └── site-packages
/// └── package1
/// └── __init__.py
///
/// test.py imports package1
/// And the command is run in the `child` directory.
#[test]
fn check_venv_resolution_with_working_venv() -> anyhow::Result<()> {
let child_conda_package1_path = if cfg!(windows) {
"conda/envs/conda-env/Lib/site-packages/package1/__init__.py"
} else {
"conda/envs/conda-env/lib/python3.13/site-packages/package1/__init__.py"
};
let base_conda_package1_path = if cfg!(windows) {
"conda/Lib/site-packages/package1/__init__.py"
} else {
"conda//lib/python3.13/site-packages/package1/__init__.py"
};
let working_venv_package1_path = if cfg!(windows) {
"project/.venv/Lib/site-packages/package1/__init__.py"
} else {
"project/.venv/lib/python3.13/site-packages/package1/__init__.py"
};
let active_venv_package1_path = if cfg!(windows) {
"myvenv/Lib/site-packages/package1/__init__.py"
} else {
"myvenv/lib/python3.13/site-packages/package1/__init__.py"
};
let case = CliTest::with_files([
(
"project/test.py",
r#"
from package1 import ActiveVenv
from package1 import ChildConda
from package1 import WorkingVenv
from package1 import BaseConda
"#,
),
(
"project/.venv/pyvenv.cfg",
r#"
home = ./
"#,
),
(
"myvenv/pyvenv.cfg",
r#"
home = ./
"#,
),
(
active_venv_package1_path,
r#"
class ChildConda: ...
class WorkingVenv: ...
class BaseConda: ...
"#,
),
(
child_conda_package1_path,
r#"
class ActiveVenv: ...
class WorkingVenv: ...
class BaseConda: ...
"#,
),
(
working_venv_package1_path,
r#"
class ActiveVenv: ...
class ChildConda: ...
class BaseConda: ...
"#,
),
(
base_conda_package1_path,
r#"
class ActiveVenv: ...
class ChildConda: ...
class WorkingVenv: ...
"#,
),
])?;
// Run with nothing set, should find the working venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project")), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `WorkingVenv`
--> test.py:4:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
| ^^^^^^^^^^^
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// Run with VIRTUAL_ENV set, should find the active venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
--> test.py:2:22
|
2 | from package1 import ActiveVenv
| ^^^^^^^^^^
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with CONDA_PREFIX set, should find the child conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env")), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find working venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("CONDA_DEFAULT_ENV", "base"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `WorkingVenv`
--> test.py:4:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
| ^^^^^^^^^^^
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal) and VIRTUAL_ENV set,
// should find child active venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("CONDA_DEFAULT_ENV", "base")
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
--> test.py:2:22
|
2 | from package1 import ActiveVenv
| ^^^^^^^^^^
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (equal!) set, should find ChildConda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env"))
.env("CONDA_DEFAULT_ENV", "conda-env"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with _CONDA_ROOT and CONDA_PREFIX (unequal!) set, should find ChildConda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env"))
.env("_CONDA_ROOT", "conda"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with _CONDA_ROOT and CONDA_PREFIX (equal!) set, should find BaseConda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("_CONDA_ROOT", "conda"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `BaseConda`
--> test.py:5:22
|
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
| ^^^^^^^^^
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}
/// The exact same test as above, but without a working venv
///
/// In this case the Base Conda should be a possible outcome.
#[test]
fn check_venv_resolution_without_working_venv() -> anyhow::Result<()> {
let child_conda_package1_path = if cfg!(windows) {
"conda/envs/conda-env/Lib/site-packages/package1/__init__.py"
} else {
"conda/envs/conda-env/lib/python3.13/site-packages/package1/__init__.py"
};
let base_conda_package1_path = if cfg!(windows) {
"conda/Lib/site-packages/package1/__init__.py"
} else {
"conda/lib/python3.13/site-packages/package1/__init__.py"
};
let active_venv_package1_path = if cfg!(windows) {
"myvenv/Lib/site-packages/package1/__init__.py"
} else {
"myvenv/lib/python3.13/site-packages/package1/__init__.py"
};
let case = CliTest::with_files([
(
"project/test.py",
r#"
from package1 import ActiveVenv
from package1 import ChildConda
from package1 import WorkingVenv
from package1 import BaseConda
"#,
),
(
"myvenv/pyvenv.cfg",
r#"
home = ./
"#,
),
(
active_venv_package1_path,
r#"
class ChildConda: ...
class WorkingVenv: ...
class BaseConda: ...
"#,
),
(
child_conda_package1_path,
r#"
class ActiveVenv: ...
class WorkingVenv: ...
class BaseConda: ...
"#,
),
(
base_conda_package1_path,
r#"
class ActiveVenv: ...
class ChildConda: ...
class WorkingVenv: ...
"#,
),
])?;
// Run with nothing set, should fail to find anything
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project")), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `package1`
--> test.py:2:6
|
2 | from package1 import ActiveVenv
| ^^^^^^^^
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. 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
error[unresolved-import]: Cannot resolve imported module `package1`
--> test.py:3:6
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. 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
error[unresolved-import]: Cannot resolve imported module `package1`
--> test.py:4:6
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
| ^^^^^^^^
5 | from package1 import BaseConda
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. 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
error[unresolved-import]: Cannot resolve imported module `package1`
--> test.py:5:6
|
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
| ^^^^^^^^
|
info: Searched in the following paths during module resolution:
info: 1. <temp_dir>/project (first-party code)
info: 2. 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 4 diagnostics
----- stderr -----
"###);
// Run with VIRTUAL_ENV set, should find the active venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
--> test.py:2:22
|
2 | from package1 import ActiveVenv
| ^^^^^^^^^^
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with CONDA_PREFIX set, should find the child conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env")), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV set (unequal), should find base conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("CONDA_DEFAULT_ENV", "base"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `BaseConda`
--> test.py:5:22
|
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
| ^^^^^^^^^
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal) and VIRTUAL_ENV set,
// should find child active venv
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("CONDA_DEFAULT_ENV", "base")
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
--> test.py:2:22
|
2 | from package1 import ActiveVenv
| ^^^^^^^^^^
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with CONDA_PREFIX and CONDA_DEFAULT_ENV (unequal!) set, should find base conda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("CONDA_DEFAULT_ENV", "base"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `BaseConda`
--> test.py:5:22
|
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
| ^^^^^^^^^
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with _CONDA_ROOT and CONDA_PREFIX (unequal!) set, should find ChildConda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda/envs/conda-env"))
.env("_CONDA_ROOT", "conda"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ChildConda`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import ChildConda
| ^^^^^^^^^^
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// run with _CONDA_ROOT and CONDA_PREFIX (equal!) set, should find BaseConda
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("CONDA_PREFIX", case.root().join("conda"))
.env("_CONDA_ROOT", "conda"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `BaseConda`
--> test.py:5:22
|
3 | from package1 import ChildConda
4 | from package1 import WorkingVenv
5 | from package1 import BaseConda
| ^^^^^^^^^
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}
#[test]
fn src_root_deprecation_warning() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.src]
root = "./src"
"#,
),
("src/test.py", ""),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead.
--> pyproject.toml:3:8
|
2 | [tool.ty.src]
3 | root = "./src"
| ^^^^^^^
|
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}
#[test]
fn src_root_deprecation_warning_with_environment_root() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.src]
root = "./src"
[tool.ty.environment]
root = ["./app"]
"#,
),
("app/test.py", ""),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead.
--> pyproject.toml:3:8
|
2 | [tool.ty.src]
3 | root = "./src"
| ^^^^^^^
4 |
5 | [tool.ty.environment]
|
info: The `src.root` setting was ignored in favor of the `environment.root` setting
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}
#[test]
fn environment_root_takes_precedence_over_src_root() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[tool.ty.src]
root = "./src"
[tool.ty.environment]
root = ["./app"]
"#,
),
("src/test.py", "import my_module"),
(
"app/my_module.py",
"# This module exists in app/ but not src/",
),
])?;
// The test should pass because environment.root points to ./app where my_module.py exists
// If src.root took precedence, it would fail because my_module.py doesn't exist in ./src
assert_cmd_snapshot!(case.command(), @r###"
success: true
exit_code: 0
----- stdout -----
warning[deprecated-setting]: The `src.root` setting is deprecated. Use `environment.root` instead.
--> pyproject.toml:3:8
|
2 | [tool.ty.src]
3 | root = "./src"
| ^^^^^^^
4 |
5 | [tool.ty.environment]
|
info: The `src.root` setting was ignored in favor of the `environment.root` setting
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}
#[test]
fn default_root_src_layout() -> anyhow::Result<()> {
let case = CliTest::with_files([
("src/foo.py", "foo = 10"),
("bar.py", "bar = 20"),
(
"src/main.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 -----
"###);
Ok(())
}
#[test]
fn default_root_project_name_folder() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"pyproject.toml",
r#"
[project]
name = "psycopg"
"#,
),
("psycopg/psycopg/foo.py", "foo = 10"),
("bar.py", "bar = 20"),
(
"psycopg/psycopg/main.py",
r#"
from psycopg.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 -----
"###);
Ok(())
}
#[test]
fn default_root_flat_layout() -> anyhow::Result<()> {
let case = CliTest::with_files([
("app/foo.py", "foo = 10"),
("bar.py", "bar = 20"),
(
"app/main.py",
r#"
from app.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 -----
"###);
Ok(())
}
#[test]
fn default_root_tests_folder() -> anyhow::Result<()> {
let case = CliTest::with_files([
("src/foo.py", "foo = 10"),
("tests/bar.py", "bar = 20"),
(
"tests/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 -----
"###);
Ok(())
}
/// If `tests/__init__.py` is present, it is considered a package and `tests` is not added to `sys.path`.
#[test]
fn default_root_tests_package() -> anyhow::Result<()> {
let case = CliTest::with_files([
("src/foo.py", "foo = 10"),
("tests/__init__.py", ""),
("tests/bar.py", "bar = 20"),
(
"tests/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`
--> tests/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>/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
Found 1 diagnostic
----- stderr -----
"###);
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 -----
"###);
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>/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
Found 1 diagnostic
----- stderr -----
"###);
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>/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
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}
#[test]
fn pythonpath_is_respected() -> anyhow::Result<()> {
let case = CliTest::with_files([
("baz-dir/baz.py", "it = 42"),
(
"src/foo.py",
r#"
import baz
print(f"{baz.it}")
"#,
),
])?;
assert_cmd_snapshot!(case.command(),
@r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `baz`
--> src/foo.py:2:8
|
2 | import baz
| ^^^
3 | print(f"{baz.it}")
|
info: Searched in the following paths during module resolution:
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
Found 1 diagnostic
----- stderr -----
"###);
assert_cmd_snapshot!(case.command()
.env("PYTHONPATH", case.root().join("baz-dir")),
@r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
Ok(())
}
#[test]
fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> {
let case = CliTest::with_files([
("baz-dir/baz.py", "it = 42"),
("foo-dir/foo.py", "it = 42"),
(
"src/main.py",
r#"
import baz
import foo
print(f"{baz.it}")
print(f"{foo.it}")
"#,
),
])?;
assert_cmd_snapshot!(case.command(),
@r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Cannot resolve imported module `baz`
--> src/main.py:2:8
|
2 | import baz
| ^^^
3 | import foo
|
info: Searched in the following paths during module resolution:
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
error[unresolved-import]: Cannot resolve imported module `foo`
--> src/main.py:3:8
|
2 | import baz
3 | import foo
| ^^^
4 |
5 | print(f"{baz.it}")
|
info: Searched in the following paths during module resolution:
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
Found 2 diagnostics
----- stderr -----
"###);
let pythonpath =
std::env::join_paths([case.root().join("baz-dir"), case.root().join("foo-dir")])?;
assert_cmd_snapshot!(case.command()
.env("PYTHONPATH", pythonpath),
@r###"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"###);
Ok(())
}