Skip to content

Commit ccfe96b

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 ccfe96b

9 files changed

Lines changed: 191 additions & 96 deletions

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: 10 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@
3939
from copy import deepcopy
4040
from functools import total_ordering
4141
import itertools
42-
import logging
43-
from pprint import pprint
4442
import re
4543
import string
4644

@@ -52,9 +50,11 @@
5250
from boolean.boolean import PARSE_ERRORS
5351
from boolean.boolean import PARSE_INVALID_EXPRESSION
5452
from boolean.boolean import PARSE_INVALID_NESTING
53+
from boolean.boolean import PARSE_INVALID_OPERATOR_SEQUENCE
5554
from boolean.boolean import PARSE_INVALID_SYMBOL_SEQUENCE
5655
from boolean.boolean import PARSE_UNBALANCED_CLOSING_PARENS
5756
from boolean.boolean import PARSE_UNKNOWN_TOKEN
57+
5858
from boolean.boolean import ParseError
5959
from boolean.boolean import TOKEN_SYMBOL
6060
from boolean.boolean import TOKEN_AND
@@ -65,6 +65,7 @@
6565
from license_expression._pyahocorasick import Trie as AdvancedTokenizer
6666
from license_expression._pyahocorasick import Token
6767

68+
6869
# Python 2 and 3 support
6970
try:
7071
# Python 2
@@ -74,23 +75,6 @@
7475
# Python 3
7576
unicode = str # NOQA
7677

77-
TRACE = False
78-
79-
logger = logging.getLogger(__name__)
80-
81-
82-
def logger_debug(*args):
83-
pass
84-
85-
86-
if TRACE:
87-
88-
def logger_debug(*args):
89-
return logger.debug(' '.join(isinstance(a, str) and a or repr(a) for a in args))
90-
91-
import sys
92-
logging.basicConfig(stream=sys.stdout)
93-
logger.setLevel(logging.DEBUG)
9478

9579
# append new error codes to PARSE_ERRORS by monkey patching
9680
PARSE_EXPRESSION_NOT_UNICODE = 100
@@ -239,10 +223,10 @@ def __init__(self, symbols=tuple(), quiet=True):
239223
raise ValueError('\n'.join(warns + errors))
240224

241225
# mapping of known symbol key to symbol for reference
242-
self.known_symbols_by_key = {symbol.key: symbol for symbol in symbols}
226+
self.known_symbols = {symbol.key: symbol for symbol in symbols}
243227

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

247231
# Aho-Corasick automaton-based Advanced Tokenizer
248232
self.advanced_tokenizer = None
@@ -375,7 +359,7 @@ def unknown_license_symbols(self, expression, unique=True, **kwargs):
375359
Extra kwargs are passed down to the parse() function.
376360
"""
377361
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]
362+
if not ls.key in self.known_symbols]
379363

380364
def unknown_license_keys(self, expression, unique=True, **kwargs):
381365
"""
@@ -447,7 +431,7 @@ def parse(self, expression, validate=False, strict=False, simple=False, **kwargs
447431
return
448432
try:
449433
# this will raise a ParseError on errors
450-
tokens = list(self.tokenize(expression, strict=strict))
434+
tokens = list(self.tokenize(expression, strict=strict, simple=simple))
451435
expression = super(Licensing, self).parse(tokens)
452436
except TypeError as e:
453437
msg = 'Invalid expression syntax: ' + repr(e)
@@ -488,40 +472,20 @@ def tokenize(self, expression, strict=False, simple=False):
488472
raise ParseError(error_code=PARSE_EXPRESSION_NOT_UNICODE)
489473

490474
if simple:
491-
if TRACE: logger_debug('using simple tokenizer')
492475
tokens = self.simple_tokenizer(expression)
493476
else:
494-
if TRACE: logger_debug('using advanced tokenizer')
495477
advanced_tokenizer = self.get_advanced_tokenizer()
496478
tokens = advanced_tokenizer.tokenize(expression)
497479

498-
if TRACE:
499-
tokens = list(tokens)
500-
logger_debug('tokenize: tokens')
501-
pprint(tokens)
502-
503480
# Assign symbol for unknown tokens
504481
tokens = build_symbols_from_unknown_tokens(tokens)
505-
if TRACE:
506-
tokens = list(tokens)
507-
logger_debug('tokenize: token with symbols')
508-
pprint(tokens)
509482

510483
# skip whitespace-only tokens
511484
tokens = (t for t in tokens if t.string and t.string.strip())
512-
if TRACE:
513-
tokens = list(tokens)
514-
logger_debug('tokenize: token NO spaces')
515-
pprint(tokens)
516485

517486
# create atomic LicenseWithExceptionSymbol from WITH subexpressions
518487
tokens = replace_with_subexpression_by_license_symbol(tokens, strict)
519488

520-
if TRACE:
521-
tokens = list(tokens)
522-
logger_debug('tokenize: LicenseWithExceptionSymbol replaced')
523-
pprint(tokens)
524-
525489
# finally yield the actual args expected by the boolean parser
526490
for token in tokens:
527491
pos = token.start
@@ -557,9 +521,9 @@ def get_advanced_tokenizer(self):
557521
for keyword in KEYWORDS:
558522
add_item(keyword.value, keyword)
559523

560-
# self.known_symbols_by_key has been created at Licensing initialization time and is
524+
# self.known_symbols has been created at Licensing initialization time and is
561525
# already validated and trusted here
562-
for key, symbol in self.known_symbols_by_key.items():
526+
for key, symbol in self.known_symbols.items():
563527
# always use the key even if there are no aliases.
564528
add_item(key, symbol)
565529
aliases = getattr(symbol, 'aliases', [])
@@ -594,7 +558,7 @@ def simple_tokenizer(self, expression):
594558
tokenizing expressions.
595559
"""
596560

597-
symbols = self.known_symbols_by_keylow or {}
561+
symbols = self.known_symbols_lowercase or {}
598562

599563
for match in _simple_tokenizer(expression):
600564
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.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
about_resource: boolean.py-3.6-py2.py3-none-any.whl
2+
attribute: true
3+
checksum_md5: da39999eb131b589e84ad935dc4ca642
4+
checksum_sha1: d31b55e7ad2ee917232b3213afe3ae9678156a9f
5+
copyright: Copyright (c) 2009-2016 Sebastian Kraemer, basti.kr@gmail.com and others
6+
description: Implements boolean algebra in one module.
7+
download_url: https://files.pythonhosted.org/packages/9b/27/d22062a221010e17935237ba4b574cd828238ea02e0765337c238466a512/boolean.py-3.6-py2.py3-none-any.whl
8+
homepage_url: https://github.com/bastikr/boolean.py
9+
license_expression: bsd-simplified
10+
licenses:
11+
- file: bsd-simplified.LICENSE
12+
key: bsd-simplified
13+
name: BSD-2-Clause
14+
name: boolean.py
15+
notice_file: boolean.py-3.6-py2.py3-none-any.whl.NOTICE
16+
notice_url: https://github.com/bastikr/boolean.py/blob/master/LICENSE.txt
17+
owner: Sebastian Kraemer
18+
version: '3.6'

0 commit comments

Comments
 (0)