codekingpro/portable-devtools
114k
1#######################################################################################2#3# Adapted from:4# https://github.com/pypa/hatch/blob/5352e44/backend/src/hatchling/licenses/parse.py5#6# MIT License7#8# Copyright (c) 2017-present Ofek Lev <oss@ofek.dev>9#10# Permission is hereby granted, free of charge, to any person obtaining a copy of this11# software and associated documentation files (the "Software"), to deal in the Software12# without restriction, including without limitation the rights to use, copy, modify,13# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to14# permit persons to whom the Software is furnished to do so, subject to the following15# conditions:16#17# The above copyright notice and this permission notice shall be included in all copies18# or substantial portions of the Software.19#20# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,21# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A22# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT23# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF24# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE25# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.26#27#28# With additional allowance of arbitrary `LicenseRef-` identifiers, not just29# `LicenseRef-Public-Domain` and `LicenseRef-Proprietary`.30#31#######################################################################################32from __future__ import annotations33 34import re35from typing import NewType, cast36 37from ._spdx import EXCEPTIONS, LICENSES38 39__all__ = [40 "InvalidLicenseExpression",41 "NormalizedLicenseExpression",42 "canonicalize_license_expression",43]44 45 46# Simple __dir__ implementation since there are no public submodules47def __dir__() -> list[str]:48 return __all__49 50 51license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")52 53NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)54"""55A :class:`typing.NewType` of :class:`str`, representing a normalized56License-Expression.57"""58 59 60class InvalidLicenseExpression(ValueError):61 """Raised when a license-expression string is invalid62 63 >>> from packaging.licenses import canonicalize_license_expression64 >>> canonicalize_license_expression("invalid")65 Traceback (most recent call last):66 ...67 packaging.licenses.InvalidLicenseExpression: Invalid license expression: 'invalid'68 """69 70 71def canonicalize_license_expression(72 raw_license_expression: str,73) -> NormalizedLicenseExpression:74 """75 This function takes a valid License-Expression, and returns the normalized76 form of it.77 78 The return type is typed as :class:`NormalizedLicenseExpression`. This79 allows type checkers to help require that a string has passed through this80 function before use.81 82 :param str raw_license_expression: The License-Expression to canonicalize.83 :raises InvalidLicenseExpression: If the License-Expression is invalid due to an84 invalid/unknown license identifier or invalid syntax.85 86 .. doctest::87 88 >>> from packaging.licenses import canonicalize_license_expression89 >>> canonicalize_license_expression("mit")90 'MIT'91 >>> canonicalize_license_expression("mit and (apache-2.0 or bsd-2-clause)")92 'MIT AND (Apache-2.0 OR BSD-2-Clause)'93 >>> canonicalize_license_expression("(mit")94 Traceback (most recent call last):95 ...96 InvalidLicenseExpression: Invalid license expression: '(mit'97 >>> canonicalize_license_expression("Use-it-after-midnight")98 Traceback (most recent call last):99 ...100 InvalidLicenseExpression: Unknown license: 'Use-it-after-midnight'101 """102 if not raw_license_expression:103 message = f"Invalid license expression: {raw_license_expression!r}"104 raise InvalidLicenseExpression(message)105 106 # Pad any parentheses so tokenization can be achieved by merely splitting on107 # whitespace.108 license_expression = raw_license_expression.replace("(", " ( ").replace(")", " ) ")109 licenseref_prefix = "LicenseRef-"110 license_refs = {111 ref.lower(): "LicenseRef-" + ref[len(licenseref_prefix) :]112 for ref in license_expression.split()113 if ref.lower().startswith(licenseref_prefix.lower())114 }115 116 # Normalize to lower case so we can look up licenses/exceptions117 # and so boolean operators are Python-compatible.118 license_expression = license_expression.lower()119 120 tokens = license_expression.split()121 122 # Rather than implementing a parenthesis/boolean logic parser, create an123 # expression that Python can parse. Everything that is not involved with the124 # grammar itself is replaced with the placeholder `False` and the resultant125 # expression should become a valid Python expression.126 python_tokens = []127 for token in tokens:128 if token not in {"or", "and", "with", "(", ")"}:129 python_tokens.append("False")130 elif token == "with":131 python_tokens.append("or")132 elif (133 token == "("134 and python_tokens135 and python_tokens[-1] not in {"or", "and", "("}136 ) or (token == ")" and python_tokens and python_tokens[-1] == "("):137 message = f"Invalid license expression: {raw_license_expression!r}"138 raise InvalidLicenseExpression(message)139 else:140 python_tokens.append(token)141 142 python_expression = " ".join(python_tokens)143 try:144 compile(python_expression, "", "eval")145 except SyntaxError:146 message = f"Invalid license expression: {raw_license_expression!r}"147 raise InvalidLicenseExpression(message) from None148 149 # Take a final pass to check for unknown licenses/exceptions.150 normalized_tokens = []151 for token in tokens:152 if token in {"or", "and", "with", "(", ")"}:153 normalized_tokens.append(token.upper())154 continue155 156 if normalized_tokens and normalized_tokens[-1] == "WITH":157 if token not in EXCEPTIONS:158 message = f"Unknown license exception: {token!r}"159 raise InvalidLicenseExpression(message)160 161 normalized_tokens.append(EXCEPTIONS[token]["id"])162 else:163 if token.endswith("+"):164 final_token = token[:-1]165 suffix = "+"166 else:167 final_token = token168 suffix = ""169 170 if final_token.startswith("licenseref-"):171 if not license_ref_allowed.match(final_token):172 message = f"Invalid licenseref: {final_token!r}"173 raise InvalidLicenseExpression(message)174 normalized_tokens.append(license_refs[final_token] + suffix)175 else:176 if final_token not in LICENSES:177 message = f"Unknown license: {final_token!r}"178 raise InvalidLicenseExpression(message)179 normalized_tokens.append(LICENSES[final_token]["id"] + suffix)180 181 normalized_expression = " ".join(normalized_tokens)182 183 return cast(184 "NormalizedLicenseExpression",185 normalized_expression.replace("( ", "(").replace(" )", ")"),186 )187 