Skip to content

Commit c8483fb

Browse files
authored
fix(jsii-pacmak): fully support the Python Version Identification part of PEP440 (#4462)
`jsii-pacmak`'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`, 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. --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
1 parent 02bced7 commit c8483fb

File tree

2 files changed

+89
-29
lines changed

2 files changed

+89
-29
lines changed

packages/jsii-pacmak/lib/targets/version-utils.ts

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -93,42 +93,80 @@ export function toReleaseVersion(
9393
}
9494
switch (target) {
9595
case TargetName.PYTHON:
96+
const baseVersion = `${version.major}.${version.minor}.${version.patch}`;
97+
9698
// Python supports a limited set of identifiers... And we have a mapping table...
9799
// https://packaging.python.org/guides/distributing-packages-using-setuptools/#pre-release-versioning
98-
const [label, sequence, ...rest] = version.prerelease;
99-
if (rest.filter((elt) => elt !== 0).length > 0 || sequence == null) {
100+
const releaseLabels: Record<string, string> = {
101+
alpha: 'a',
102+
beta: 'b',
103+
rc: 'rc',
104+
post: 'post',
105+
dev: 'dev',
106+
pre: 'pre',
107+
};
108+
109+
const validationErrors: string[] = [];
110+
111+
// Ensure that prerelease composed entirely of [label, sequence] pairs
112+
version.prerelease.forEach((elem, idx, arr) => {
113+
const next: string | number | undefined = arr[idx + 1];
114+
if (typeof elem === 'string') {
115+
if (!Object.keys(releaseLabels).includes(elem)) {
116+
validationErrors.push(
117+
`Label ${elem} is not one of ${Object.keys(releaseLabels).join(
118+
',',
119+
)}`,
120+
);
121+
}
122+
if (next === undefined || !Number.isInteger(next)) {
123+
validationErrors.push(
124+
`Label ${elem} must be followed by a positive integer`,
125+
);
126+
}
127+
}
128+
});
129+
130+
if (validationErrors.length > 0) {
100131
throw new Error(
101132
`Unable to map prerelease identifier (in: ${assemblyVersion}) components to python: ${inspect(
102133
version.prerelease,
103-
)}. The format should be 'X.Y.Z-label.sequence', where sequence is a positive integer, and label is "dev", "pre", "alpha", beta", or "rc"`,
134+
)}. 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(
135+
Object.keys(releaseLabels),
136+
)}. Validation errors encountered: ${validationErrors.join(', ')}`,
104137
);
105138
}
106-
if (!Number.isInteger(sequence)) {
107-
throw new Error(
108-
`Unable to map prerelease identifier (in: ${assemblyVersion}) to python, as sequence ${inspect(
109-
sequence,
110-
)} is not an integer`,
111-
);
112-
}
113-
const baseVersion = `${version.major}.${version.minor}.${version.patch}`;
114-
// See PEP 440: https://www.python.org/dev/peps/pep-0440/#pre-releases
115-
switch (label) {
116-
case 'dev':
117-
case 'pre':
118-
return `${baseVersion}.dev${sequence}`;
119-
case 'alpha':
120-
return `${baseVersion}.a${sequence}`;
121-
case 'beta':
122-
return `${baseVersion}.b${sequence}`;
123-
case 'rc':
124-
return `${baseVersion}.rc${sequence}`;
125-
default:
126-
throw new Error(
127-
`Unable to map prerelease identifier (in: ${assemblyVersion}) to python, as label ${inspect(
128-
label,
129-
)} is not mapped (only "dev", "pre", "alpha", "beta" and "rc" are)`,
130-
);
131-
}
139+
140+
// PEP440 supports multiple labels in a given version, so
141+
// we should attempt to identify and map as many labels as
142+
// possible from the given prerelease input
143+
// e.g. 1.2.3-rc.123.dev.456.post.789 => 1.2.3.rc123.dev456.post789
144+
const postIdx = version.prerelease.findIndex(
145+
(v) => v.toString() === 'post',
146+
);
147+
const devIdx = version.prerelease.findIndex((v) =>
148+
['dev', 'pre'].includes(v.toString()),
149+
);
150+
const preReleaseIdx = version.prerelease.findIndex((v) =>
151+
['alpha', 'beta', 'rc'].includes(v.toString()),
152+
);
153+
const prereleaseVersion = [
154+
preReleaseIdx > -1
155+
? `${releaseLabels[version.prerelease[preReleaseIdx]]}${
156+
version.prerelease[preReleaseIdx + 1] ?? 0
157+
}`
158+
: undefined,
159+
postIdx > -1
160+
? `post${version.prerelease[postIdx + 1] ?? 0}`
161+
: undefined,
162+
devIdx > -1 ? `dev${version.prerelease[devIdx + 1] ?? 0}` : undefined,
163+
]
164+
.filter((v) => v)
165+
.join('.');
166+
167+
return version.build.length > 0
168+
? `${baseVersion}.${prereleaseVersion}+${version.build.join('.')}`
169+
: `${baseVersion}.${prereleaseVersion}`;
132170
case TargetName.DOTNET:
133171
case TargetName.GO:
134172
case TargetName.JAVA:

packages/jsii-pacmak/test/targets/version-utils.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@ describe(toReleaseVersion, () => {
127127
python:
128128
/Unable to map prerelease identifier \(in: 1\.2\.3-pre\) components to python: \[ 'pre' \]/,
129129
},
130+
'1.2.3-dev.123.0+abc123.foo.bar': {
131+
dotnet: '1.2.3-dev.123.0+abc123.foo.bar',
132+
go: '1.2.3-dev.123.0+abc123.foo.bar',
133+
java: '1.2.3-dev.123.0+abc123.foo.bar',
134+
js: '1.2.3-dev.123.0+abc123.foo.bar',
135+
python: '1.2.3.dev123+abc123.foo.bar',
136+
},
130137
'1.2.3-alpha.1337': {
131138
dotnet: '1.2.3-alpha.1337',
132139
go: '1.2.3-alpha.1337',
@@ -148,6 +155,21 @@ describe(toReleaseVersion, () => {
148155
js: '1.2.3-rc.9',
149156
python: '1.2.3.rc9',
150157
},
158+
'1.2.3-rc.123.post.456.dev.789': {
159+
dotnet: '1.2.3-rc.123.post.456.dev.789',
160+
go: '1.2.3-rc.123.post.456.dev.789',
161+
java: '1.2.3-rc.123.post.456.dev.789',
162+
js: '1.2.3-rc.123.post.456.dev.789',
163+
python: '1.2.3.rc123.post456.dev789',
164+
},
165+
'1.2.3-rc.alpha': {
166+
dotnet: '1.2.3-rc.alpha',
167+
go: '1.2.3-rc.alpha',
168+
java: '1.2.3-rc.alpha',
169+
js: '1.2.3-rc.alpha',
170+
python:
171+
/Unable to map prerelease identifier \(in: 1.2.3-rc.alpha\) components to python: \[ 'rc', 'alpha' \]/,
172+
},
151173
};
152174

153175
for (const [version, targets] of Object.entries(examples)) {

0 commit comments

Comments
 (0)