Skip to content

Commit 5c5b281

Browse files
committed
Add simple tokenizer and improve error checks #29
Also update boolean.py to 3.6 Signed-off-by: Philippe Ombredanne <pombredanne@nexb.com>
1 parent 1e34fc5 commit 5c5b281

6 files changed

Lines changed: 132 additions & 26 deletions

File tree

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
include_package_data=True,
3131
zip_safe=False,
3232
classifiers=[
33-
'Development Status :: 4 - Beta',
33+
'Development Status :: 5 - Production/Stable',
3434
'License :: OSI Approved :: Apache Software License',
3535
'Intended Audience :: Developers',
3636
'Operating System :: OS Independent',
@@ -48,6 +48,6 @@
4848
'licence'
4949
],
5050
install_requires=[
51-
'boolean.py >= 3.5, < 4.0.0',
51+
'boolean.py >= 3.6, < 4.0.0',
5252
]
5353
)

src/license_expression/__init__.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@
5252
from boolean.boolean import PARSE_ERRORS
5353
from boolean.boolean import PARSE_INVALID_EXPRESSION
5454
from boolean.boolean import PARSE_INVALID_NESTING
55+
from boolean.boolean import PARSE_INVALID_OPERATOR_SEQUENCE
5556
from boolean.boolean import PARSE_INVALID_SYMBOL_SEQUENCE
5657
from boolean.boolean import PARSE_UNBALANCED_CLOSING_PARENS
5758
from boolean.boolean import PARSE_UNKNOWN_TOKEN
59+
5860
from boolean.boolean import ParseError
5961
from boolean.boolean import TOKEN_SYMBOL
6062
from boolean.boolean import TOKEN_AND
@@ -239,10 +241,10 @@ def __init__(self, symbols=tuple(), quiet=True):
239241
raise ValueError('\n'.join(warns + errors))
240242

241243
# mapping of known symbol key to symbol for reference
242-
self.known_symbols_by_key = {symbol.key: symbol for symbol in symbols}
244+
self.known_symbols = {symbol.key: symbol for symbol in symbols}
243245

244246
# mapping of known symbol lowercase key to symbol for reference
245-
self.known_symbols_by_keylow = {symbol.key.lower(): symbol for symbol in symbols}
247+
self.known_symbols_lowercase = {symbol.key.lower(): symbol for symbol in symbols}
246248

