A comprehensive utility library to parse, compare, simplify and normalize license expressions using boolean logic
—
The license-expression library provides comprehensive error handling through exception classes and error constants. It also exposes token constants for advanced parsing and processing scenarios.
Custom exception classes for handling license expression errors.
class ExpressionError(Exception):
"""
Base exception class for license expression errors.
Raised when general expression processing fails.
"""
pass
class ParseError(Exception):
"""
Base parsing error class imported from boolean.py.
Contains information about parsing failures.
"""
@property
def token_type(self) -> int:
"""Type of token that caused the error."""
@property
def token_string(self) -> str:
"""String representation of the problematic token."""
@property
def position(self) -> int:
"""Position in the input string where the error occurred."""
@property
def error_code(self) -> int:
"""Numeric error code identifying the specific parsing error."""
class ExpressionParseError(ParseError, ExpressionError):
"""
Exception raised during license expression parsing.
Inherits from both ParseError (from boolean.py) and ExpressionError.
Contains detailed information about parsing failures.
"""
@property
def token_type(self) -> int:
"""Type of token that caused the error."""
@property
def token_string(self) -> str:
"""String representation of the problematic token."""
@property
def position(self) -> int:
"""Position in the input string where the error occurred."""
@property
def error_code(self) -> int:
"""Numeric error code identifying the specific parsing error."""Data structure containing validation results.
class ExpressionInfo:
"""
Contains detailed information about expression validation results.
"""
original_expression: str
"""The original expression string that was validated."""
normalized_expression: str
"""Normalized version of the expression, or None if validation failed."""
errors: list
"""List of error messages describing validation failures."""
invalid_symbols: list
"""List of unrecognized license symbols found in the expression."""Error codes specific to license expression parsing.
# License expression specific error codes
PARSE_EXPRESSION_NOT_UNICODE: int = 100
"""Expression string must be a string."""
PARSE_INVALID_EXCEPTION: int = 101
"""A license exception symbol can only be used as an exception in a 'WITH exception' statement."""
PARSE_INVALID_SYMBOL_AS_EXCEPTION: int = 102
"""A plain license symbol cannot be used as an exception in a 'WITH symbol' statement."""
PARSE_INVALID_SYMBOL: int = 103
"""A proper license symbol is needed."""Error codes inherited from the boolean.py library.
# Imported from boolean.py
PARSE_ERRORS: dict
"""Dictionary mapping error codes to error messages."""
PARSE_INVALID_EXPRESSION: int
"""Invalid expression structure."""
PARSE_INVALID_NESTING: int
"""Invalid parentheses nesting."""
PARSE_INVALID_OPERATOR_SEQUENCE: int
"""Invalid sequence of boolean operators."""
PARSE_INVALID_SYMBOL_SEQUENCE: int
"""Invalid sequence of symbols."""
PARSE_UNBALANCED_CLOSING_PARENS: int
"""Unbalanced closing parentheses."""
PARSE_UNKNOWN_TOKEN: int
"""Unknown token encountered during parsing."""Constants representing different types of tokens in license expressions.
# Token type constants (imported from boolean.py)
TOKEN_SYMBOL: int
"""Token representing a license symbol."""
TOKEN_AND: int
"""Token representing the AND boolean operator."""
TOKEN_OR: int
"""Token representing the OR boolean operator."""
TOKEN_LPAR: int
"""Token representing a left parenthesis '('."""
TOKEN_RPAR: int
"""Token representing a right parenthesis ')'."""
TOKEN_WITH: int = 10
"""Token representing the WITH exception operator (license-expression specific)."""Predefined keyword objects and collections for license expression parsing.
# Keyword objects for expression parsing
KW_LPAR: Keyword
"""Keyword object for left parenthesis '('."""
KW_RPAR: Keyword
"""Keyword object for right parenthesis ')'."""
KW_AND: Keyword
"""Keyword object for AND boolean operator."""
KW_OR: Keyword
"""Keyword object for OR boolean operator."""
KW_WITH: Keyword
"""Keyword object for WITH exception operator."""
# Keyword collections
KEYWORDS: tuple
"""Tuple containing all keyword objects (KW_LPAR, KW_RPAR, KW_AND, KW_OR, KW_WITH)."""
KEYWORDS_STRINGS: set
"""Set of keyword string values for quick lookup."""
OPERATORS: dict
"""Dictionary mapping operator strings to keyword objects."""Named tuple and helper classes for token processing.
class Keyword:
"""
Named tuple representing a keyword token.
"""
value: str
"""The string value of the keyword."""
type: int
"""The token type of the keyword."""
def __len__(self) -> int:
"""Return length of the keyword value."""from license_expression import (
get_spdx_licensing,
ExpressionError,
ExpressionParseError
)
licensing = get_spdx_licensing()
# Handle general expression errors
try:
result = licensing.validate('unknown-license')
if result.errors:
print("Validation errors:", result.errors)
except ExpressionError as e:
print(f"Expression error: {e}")
# Handle parsing errors with detailed information
try:
licensing.parse('MIT and', validate=True, strict=True)
except ExpressionParseError as e:
print(f"Parse error at position {e.position}: {e}")
print(f"Error code: {e.error_code}")
print(f"Problematic token: '{e.token_string}' (type: {e.token_type})")from license_expression import (
PARSE_INVALID_EXCEPTION,
PARSE_INVALID_SYMBOL_AS_EXCEPTION,
PARSE_ERRORS,
get_spdx_licensing
)
licensing = get_spdx_licensing()
# Test different error conditions
try:
# Using a license as an exception (should fail)
licensing.parse('MIT WITH Apache-2.0', validate=True, strict=True)
except ExpressionParseError as e:
if e.error_code == PARSE_INVALID_SYMBOL_AS_EXCEPTION:
print("Cannot use regular license as exception")
print(f"Error message: {PARSE_ERRORS[e.error_code]}")
try:
# Using an exception without WITH (should fail)
licensing.parse('Classpath-exception-2.0', validate=True, strict=True)
except ExpressionParseError as e:
if e.error_code == PARSE_INVALID_EXCEPTION:
print("Exception symbols must be used with WITH")from license_expression import get_spdx_licensing
licensing = get_spdx_licensing()
# Validate expressions and examine results
expressions = [
'MIT and Apache-2.0', # Valid
'UnknownLicense or MIT', # Has unknown symbol
'MIT WITH ClasspathException', # Invalid exception usage
]
for expr in expressions:
result = licensing.validate(expr)
print(f"\nExpression: {result.original_expression}")
print(f"Normalized: {result.normalized_expression}")
print(f"Errors: {result.errors}")
print(f"Invalid symbols: {result.invalid_symbols}")from license_expression import (
TOKEN_SYMBOL, TOKEN_AND, TOKEN_OR,
TOKEN_LPAR, TOKEN_RPAR, TOKEN_WITH
)
# These constants can be used for custom token processing
def analyze_token_type(token_type):
token_names = {
TOKEN_SYMBOL: "License Symbol",
TOKEN_AND: "AND Operator",
TOKEN_OR: "OR Operator",
TOKEN_LPAR: "Left Parenthesis",
TOKEN_RPAR: "Right Parenthesis",
TOKEN_WITH: "WITH Operator"
}
return token_names.get(token_type, f"Unknown token type: {token_type}")
# Example usage (for advanced scenarios)
print(analyze_token_type(TOKEN_SYMBOL)) # "License Symbol"
print(analyze_token_type(TOKEN_AND)) # "AND Operator"from license_expression import (
get_spdx_licensing,
ExpressionError,
ExpressionParseError,
PARSE_ERRORS
)
def safe_parse_expression(licensing, expression):
"""
Safely parse an expression with comprehensive error handling.
"""
try:
return licensing.parse(expression, validate=True, strict=True)
except ExpressionParseError as e:
error_msg = PARSE_ERRORS.get(e.error_code, "Unknown parse error")
return {
'error': 'parse_error',
'message': error_msg,
'position': e.position,
'token': e.token_string
}
except ExpressionError as e:
return {
'error': 'expression_error',
'message': str(e)
}
except Exception as e:
return {
'error': 'unexpected_error',
'message': str(e)
}
# Use the safe parser
licensing = get_spdx_licensing()
result = safe_parse_expression(licensing, 'MIT WITH Apache-2.0')
print(result)from license_expression import PARSE_ERRORS
# Display all available error codes and messages
print("Available parse error codes:")
for code, message in PARSE_ERRORS.items():
print(f" {code}: {message}")from license_expression import Keyword
# Create keyword tokens (used internally by the parser)
and_keyword = Keyword('AND', TOKEN_AND)
print(f"Keyword: {and_keyword.value}, Type: {and_keyword.type}")
print(f"Length: {len(and_keyword)}") # Length of 'AND' = 3For applications that need to provide user-friendly error messages:
from license_expression import (
get_spdx_licensing,
ExpressionParseError,
PARSE_INVALID_SYMBOL_AS_EXCEPTION,
PARSE_INVALID_EXCEPTION
)
def user_friendly_error(expression, error):
"""Convert technical error to user-friendly message."""
if isinstance(error, ExpressionParseError):
if error.error_code == PARSE_INVALID_SYMBOL_AS_EXCEPTION:
return f"'{error.token_string}' is a license, not an exception. Use it without 'WITH'."
elif error.error_code == PARSE_INVALID_EXCEPTION:
return f"'{error.token_string}' is an exception and must be used with 'WITH'."
return f"Error parsing expression: {error}"
# Example usage
licensing = get_spdx_licensing()
try:
licensing.parse('MIT WITH Apache-2.0', validate=True, strict=True)
except ExpressionParseError as e:
friendly_msg = user_friendly_error('MIT WITH Apache-2.0', e)
print(friendly_msg)Install with Tessl CLI
npx tessl i tessl/pypi-license-expression