Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 7 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
lucab marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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)]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: IgnoredAny doesn't implement Eq, but the trait here seemed unused and fine to drop. Alternatively, we can either 1) store TOML tables in here, or 2) fully deserialize both scripts fields.

#[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
41 changes: 41 additions & 0 deletions crates/uv/tests/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1950,3 +1950,44 @@ fn run_invalid_project_table() -> Result<()> {

Ok(())
}

#[test]
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"
"#
})?;
lucab marked this conversation as resolved.
Show resolved Hide resolved

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<()> {
zanieb marked this conversation as resolved.
Show resolved Hide resolved
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(())
}
Loading