Skip to content

Commit

Permalink
Warn when trying to uv sync a package without build configuration (#…
Browse files Browse the repository at this point in the history
…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: #6998 (comment)
Closes: #7034
  • Loading branch information
lucab authored Sep 16, 2024
1 parent d4f4ded commit 23494d8
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 2 deletions.
20 changes: 18 additions & 2 deletions crates/uv-workspace/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
}
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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)]
Expand Down
9 changes: 9 additions & 0 deletions crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(),
Expand Down
42 changes: 42 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
91 changes: 91 additions & 0 deletions crates/uv/tests/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

0 comments on commit 23494d8

Please sign in to comment.