247249
# Aho-Corasick automaton-based Advanced Tokenizer
248250
self.advanced_tokenizer = None
@@ -375,7 +377,7 @@ def unknown_license_symbols(self, expression, unique=True, **kwargs):
375377
Extra kwargs are passed down to the parse() function.
376378
"""
377379
return [ls for ls in self.license_symbols(expression, unique=unique, decompose=True, **kwargs)
378-
if not ls.key in self.known_symbols_by_key]
380+
if not ls.key in self.known_symbols]
379381

380382
def unknown_license_keys(self, expression, unique=True, **kwargs):
381383
"""
@@ -447,7 +449,7 @@ def parse(self, expression, validate=False, strict=False, simple=False, **kwargs
447449
return
448450
try:
449451
# this will raise a ParseError on errors
450-
tokens = list(self.tokenize(expression, strict=strict))
452+
tokens = list(self.tokenize(expression, strict=strict, simple=simple))
451453
expression = super(Licensing, self).parse(tokens)
452454
except TypeError as e:
453455
msg = 'Invalid expression syntax: ' + repr(e)
@@ -557,9 +559,9 @@ def get_advanced_tokenizer(self):
557559
for keyword in KEYWORDS:
558560
add_item(keyword.value, keyword)
559561

560-
# self.known_symbols_by_key has been created at Licensing initialization time and is
562+
# self.known_symbols has been created at Licensing initialization time and is
561563
# already validated and trusted here
562-
for key, symbol in self.known_symbols_by_key.items():
564+
for key, symbol in self.known_symbols.items():
563565
# always use the key even if there are no aliases.
564566
add_item(key, symbol)
565567
aliases = getattr(symbol, 'aliases', [])
@@ -594,7 +596,7 @@ def simple_tokenizer(self, expression):
594596
tokenizing expressions.
595597
"""
596598

597-
symbols = self.known_symbols_by_keylow or {}
599+
symbols = self.known_symbols_lowercase or {}
598600

599601
for match in _simple_tokenizer(expression):
600602
if not match:

tests/test_license_expression.py

Lines changed: 119 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from collections import namedtuple
2121
from collections import OrderedDict
2222
from unittest import TestCase
23+
from unittest.case import expectedFailure
2324
import sys
2425

2526
from boolean.boolean import PARSE_UNBALANCED_CLOSING_PARENS
@@ -29,6 +30,7 @@
2930
from license_expression import PARSE_INVALID_NESTING
3031
from license_expression import PARSE_INVALID_EXCEPTION
3132
from license_expression import PARSE_INVALID_SYMBOL_AS_EXCEPTION
33+
from license_expression import PARSE_INVALID_OPERATOR_SEQUENCE
3234

3335
from license_expression import ExpressionError
3436
from license_expression import Keyword
@@ -424,8 +426,15 @@ def test_parse_invalid_expression_raise_exception6(self):
424426
try:
425427
licensing.parse(expr)
426428
self.fail("Exception not raised when validating '%s'" % expr)
427-
except ExpressionError as ee:
428-
assert 'OR requires two or more licenses as in: MIT OR BSD' == str(ee)
429+
self.fail('Exception not raised')
430+
except ParseError as pe:
431+
expected = {
432+
'error_code': PARSE_INVALID_OPERATOR_SEQUENCE,
433+
'position': 0,
434+
'token_string': 'OR',
435+
'token_type': TOKEN_OR
436+
}
437+
assert expected == _parse_error_as_dict(pe)
429438

430439
def test_parse_not_invalid_expression_raise_no_exception2(self):
431440
licensing = Licensing()
@@ -466,18 +475,28 @@ def test_parse_errors_catch_invalid_expression_with_bare_and(self):
466475
try:
467476
licensing.parse('and')
468477
self.fail('Exception not raised')
469-
except ExpressionError as pe:
470-
expected = 'AND requires two or more licenses as in: MIT AND BSD'
471-
assert expected == str(pe)
478+
except ParseError as pe:
479+
expected = {
480+
'error_code': PARSE_INVALID_OPERATOR_SEQUENCE,
481+
'position': 0,
482+
'token_string': 'and',
483+
'token_type': TOKEN_AND
484+
}
485+
assert expected == _parse_error_as_dict(pe)
472486

473487
def test_parse_errors_catch_invalid_expression_with_or_and_no_other(self):
474488
licensing = Licensing()
475489
try:
476490
licensing.parse('or that')
477491
self.fail('Exception not raised')
478-
except ExpressionError as pe:
479-
expected = 'OR requires two or more licenses as in: MIT OR BSD'
480-
assert expected == str(pe)
492+
except ParseError as pe:
493+
expected = {
494+
'error_code': PARSE_INVALID_OPERATOR_SEQUENCE,
495+
'position': 0,
496+
'token_string': 'or',
497+
'token_type': TOKEN_OR
498+
}
499+
assert expected == _parse_error_as_dict(pe)
481500

482501
def test_parse_errors_catch_invalid_expression_with_empty_parens(self):
483502
licensing = Licensing()
@@ -828,6 +847,78 @@ def test_Licensing_can_parse_valid_expressions_with_symbols_that_contain_spaces(
828847
expected = 'GPL-2.0 OR (mit AND LGPL 2.1) OR bsd OR GPL-2.0 OR (mit AND LGPL 2.1)'
829848
assert expected == str(parsed)
830849

850+
def test_parse_invalid_expression_with_trailing_or(self):
851+
licensing = Licensing()
852+
expr = 'mit or'
853+
try:
854+
licensing.parse(expr)
855+
self.fail("Exception not raised when validating '%s'" % expr)
856+
except ExpressionError as ee:
857+
assert 'OR requires two or more licenses as in: MIT OR BSD' == str(ee)
858+
859+
def test_parse_invalid_expression_with_trailing_or_and_valid_start_does_not_raise_exception(self):
860+
licensing = Licensing()
861+
expression = ' mit or mit or '
862+
parsed = licensing.parse(expression)
863+
# ExpressionError: OR requires two or more licenses as in: MIT OR BSD
864+
expected = 'mit OR mit'
865+
assert expected == str(parsed)
866+
867+
def test_parse_invalid_expression_with_repeated_trailing_or_raise_exception(self):
868+
licensing = Licensing()
869+
expression = 'mit or mit or or'
870+
try:
871+
licensing.parse(expression, simple=False)
872+
self.fail('Exception not raised')
873+
except ParseError as pe:
874+
expected = {
875+
'error_code': PARSE_INVALID_OPERATOR_SEQUENCE,
876+
'position': 14,
877+
'token_string': 'or',
878+
'token_type': TOKEN_OR
879+
}
880+
assert expected == _parse_error_as_dict(pe)
881+
882+
@expectedFailure
883+
def test_parse_invalid_expression_with_single_trailing_or_raise_exception(self):
884+
licensing = Licensing()
885+
expression = 'mit or mit or'
886+
try:
887+
licensing.parse(expression, simple=False)
888+
self.fail('Exception not raised')
889+
except ParseError as pe:
890+
expected = {
891+
'error_code': PARSE_INVALID_OPERATOR_SEQUENCE,
892+
'position': 14,
893+
'token_string': 'or',
894+
'token_type': TOKEN_OR
895+
}
896+
assert expected == _parse_error_as_dict(pe)
897+
898+
def test_parse_invalid_expression_with_single_trailing_and_raise_exception(self):
899+
licensing = Licensing()
900+
expression = 'mit or mit and'
901+
try:
902+
licensing.parse(expression, simple=False)
903+
self.fail('Exception not raised')
904+
except ExpressionError as ee:
905+
assert 'AND requires two or more licenses as in: MIT AND BSD' == str(ee)
906+
907+
def test_parse_invalid_expression_with_single_leading_or_raise_exception(self):
908+
licensing = Licensing()
909+
expression = 'or mit or mit'
910+
try:
911+
licensing.parse(expression, simple=False)
912+
self.fail('Exception not raised')
913+
except ParseError as pe:
914+
expected = {
915+
'error_code': PARSE_INVALID_OPERATOR_SEQUENCE,
916+
'position': 0,
917+
'token_string': 'or',
918+
'token_type': TOKEN_OR
919+
}
920+
assert expected == _parse_error_as_dict(pe)
921+
831922

832923
class LicensingParseWithSymbolsSimpleTest(TestCase):
833924

@@ -1950,17 +2041,29 @@ def test_and_and_or_is_invalid(self):
19502041
try:
19512042
licensing.parse(expression)
19522043
self.fail('Exception not raised')
1953-
except ExpressionError as e:
1954-
assert 'AND requires two or more licenses as in: MIT AND BSD' == str(e)
2044+
except ParseError as pe:
2045+
expected = {
2046+
'error_code': PARSE_INVALID_OPERATOR_SEQUENCE,
2047+
'position': 27,
2048+
'token_string': 'and',
2049+
'token_type': TOKEN_AND}
2050+
assert expected == _parse_error_as_dict(pe)
19552051

1956-
def test_or_or_is_not_invalid(self):
2052+
def test_or_or_is_invalid(self):
19572053
expression = 'gpl-2.0 with classpath or or or or gpl-2.0-plus'
19582054
licensing = Licensing()
1959-
result = str(licensing.parse(expression))
1960-
assert 'gpl-2.0 WITH classpath OR gpl-2.0-plus' == result
2055+
try:
2056+
licensing.parse(expression)
2057+
except ParseError as pe:
2058+
expected = {
2059+
'error_code': PARSE_INVALID_OPERATOR_SEQUENCE,
2060+
'position': 26,
2061+
'token_string': 'or',
2062+
'token_type': TOKEN_OR}
2063+
assert expected == _parse_error_as_dict(pe)
19612064

19622065
def test_tokenize_or_or(self):
1963-
expression = 'gpl-2.0 with classpath or or gpl-2.0-plus'
2066+
expression = 'gpl-2.0 with classpath or or or gpl-2.0-plus'
19642067
licensing = Licensing()
19652068
results = list(licensing.tokenize(expression))
19662069
expected = [
@@ -1969,7 +2072,8 @@ def test_tokenize_or_or(self):
19692072
exception_symbol=LicenseSymbol(u'classpath')), 'gpl-2.0 with classpath', 0),
19702073
(2, 'or', 23),
19712074
(2, 'or', 26),
1972-
(LicenseSymbol(u'gpl-2.0-plus'), 'gpl-2.0-plus', 29)
2075+
(2, 'or', 29),
2076+
(LicenseSymbol(u'gpl-2.0-plus'), 'gpl-2.0-plus', 32)
19732077
]
19742078

19752079
assert expected == results
-22.4 KB
Binary file not shown.
21.5 KB
Binary file not shown.

thirdparty/prod/boolean.py.ABOUT

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
about_resource: boolean.py-3.5-py2.py3-none-any.whl
2-
version: 3.5
1+
about_resource: boolean.py-3.6-py2.py3-none-any.whl
2+
version: 3.6
33
download_url: https://pypi.python.org/packages/80/f3/0508ae7ba76b02f7fd666b705766edc1863fc8ef29d0519b4c95d60ab1bb/boolean.py-3.5-py2.py3-none-any.whl#md5=cf90b0c0530663bbf71a53fb58f6fa72
44

55
name: boolean.py

0 commit comments

Comments
 (0)