Skip to content

Commit

Permalink
fix(semver): improved support of the Python Version Identification pa…
Browse files Browse the repository at this point in the history
…rt of PEP440 (#3474)

The semver util's current version logic does not fully implement/adhere to PEP440 in two respects:
* There is no support for "[local version identifiers](https://packaging.python.org/en/latest/specifications/version-specifiers/#local-version-identifiers)", which are basically the same as SemVer's build metadata (e.g. `1.2.3+foobar`), when used in conjunction with a pre-release label.
* The current pre-release logic doesn't reflect the ability for python pre-releases to include [post-release](https://packaging.python.org/en/latest/specifications/version-specifiers/#post-releases) and [developmental release](https://packaging.python.org/en/latest/specifications/version-specifiers/#developmental-releases) labels in conjunction with the pre-release itself (e.g. `1.2.3.rc1.post2.dev3`)

This PR addresses these gaps so that the python release supports these features. I've kept support for the `pre` label as a synonym for `dev`.

Projen and JSII's logic diverged ever so slightly re: including a `.` between the version and pre-release labels (e.g. projen was yielding `1.2.3rc0` while JSII would yield `1.2.3.rc0`. I decided to update projen's logic to mirror that from JSII.

E.g. now `1.2.3-rc.1.dev.2.post.3+foobar` will now yield `1.2.3.rc1.post3.dev2+foobar` for python packages.

I have an identical PR open over in jsii: aws/jsii#4462

---
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • Loading branch information
icj217 committed Mar 25, 2024
1 parent cf67e86 commit 2ec610d
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 32 deletions.
96 changes: 67 additions & 29 deletions src/util/semver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,42 +101,80 @@ export function toReleaseVersion(
}
switch (target) {
case TargetName.PYTHON:
const baseVersion = `${version.major}.${version.minor}.${version.patch}`;

// Python supports a limited set of identifiers... And we have a mapping table...
// https://packaging.python.org/guides/distributing-packages-using-setuptools/#pre-release-versioning
const [label, sequence, ...rest] = version.prerelease;
if (rest.filter((elt) => elt !== 0).length > 0 || sequence == null) {
const releaseLabels: Record<string, string> = {
alpha: "a",
beta: "b",
rc: "rc",
post: "post",
dev: "dev",
pre: "pre",
};

const validationErrors: string[] = [];

// Ensure that prerelease composed entirely of [label, sequence] pairs
version.prerelease.forEach((elem, idx, arr) => {
const next: string | number | undefined = arr[idx + 1];
if (typeof elem === "string") {
if (!Object.keys(releaseLabels).includes(elem)) {
validationErrors.push(
`Label ${elem} is not one of ${Object.keys(releaseLabels).join(
","
)}`
);
}
if (next === undefined || !Number.isInteger(next)) {
validationErrors.push(
`Label ${elem} must be followed by a positive integer`
);
}
}
});

if (validationErrors.length > 0) {
throw new Error(
`Unable to map prerelease identifier (in: ${assemblyVersion}) components to python: ${inspect(
version.prerelease
)}. The format should be 'X.Y.Z-label.sequence', where sequence is a positive integer, and label is "dev", "pre", "alpha", beta", or "rc"`
)}. The format should be 'X.Y.Z-[label.sequence][.post.sequence][.(dev|pre).sequence]', where sequence is a positive integer and label is one of ${inspect(
Object.keys(releaseLabels)
)}. Validation errors encountered: ${validationErrors.join(", ")}`
);
}
if (!Number.isInteger(sequence)) {
throw new Error(
`Unable to map prerelease identifier (in: ${assemblyVersion}) to python, as sequence ${inspect(
sequence
)} is not an integer`
);
}
const baseVersion = `${version.major}.${version.minor}.${version.patch}`;
// See PEP 440: https://www.python.org/dev/peps/pep-0440/#pre-releases
switch (label) {
case "dev":
case "pre":
return `${baseVersion}.dev${sequence}`; // PEP 404 see developmental release as X.Y.devN
case "alpha":
return `${baseVersion}a${sequence}`; // PEP 404 see alpha release as X.YaN
case "beta":
return `${baseVersion}b${sequence}`; // PEP 404 see beta release as X.YbN
case "rc":
return `${baseVersion}rc${sequence}`; // PEP 404 see release candidate as X.YrcN
default:
throw new Error(
`Unable to map prerelease identifier (in: ${assemblyVersion}) to python, as label ${inspect(
label
)} is not mapped (only "dev", "pre", "alpha", "beta" and "rc" are)`
);
}

// PEP440 supports multiple labels in a given version, so
// we should attempt to identify and map as many labels as
// possible from the given prerelease input
// e.g. 1.2.3-rc.123.dev.456.post.789 => 1.2.3.rc123.dev456.post789
const postIdx = version.prerelease.findIndex(
(v) => v.toString() === "post"
);
const devIdx = version.prerelease.findIndex((v) =>
["dev", "pre"].includes(v.toString())
);
const preReleaseIdx = version.prerelease.findIndex((v) =>
["alpha", "beta", "rc"].includes(v.toString())
);
const prereleaseVersion = [
preReleaseIdx > -1
? `${releaseLabels[version.prerelease[preReleaseIdx]]}${
version.prerelease[preReleaseIdx + 1] ?? 0
}`
: undefined,
postIdx > -1
? `post${version.prerelease[postIdx + 1] ?? 0}`
: undefined,
devIdx > -1 ? `dev${version.prerelease[devIdx + 1] ?? 0}` : undefined,
]
.filter((v) => v)
.join(".");

return version.build.length > 0
? `${baseVersion}.${prereleaseVersion}+${version.build.join(".")}`
: `${baseVersion}.${prereleaseVersion}`;
case TargetName.DOTNET:
case TargetName.GO:
case TargetName.JAVA:
Expand Down
6 changes: 3 additions & 3 deletions test/cdk8s/cdk8s-app-project-py.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ test("cdk8s-plus-22 undefined", () => {

// THEN
expect(output["requirements.txt"]).toContain(
"cdk8s-plus-22>=2.0.0rc26, <3.0.0"
"cdk8s-plus-22>=2.0.0.rc26, <3.0.0"
);
});

Expand All @@ -194,7 +194,7 @@ test("cdk8s-plus-22 defined", () => {

// THEN
expect(output["requirements.txt"]).toContain(
"cdk8s-plus-22>=2.0.0rc27, <3.0.0"
"cdk8s-plus-22>=2.0.0.rc27, <3.0.0"
);
});

Expand All @@ -215,5 +215,5 @@ test("cdk8s-plus-22 pinning", () => {
const output = synthSnapshot(project);

// THEN
expect(output["requirements.txt"]).toContain("cdk8s-plus-22==2.0.0rc26");
expect(output["requirements.txt"]).toContain("cdk8s-plus-22==2.0.0.rc26");
});
35 changes: 35 additions & 0 deletions test/util/semver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { toReleaseVersion, TargetName } from "../../src/util/semver";

describe(`toReleaseVersion (${TargetName.PYTHON})`, () => {
test("supports standard versions", () => {
expect(toReleaseVersion("1.2.3", TargetName.PYTHON)).toBe("1.2.3");
});
test("supports build metadata", () => {
expect(toReleaseVersion("1.2.3+foobar", TargetName.PYTHON)).toBe(
"1.2.3+foobar"
);
});
test("supports pre-releases", () => {
expect(toReleaseVersion("1.2.3-rc.123", TargetName.PYTHON)).toBe(
"1.2.3.rc123"
);
});
test("supports developmental releases", () => {
expect(toReleaseVersion("1.2.3-dev.123", TargetName.PYTHON)).toBe(
"1.2.3.dev123"
);
});
test("supports post releases", () => {
expect(toReleaseVersion("1.2.3-post.123", TargetName.PYTHON)).toBe(
"1.2.3.post123"
);
});
test("supports pre-releases with dev/post and build metadata", () => {
expect(
toReleaseVersion(
"1.2.3-rc.123.post.456.dev.789+foobar",
TargetName.PYTHON
)
).toBe("1.2.3.rc123.post456.dev789+foobar");
});
});

0 comments on commit 2ec610d

Please sign in to comment.