Warn when trying to `uv sync` a package without build configuration (#7420)

This enhances `uv sync` logic in order to detect and warn if it is
trying to operate on a packaged project with entrypoints.

Ref: https://github.com/astral-sh/uv/issues/6998#issuecomment-2329291764
Closes: https://github.com/astral-sh/uv/issues/7034
This commit is contained in:
Luca Bruno 2024-09-16 17:50:42 +02:00 committed by GitHub
parent d4f4dedc3b
commit 23494d85ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 160 additions and 2 deletions

View File

@ -44,7 +44,7 @@ pub struct PyProjectToml {
#[serde(skip)]
pub raw: String,
/// Used to determine whether a `build-system` is present.
/// Used to determine whether a `build-system` section is present.
#[serde(default, skip_serializing)]
build_system: Option<serde::de::IgnoredAny>,
}
@ -82,6 +82,15 @@ impl PyProjectToml {
// Otherwise, a project is assumed to be a package if `build-system` is present.
self.build_system.is_some()
}
/// Returns whether the project manifest contains any script table.
pub fn has_scripts(&self) -> bool {
if let Some(ref project) = self.project {
project.gui_scripts.is_some() || project.scripts.is_some()
} else {
false
}
}
}
// Ignore raw document in comparison.
@ -102,7 +111,7 @@ impl AsRef<[u8]> for PyProjectToml {
/// PEP 621 project metadata (`project`).
///
/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct Project {
/// The name of the project
@ -113,6 +122,13 @@ pub struct Project {
pub requires_python: Option<VersionSpecifiers>,
/// The optional dependencies of the project.
pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
/// Used to determine whether a `gui-scripts` section is present.
#[serde(default, skip_serializing)]
pub(crate) gui_scripts: Option<serde::de::IgnoredAny>,
/// Used to determine whether a `scripts` section is present.
#[serde(default, skip_serializing)]
pub(crate) scripts: Option<serde::de::IgnoredAny>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]

View File

@ -16,6 +16,7 @@ use uv_normalize::{PackageName, DEV_DEPENDENCIES};
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_resolver::{FlatIndex, Lock};
use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, InstallTarget, MemberDiscovery, VirtualProject, Workspace};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
@ -74,6 +75,14 @@ pub(crate) async fn sync(
InstallTarget::from(&project)
};
// TODO(lucab): improve warning content
// <https://github.com/astral-sh/uv/issues/7428>
if project.workspace().pyproject_toml().has_scripts()
&& !project.workspace().pyproject_toml().is_package()
{
warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`");
}
// Discover or create the virtual environment.
let venv = project::get_or_init_environment(
target.workspace(),

View File

@ -1950,3 +1950,45 @@ fn run_invalid_project_table() -> Result<()> {
Ok(())
}
#[test]
#[cfg(target_family = "unix")]
fn run_script_without_build_system() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "foo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
entry = "foo:custom_entry"
"#
})?;
let test_script = context.temp_dir.child("src/__init__.py");
test_script.write_str(indoc! { r#"
def custom_entry():
print!("Hello")
"#
})?;
// TODO(lucab): this should match `entry` and warn
// <https://github.com/astral-sh/uv/issues/7428>
uv_snapshot!(context.filters(), context.run().arg("entry"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
error: Failed to spawn: `entry`
Caused by: No such file or directory (os error 2)
"###);
Ok(())
}

View File

@ -2328,3 +2328,94 @@ fn transitive_dev() -> Result<()> {
Ok(())
}
#[test]
/// Check warning message for <https://github.com/astral-sh/uv/issues/6998>
/// if no `build-system` section is defined.
fn sync_scripts_without_build_system() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "foo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
entry = "foo:custom_entry"
"#,
)?;
let test_script = context.temp_dir.child("src/__init__.py");
test_script.write_str(
r#"
def custom_entry():
print!("Hello")
"#,
)?;
uv_snapshot!(context.filters(), context.sync(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`
Resolved 1 package in [TIME]
Audited in [TIME]
"###);
Ok(())
}
#[test]
/// Check warning message for <https://github.com/astral-sh/uv/issues/6998>
/// if the project is marked as `package = false`.
fn sync_scripts_project_not_packaged() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "foo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
entry = "foo:custom_entry"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
package = false
"#,
)?;
let test_script = context.temp_dir.child("src/__init__.py");
test_script.write_str(
r#"
def custom_entry():
print!("Hello")
"#,
)?;
uv_snapshot!(context.filters(), context.sync(), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`
Resolved 1 package in [TIME]
Audited in [TIME]
"###);
Ok(())
}