1
0
mirror of https://github.com/badges/shields.git synced 2025-04-18 19:44:04 +03:00
shields/services/pypi/pypi-helpers.js
Jeremy Lainé 015ea0023e
[PyPI] Fix license for packages following PEP 639 (#11001)
* [PyPI] Fix license for packages following PEP 639

PEP 639 states that the preferred way of documenting a Python project's
license is an SPDX expression in a `License-Expression` metadata field.
PyPI exposes this information in `info.license_expression` in its JSON
data.

Fixes: #11000

* add license_expression to pypi response schema

* move comments inline into the relevant blocks

* assign both license and license_expression to intermediate variables

* always pass a license_expression in test input objects

---------

Co-authored-by: chris48s <git@chris-shaw.dev>
2025-04-06 18:31:44 +00:00

121 lines
3.7 KiB
JavaScript

/*
Django versions will be specified in the form major.minor
trying to sort with `semver.compare` will throw e.g:
TypeError: Invalid Version: 1.11
because no patch release is specified, so we will define
our own functions to parse and sort django versions
*/
function parsePypiVersionString(str) {
if (typeof str !== 'string') {
return false
}
const x = str.split('.')
const maj = parseInt(x[0]) || 0
const min = parseInt(x[1]) || 0
return {
major: maj,
minor: min,
}
}
// Sort an array of django versions low to high.
function sortPypiVersions(versions) {
return versions.sort((a, b) => {
if (parsePypiVersionString(a).major === parsePypiVersionString(b).major) {
return parsePypiVersionString(a).minor - parsePypiVersionString(b).minor
} else {
return parsePypiVersionString(a).major - parsePypiVersionString(b).major
}
})
}
// Extract classifiers from a pypi json response based on a regex.
function parseClassifiers(parsedData, pattern, preserveCase = false) {
const results = []
for (let i = 0; i < parsedData.info.classifiers.length; i++) {
const matched = pattern.exec(parsedData.info.classifiers[i])
if (matched && matched[1]) {
if (preserveCase) {
results.push(matched[1])
} else {
results.push(matched[1].toLowerCase())
}
}
}
return results
}
function getLicenses(packageData) {
const license = packageData.info.license
const licenseExpression = packageData.info.license_expression
if (licenseExpression) {
/*
The .license_expression field contains an SPDX expression, and it
is the preferred way of documenting a Python project's license.
See https://peps.python.org/pep-0639/
*/
return [licenseExpression]
} else if (license && license.length < 40) {
/*
The .license field may either contain
- a short license description (e.g: 'MIT' or 'GPL-3.0') or
- the full text of a license
but there is nothing in the response that tells us explicitly.
We have to make an assumption based on the length.
See https://github.com/badges/shields/issues/8689 and
https://github.com/badges/shields/pull/8690 for more info.
*/
return [license]
} else {
// else fall back to trove classifiers
const parenthesizedAcronymRegex = /\(([^)]+)\)/
const bareAcronymRegex = /^[a-z0-9]+$/
const spdxAliases = {
'OSI Approved :: Apache Software License': 'Apache-2.0',
'CC0 1.0 Universal (CC0 1.0) Public Domain Dedication': 'CC0-1.0',
'OSI Approved :: GNU Affero General Public License v3': 'AGPL-3.0',
'OSI Approved :: Zero-Clause BSD (0BSD)': '0BSD',
}
let licenses = parseClassifiers(packageData, /^License :: (.+)$/, true)
.map(classifier =>
classifier in spdxAliases ? spdxAliases[classifier] : classifier,
)
.map(classifier => classifier.split(' :: ').pop())
.map(license => license.replace(' License', ''))
.map(license => {
const match = license.match(parenthesizedAcronymRegex)
return match ? match[1].toUpperCase() : license
})
.map(license => {
const match = license.match(bareAcronymRegex)
return match ? license.toUpperCase() : license
})
if (licenses.length > 1) {
licenses = licenses.filter(l => l !== 'DFSG approved')
}
return licenses
}
}
function getPackageFormats(packageData) {
const { urls } = packageData
return {
hasWheel: urls.some(({ packagetype }) =>
['wheel', 'bdist_wheel'].includes(packagetype),
),
hasEgg: urls.some(({ packagetype }) =>
['egg', 'bdist_egg'].includes(packagetype),
),
}
}
export {
parseClassifiers,
parsePypiVersionString,
sortPypiVersions,
getLicenses,
getPackageFormats,
}