You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			146 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			146 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			Python
		
	
#######################################################################################
 | 
						|
#
 | 
						|
# Adapted from:
 | 
						|
#  https://github.com/pypa/hatch/blob/5352e44/backend/src/hatchling/licenses/parse.py
 | 
						|
#
 | 
						|
# MIT License
 | 
						|
#
 | 
						|
# Copyright (c) 2017-present Ofek Lev <oss@ofek.dev>
 | 
						|
#
 | 
						|
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
 | 
						|
# software and associated documentation files (the "Software"), to deal in the Software
 | 
						|
# without restriction, including without limitation the rights to use, copy, modify,
 | 
						|
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
 | 
						|
# permit persons to whom the Software is furnished to do so, subject to the following
 | 
						|
# conditions:
 | 
						|
#
 | 
						|
# The above copyright notice and this permission notice shall be included in all copies
 | 
						|
# or substantial portions of the Software.
 | 
						|
#
 | 
						|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 | 
						|
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 | 
						|
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 | 
						|
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
 | 
						|
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
 | 
						|
# OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 | 
						|
#
 | 
						|
#
 | 
						|
# With additional allowance of arbitrary `LicenseRef-` identifiers, not just
 | 
						|
# `LicenseRef-Public-Domain` and `LicenseRef-Proprietary`.
 | 
						|
#
 | 
						|
#######################################################################################
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import re
 | 
						|
from typing import NewType, cast
 | 
						|
 | 
						|
from packaging.licenses._spdx import EXCEPTIONS, LICENSES
 | 
						|
 | 
						|
__all__ = [
 | 
						|
    "InvalidLicenseExpression",
 | 
						|
    "NormalizedLicenseExpression",
 | 
						|
    "canonicalize_license_expression",
 | 
						|
]
 | 
						|
 | 
						|
license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")
 | 
						|
 | 
						|
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
 | 
						|
 | 
						|
 | 
						|
class InvalidLicenseExpression(ValueError):
 | 
						|
    """Raised when a license-expression string is invalid
 | 
						|
 | 
						|
    >>> canonicalize_license_expression("invalid")
 | 
						|
    Traceback (most recent call last):
 | 
						|
        ...
 | 
						|
    packaging.licenses.InvalidLicenseExpression: Invalid license expression: 'invalid'
 | 
						|
    """
 | 
						|
 | 
						|
 | 
						|
def canonicalize_license_expression(
 | 
						|
    raw_license_expression: str,
 | 
						|
) -> NormalizedLicenseExpression:
 | 
						|
    if not raw_license_expression:
 | 
						|
        message = f"Invalid license expression: {raw_license_expression!r}"
 | 
						|
        raise InvalidLicenseExpression(message)
 | 
						|
 | 
						|
    # Pad any parentheses so tokenization can be achieved by merely splitting on
 | 
						|
    # whitespace.
 | 
						|
    license_expression = raw_license_expression.replace("(", " ( ").replace(")", " ) ")
 | 
						|
    licenseref_prefix = "LicenseRef-"
 | 
						|
    license_refs = {
 | 
						|
        ref.lower(): "LicenseRef-" + ref[len(licenseref_prefix) :]
 | 
						|
        for ref in license_expression.split()
 | 
						|
        if ref.lower().startswith(licenseref_prefix.lower())
 | 
						|
    }
 | 
						|
 | 
						|
    # Normalize to lower case so we can look up licenses/exceptions
 | 
						|
    # and so boolean operators are Python-compatible.
 | 
						|
    license_expression = license_expression.lower()
 | 
						|
 | 
						|
    tokens = license_expression.split()
 | 
						|
 | 
						|
    # Rather than implementing boolean logic, we create an expression that Python can
 | 
						|
    # parse. Everything that is not involved with the grammar itself is treated as
 | 
						|
    # `False` and the expression should evaluate as such.
 | 
						|
    python_tokens = []
 | 
						|
    for token in tokens:
 | 
						|
        if token not in {"or", "and", "with", "(", ")"}:
 | 
						|
            python_tokens.append("False")
 | 
						|
        elif token == "with":
 | 
						|
            python_tokens.append("or")
 | 
						|
        elif token == "(" and python_tokens and python_tokens[-1] not in {"or", "and"}:
 | 
						|
            message = f"Invalid license expression: {raw_license_expression!r}"
 | 
						|
            raise InvalidLicenseExpression(message)
 | 
						|
        else:
 | 
						|
            python_tokens.append(token)
 | 
						|
 | 
						|
    python_expression = " ".join(python_tokens)
 | 
						|
    try:
 | 
						|
        invalid = eval(python_expression, globals(), locals())
 | 
						|
    except Exception:
 | 
						|
        invalid = True
 | 
						|
 | 
						|
    if invalid is not False:
 | 
						|
        message = f"Invalid license expression: {raw_license_expression!r}"
 | 
						|
        raise InvalidLicenseExpression(message) from None
 | 
						|
 | 
						|
    # Take a final pass to check for unknown licenses/exceptions.
 | 
						|
    normalized_tokens = []
 | 
						|
    for token in tokens:
 | 
						|
        if token in {"or", "and", "with", "(", ")"}:
 | 
						|
            normalized_tokens.append(token.upper())
 | 
						|
            continue
 | 
						|
 | 
						|
        if normalized_tokens and normalized_tokens[-1] == "WITH":
 | 
						|
            if token not in EXCEPTIONS:
 | 
						|
                message = f"Unknown license exception: {token!r}"
 | 
						|
                raise InvalidLicenseExpression(message)
 | 
						|
 | 
						|
            normalized_tokens.append(EXCEPTIONS[token]["id"])
 | 
						|
        else:
 | 
						|
            if token.endswith("+"):
 | 
						|
                final_token = token[:-1]
 | 
						|
                suffix = "+"
 | 
						|
            else:
 | 
						|
                final_token = token
 | 
						|
                suffix = ""
 | 
						|
 | 
						|
            if final_token.startswith("licenseref-"):
 | 
						|
                if not license_ref_allowed.match(final_token):
 | 
						|
                    message = f"Invalid licenseref: {final_token!r}"
 | 
						|
                    raise InvalidLicenseExpression(message)
 | 
						|
                normalized_tokens.append(license_refs[final_token] + suffix)
 | 
						|
            else:
 | 
						|
                if final_token not in LICENSES:
 | 
						|
                    message = f"Unknown license: {final_token!r}"
 | 
						|
                    raise InvalidLicenseExpression(message)
 | 
						|
                normalized_tokens.append(LICENSES[final_token]["id"] + suffix)
 | 
						|
 | 
						|
    normalized_expression = " ".join(normalized_tokens)
 | 
						|
 | 
						|
    return cast(
 | 
						|
        NormalizedLicenseExpression,
 | 
						|
        normalized_expression.replace("( ", "(").replace(" )", ")"),
 | 
						|
    )
 |