Skip to content

Commit 66c36d2

Browse files
committed
Read Poetry metadata the way poetry-core merges it
poetry-core takes the name from [project] or else [tool.poetry], and the version the same way, with no need for version to be dynamic. A [project].name with a static [tool.poetry].version now yields a product, and a [tool.poetry].name that differs from [project].name (Poetry 1.x and 2 would build different names) yields none instead of the legacy name. The poetry-dynamic-versioning backend counts as Poetry when the plugin is not enabled, as it then builds the static version. Assisted-by: Claude Code:claude-opus-5-5
1 parent 6eba4d9 commit 66c36d2

1 file changed

Lines changed: 66 additions & 31 deletions

File tree

‎crates/socket-patch-core/src/vex/product.rs‎

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -171,43 +171,50 @@ fn parse_pyproject(content: &str) -> Option<String> {
171171
/// them:
172172
///
173173
/// 1. PEP 621 `[project]` `name` + `version` (newer projects favor it).
174-
/// 2. `[project].name` with `version` listed in `[project].dynamic`: Poetry
175-
/// 2 then takes the version from `[tool.poetry].version`.
176-
/// 3. The legacy tables that hold both keys: Poetry's `[tool.poetry]`,
177-
/// then PDM 0.x's `[tool.pdm]` (PDM 1.x migrates it to `[project]`).
178-
///
179-
/// `[tool.poetry].version` counts only when Poetry builds the project and
180-
/// poetry-dynamic-versioning does not replace it at build time: under
181-
/// another backend (hatch-vcs, setuptools-scm) it is a leftover, and under
182-
/// the plugin it is a placeholder such as `0.0.0`.
174+
/// 2. When Poetry builds the project, poetry-core's own merge: the name is
175+
/// `[project].name` or else `[tool.poetry].name`, and the version is
176+
/// `[project].version` or else `[tool.poetry].version`. This covers
177+
/// Poetry 2's dynamic version and the legacy `[tool.poetry]` layout.
178+
/// 3. PDM 0.x's legacy `[tool.pdm]` table (PDM 1.x migrates it to
179+
/// `[project]`).
183180
fn pyproject_name_version(doc: &toml_edit::DocumentMut) -> Option<(String, String)> {
184181
let project = toml_table(doc, "project");
185182
let project_str = |key| project.and_then(|t| table_str(t, key));
186-
if let Some(name) = project_str("name") {
187-
if let Some(version) = project_str("version") {
188-
return Some((name.to_string(), version.to_string()));
189-
}
190-
let version_is_dynamic = project
191-
.and_then(|t| t.get("dynamic"))
192-
.and_then(|d| d.as_array())
193-
.is_some_and(|a| a.iter().any(|v| v.as_str() == Some("version")));
194-
if version_is_dynamic && poetry_version_is_static(doc) {
195-
if let Some(version) = toml_str(doc, "tool.poetry", "version") {
196-
return Some((name.to_string(), version.to_string()));
183+
let (project_name, project_version) = (project_str("name"), project_str("version"));
184+
if let (Some(name), Some(version)) = (project_name, project_version) {
185+
return Some((name.to_string(), version.to_string()));
186+
}
187+
if poetry_version_is_static(doc) {
188+
let poetry_name = toml_str(doc, "tool.poetry", "name");
189+
// Poetry 2 builds `[project].name`, Poetry 1.x ignores `[project]`
190+
// and builds `[tool.poetry].name`: when they differ, the product
191+
// depends on the Poetry line, so name none rather than guess.
192+
if let (Some(a), Some(b)) = (project_name, poetry_name) {
193+
if a != b {
194+
return None;
197195
}
198196
}
197+
let name = project_name.or(poetry_name);
198+
let version = project_version.or_else(|| toml_str(doc, "tool.poetry", "version"));
199+
if let (Some(name), Some(version)) = (name, version) {
200+
return Some((name.to_string(), version.to_string()));
201+
}
199202
}
200-
let poetry = toml_name_version(doc, "tool.poetry").filter(|_| poetry_version_is_static(doc));
201-
poetry.or_else(|| toml_name_version(doc, "tool.pdm"))
203+
toml_name_version(doc, "tool.pdm")
202204
}
203205

204-
/// Whether `[tool.poetry].version` is the version the build produces: no
205-
/// non-Poetry `[build-system].build-backend` (Poetry's own projects may
206-
/// omit the table), and poetry-dynamic-versioning not enabled.
206+
/// Whether Poetry builds the project from its static `[tool.poetry]`
207+
/// metadata: no non-Poetry `[build-system].build-backend` (Poetry's own
208+
/// projects may omit the table), and poetry-dynamic-versioning not
209+
/// enabled. Under another backend (hatch-vcs, setuptools-scm) the table is
210+
/// a leftover, and under the enabled plugin its version is a placeholder
211+
/// such as `0.0.0`. The plugin's backend re-exports poetry-core's and
212+
/// changes nothing until `enable = true`.
207213
fn poetry_version_is_static(doc: &toml_edit::DocumentMut) -> bool {
208214
// `poetry.core.masonry.api`, or `poetry.masonry.api` before poetry-core.
209-
let poetry_backend = toml_str(doc, "build-system", "build-backend")
210-
.is_none_or(|backend| backend.starts_with("poetry."));
215+
let poetry_backend = toml_str(doc, "build-system", "build-backend").is_none_or(|backend| {
216+
backend.starts_with("poetry.") || backend == "poetry_dynamic_versioning.backend"
217+
});
211218
let dynamic_versioning = toml_table(doc, "tool.poetry-dynamic-versioning")
212219
.and_then(|t| t.get("enable"))
213220
.and_then(|e| e.as_bool())
@@ -1294,12 +1301,40 @@ mod tests {
12941301
);
12951302
}
12961303

1297-
/// Without `version` in `dynamic`, Poetry does not take the
1298-
/// version from `[tool.poetry]`, and neither does detection.
1304+
/// poetry-core takes `[tool.poetry].version` when `[project]` has
1305+
/// none, whether or not `dynamic` lists it (poetry-core 2.2
1306+
/// `Factory.create_poetry`): this builds `demo 0.3.0`.
1307+
#[test]
1308+
fn pyproject_tool_poetry_version_without_dynamic() {
1309+
let content = "[project]\nname = \"demo\"\n\n[tool.poetry]\nversion = \"0.3.0\"\n\n[build-system]\nbuild-backend = \"poetry.core.masonry.api\"\n";
1310+
assert_eq!(
1311+
parse_pyproject(content).as_deref(),
1312+
Some("pkg:pypi/demo@0.3.0")
1313+
);
1314+
}
1315+
1316+
/// `[project].name` and a different `[tool.poetry].name`: Poetry 2
1317+
/// builds `new`, Poetry 1.x builds `old`, so no product is named
1318+
/// rather than a guess.
12991319
#[test]
1300-
fn pyproject_tool_poetry_version_needs_dynamic() {
1301-
let content = "[project]\nname = \"demo\"\ndynamic = [\"readme\"]\n\n[tool.poetry]\nversion = \"0.3.0\"\n";
1320+
fn pyproject_conflicting_poetry_names_is_none() {
1321+
let content =
1322+
"[project]\nname = \"new\"\n\n[tool.poetry]\nname = \"old\"\nversion = \"1.0\"\n";
13021323
assert!(parse_pyproject(content).is_none());
1324+
let same =
1325+
"[project]\nname = \"same\"\n\n[tool.poetry]\nname = \"same\"\nversion = \"1.0\"\n";
1326+
assert_eq!(parse_pyproject(same).as_deref(), Some("pkg:pypi/same@1.0"));
1327+
}
1328+
1329+
/// The plugin's backend re-exports poetry-core's and patches nothing
1330+
/// until `enable = true`, so the static version is what it builds.
1331+
#[test]
1332+
fn pyproject_dynamic_versioning_backend_disabled_keeps_static_version() {
1333+
let content = "[tool.poetry]\nname = \"demo\"\nversion = \"1.2.3\"\n\n[build-system]\nrequires = [\"poetry-core\", \"poetry-dynamic-versioning\"]\nbuild-backend = \"poetry_dynamic_versioning.backend\"\n";
1334+
assert_eq!(
1335+
parse_pyproject(content).as_deref(),
1336+
Some("pkg:pypi/demo@1.2.3")
1337+
);
13031338
}
13041339

13051340
/// A dynamic version with no `[tool.poetry].version` (setuptools-scm,

0 commit comments

Comments
 (0)