diff --git a/pex/interpreter_constraints.py b/pex/interpreter_constraints.py index c174b228e..a4fffc4e3 100644 --- a/pex/interpreter_constraints.py +++ b/pex/interpreter_constraints.py @@ -7,6 +7,7 @@ from pex.common import die from pex.interpreter import PythonIdentity +from pex.third_party import boolean from pex.tracer import TRACER @@ -26,13 +27,70 @@ def matched_interpreters_iter(interpreters_iter, constraints): :param interpreters_iter: A `PythonInterpreter` iterable for filtering. :param constraints: A sequence of strings that constrain the interpreter compatibility for this - pex. Each string uses the Requirement-style format, e.g. 'CPython>=3' or '>=2.7,<3' for - requirements agnostic to interpreter class. Multiple requirement strings may be combined - into a list to OR the constraints, such as ['CPython>=2.7,<3', 'CPython>=3.4']. + pex. Eeach string is an arbitrary boolean expression in which the atoms are Requirement-style + strings such as 'CPython>=3', or '>=2.7,<3' for requirements agnostic to interpreter class. + The infix boolean operators are |, & and ~, and parentheses are used for precedence. + Multiple requirement strings are OR-ed, e.g., ['CPython>=2.7,<3', 'CPython>=3.4'], is the same + as ['CPython>=2.7,<3 | CPython>=3.4']. :return interpreter: returns a generator that yields compatible interpreters """ + # TODO: Deprecate specifying multiple constraints, and instead require the input to be a + # single explicit boolean expression. + constraint_expr = '({})'.format(' | '.join(constraints)) for interpreter in interpreters_iter: - if any(interpreter.identity.matches(filt) for filt in constraints): + if match_interpreter_constraint(interpreter.identity, constraint_expr): TRACER.log("Constraints on interpreters: %s, Matching Interpreter: %s" % (constraints, interpreter.binary), V=3) yield interpreter + + +class ConstraintAlgebra(boolean.BooleanAlgebra): + def __init__(self, identity): + super(ConstraintAlgebra, self).__init__() + self._identity = identity + + def tokenize(self, s): + # Remove all spaces from the string. Doesn't change its semantics, but makes it much + # easier to tokenize. + s = ''.join(s.split()) + if not s: + return + ops = { + '|': boolean.TOKEN_OR, + '&': boolean.TOKEN_AND, + '~': boolean.TOKEN_NOT, + '(': boolean.TOKEN_LPAR, + ')': boolean.TOKEN_RPAR, + } + s = '({})'.format(s) # Wrap with parens, to simplify constraint tokenizing. + it = enumerate(s) + try: + i, c = next(it) + while True: + if c in ops: + yield ops[c], c, i + i, c = next(it) + else: + constraint_start = i + while not c in ops: + i, c = next(it) # We wrapped with parens, so this cannot throw StopIteration. + constraint = s[constraint_start:i] + yield ((boolean.TOKEN_TRUE if self._identity.matches(constraint) + else boolean.TOKEN_FALSE), + constraint, constraint_start) + except StopIteration: + pass + + +def match_interpreter_constraint(identity, constraint_expr): + """Return True iff the given identity matches the constraint expression. + + The constraint expression is an arbitrary boolean expression in which the atoms are + Requirement-style strings such as 'CPython>=2.7,<3', the infix boolean operators are |, & and ~, + and parentheses are used for precedence. + + :param identity: A `pex.interpreter.PythonIdentity` instance. + :param constraint_expr: A boolean interpreter constraint expression. + """ + algebra = ConstraintAlgebra(identity) + return bool(algebra.parse(constraint_expr).simplify()) diff --git a/pex/pex_builder.py b/pex/pex_builder.py index afa735382..b91d49889 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -370,7 +370,7 @@ def _prepare_bootstrap(self): # NB: We use pip here in the builder, but that's only at buildtime and # although we don't use pyparsing directly, packaging.markers, which we # do use at runtime, does. - root_module_names=['packaging', 'pkg_resources', 'pyparsing']) + root_module_names=['boolean', 'packaging', 'pkg_resources', 'pyparsing']) source_name = 'pex' provider = get_provider(source_name) diff --git a/pex/vendor/README.md b/pex/vendor/README.md index e2e998e68..04dde5e19 100644 --- a/pex/vendor/README.md +++ b/pex/vendor/README.md @@ -11,29 +11,7 @@ re-vendor when the fix is released. To update versions of vendored code or add new vendored code: -1. Modify `pex.vendor.iter_vendor_specs` with updated versions or new distributions. - Today that function looks like: - ```python - def iter_vendor_specs(): - """Iterate specifications for code vendored by pex. - - :return: An iterator over specs of all vendored code. - :rtype: :class:`collection.Iterator` of :class:`VendorSpec` - """ - # We use this via pex.third_party at runtime to check for compatible wheel tags. - yield VendorSpec.pinned('packaging', '19.2') - - # We shell out to pip at buildtime to resolve and install dependencies. - # N.B.: This is pip 20.0.dev0 with a patch to support foreign download targets more fully. - yield VendorSpec.vcs('git+https://github.com/pantsbuild/pip@5eb9470c0c59#egg=pip', rewrite=False) - - # We expose this to pip at buildtime for legacy builds, but we also use pkg_resources via - # pex.third_party at runtime in various ways. - yield VendorSpec.pinned('setuptools', '42.0.2') - - # We expose this to pip at buildtime for legacy builds. - yield VendorSpec.pinned('wheel', '0.33.6', rewrite=False) - ``` +1. Modify [`pex.vendor.iter_vendor_specs`](./__init__.py#L91) with updated versions or new distributions. Simply edit an existing `VendorSpec` or `yield` a new one. 2. Run `tox -e vendor`. This will replace all vendored code even if versions have not changed and then rewrite any diff --git a/pex/vendor/__init__.py b/pex/vendor/__init__.py index 9538b9d8e..1978a7aa7 100644 --- a/pex/vendor/__init__.py +++ b/pex/vendor/__init__.py @@ -97,6 +97,12 @@ def iter_vendor_specs(): # We use this via pex.third_party at runtime to check for compatible wheel tags. yield VendorSpec.pinned('packaging', '19.2') + # We use this to evaluate interpreter compatibility expressions. + # TODO: Switch to a published release once https://github.com/bastikr/boolean.py/pull/95 + # is merged and released: + #yield VendorSpec.pinned('boolean.py', '3.8') + yield VendorSpec.vcs('git+https://github.com/benjyw/boolean.py@db41511ea311#egg=boolean') + # We shell out to pip at buildtime to resolve and install dependencies. # N.B.: This is pip 20.0.dev0 with a patch to support foreign download targets more fully. yield VendorSpec.vcs('git+https://github.com/pantsbuild/pip@5eb9470c0c59#egg=pip', rewrite=False) diff --git a/pex/vendor/__main__.py b/pex/vendor/__main__.py index 80558e646..c0ce715a5 100644 --- a/pex/vendor/__main__.py +++ b/pex/vendor/__main__.py @@ -82,17 +82,17 @@ def __init__(self, prefix, packages): self._packages = packages def rewrite(self, python_file): - modififications = OrderedDict() + modifications = OrderedDict() red_baron = self._parse(python_file) - modififications.update(self._modify__import__calls(red_baron)) - modififications.update(self._modify_import_statements(red_baron)) - modififications.update(self._modify_from_import_statements(red_baron)) + modifications.update(self._modify__import__calls(red_baron)) + modifications.update(self._modify_import_statements(red_baron)) + modifications.update(self._modify_from_import_statements(red_baron)) - if modififications: + if modifications: with open(python_file, 'w') as fp: fp.write(red_baron.dumps()) - return modififications + return modifications def _modify__import__calls(self, red_baron): # noqa: We want __import__ as part of the name. for call_node in red_baron.find_all('CallNode'): diff --git a/pex/vendor/_vendored/boolean/__init__.py b/pex/vendor/_vendored/boolean/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/INSTALLER b/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/INSTALLER new file mode 100644 index 000000000..a1b589e38 --- /dev/null +++ b/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/LICENSE.txt b/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/LICENSE.txt new file mode 100644 index 000000000..7521c9b68 --- /dev/null +++ b/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/LICENSE.txt @@ -0,0 +1,23 @@ +Copyright (c) 2009-2017 Sebastian Kraemer, basti.kr@gmail.com +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/METADATA b/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/METADATA new file mode 100644 index 000000000..d8e5d9399 --- /dev/null +++ b/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/METADATA @@ -0,0 +1,39 @@ +Metadata-Version: 2.1 +Name: boolean.py +Version: 3.7 +Summary: Define boolean algebras, create and parse boolean expressions and create custom boolean DSL. +Home-page: https://github.com/bastikr/boolean.py +Author: Sebastian Kraemer +Author-email: basti.kr@gmail.com +License: BSD-2-Clause +Keywords: boolean expression,boolean algebra,logic,expression parser +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Scientific/Engineering :: Mathematics +Classifier: Topic :: Software Development :: Compilers +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities + + +This library helps you deal with boolean expressions and algebra with variables +and the boolean functions AND, OR, NOT. + +You can parse expressions from strings and simplify and compare expressions. +You can also easily create your custom algreba and mini DSL and create custom +tokenizers to handle custom expressions. + +For extensive documentation look either into the docs directory or view it online, at +https://booleanpy.readthedocs.org/en/latest/ + +https://github.com/bastikr/boolean.py + +Copyright (c) 2009-2017 Sebastian Kraemer, basti.kr@gmail.com and others + +Released under revised BSD license aka. BSD Simplified or BSD-2-Clause. + + diff --git a/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/WHEEL b/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/WHEEL new file mode 100644 index 000000000..8b701e93c --- /dev/null +++ b/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.33.6) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/top_level.txt b/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/top_level.txt new file mode 100644 index 000000000..7b19ee8df --- /dev/null +++ b/pex/vendor/_vendored/boolean/boolean.py-3.7.dist-info/top_level.txt @@ -0,0 +1 @@ +boolean diff --git a/pex/vendor/_vendored/boolean/boolean/__init__.py b/pex/vendor/_vendored/boolean/boolean/__init__.py new file mode 100644 index 000000000..e8648785d --- /dev/null +++ b/pex/vendor/_vendored/boolean/boolean/__init__.py @@ -0,0 +1,101 @@ +""" +Boolean Algebra. + +This module defines a Boolean Algebra over the set {TRUE, FALSE} with boolean +variables and the boolean functions AND, OR, NOT. For extensive documentation +look either into the docs directory or view it online, at +https://booleanpy.readthedocs.org/en/latest/. + +Copyright (c) 2009-2017 Sebastian Kraemer, basti.kr@gmail.com +Released under revised BSD license. +""" + +from __future__ import absolute_import +from __future__ import unicode_literals +from __future__ import print_function + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import BooleanAlgebra # vendor:skip +else: + from pex.third_party.boolean.boolean import BooleanAlgebra + + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import Expression # vendor:skip +else: + from pex.third_party.boolean.boolean import Expression + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import Symbol # vendor:skip +else: + from pex.third_party.boolean.boolean import Symbol + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import ParseError # vendor:skip +else: + from pex.third_party.boolean.boolean import ParseError + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import PARSE_ERRORS # vendor:skip +else: + from pex.third_party.boolean.boolean import PARSE_ERRORS + + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import AND # vendor:skip +else: + from pex.third_party.boolean.boolean import AND + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import NOT # vendor:skip +else: + from pex.third_party.boolean.boolean import NOT + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import OR # vendor:skip +else: + from pex.third_party.boolean.boolean import OR + + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import TOKEN_TRUE # vendor:skip +else: + from pex.third_party.boolean.boolean import TOKEN_TRUE + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import TOKEN_FALSE # vendor:skip +else: + from pex.third_party.boolean.boolean import TOKEN_FALSE + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import TOKEN_SYMBOL # vendor:skip +else: + from pex.third_party.boolean.boolean import TOKEN_SYMBOL + + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import TOKEN_AND # vendor:skip +else: + from pex.third_party.boolean.boolean import TOKEN_AND + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import TOKEN_OR # vendor:skip +else: + from pex.third_party.boolean.boolean import TOKEN_OR + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import TOKEN_NOT # vendor:skip +else: + from pex.third_party.boolean.boolean import TOKEN_NOT + + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import TOKEN_LPAR # vendor:skip +else: + from pex.third_party.boolean.boolean import TOKEN_LPAR + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import TOKEN_RPAR # vendor:skip +else: + from pex.third_party.boolean.boolean import TOKEN_RPAR + diff --git a/pex/vendor/_vendored/boolean/boolean/boolean.py b/pex/vendor/_vendored/boolean/boolean/boolean.py new file mode 100644 index 000000000..3419b6888 --- /dev/null +++ b/pex/vendor/_vendored/boolean/boolean/boolean.py @@ -0,0 +1,1469 @@ +""" +Boolean expressions algebra. + +This module defines a Boolean algebra over the set {TRUE, FALSE} with boolean +variables called Symbols and the boolean functions AND, OR, NOT. + +Some basic logic comparison is supported: two expressions can be +compared for equivalence or containment. Furthermore you can simplify +an expression and obtain its normal form. + +You can create expressions in Python using familiar boolean operators +or parse expressions from strings. The parsing can be extended with +your own tokenizer. You can also customize how expressions behave and +how they are presented. + +For extensive documentation look either into the docs directory or view it +online, at https://booleanpy.readthedocs.org/en/latest/. + +Copyright (c) 2009-2017 Sebastian Kraemer, basti.kr@gmail.com and others +Released under revised BSD license. +""" + +from __future__ import absolute_import +from __future__ import unicode_literals +from __future__ import print_function + +import inspect +import itertools + +# Python 2 and 3 +try: + basestring # NOQA +except NameError: + basestring = str # NOQA + +# Set to True to enable tracing for parsing +TRACE_PARSE = False + +# Token types for standard operators and parens +TOKEN_AND = 1 +TOKEN_OR = 2 +TOKEN_NOT = 3 +TOKEN_LPAR = 4 +TOKEN_RPAR = 5 +TOKEN_TRUE = 6 +TOKEN_FALSE = 7 +TOKEN_SYMBOL = 8 + +TOKEN_TYPES = { + TOKEN_AND: 'AND', + TOKEN_OR: 'OR', + TOKEN_NOT: 'NOT', + TOKEN_LPAR: '(', + TOKEN_RPAR: ')', + TOKEN_TRUE: 'TRUE', + TOKEN_FALSE: 'FALSE', + TOKEN_SYMBOL: 'SYMBOL', +} + + +# parsing error code and messages +PARSE_UNKNOWN_TOKEN = 1 +PARSE_UNBALANCED_CLOSING_PARENS = 2 +PARSE_INVALID_EXPRESSION = 3 +PARSE_INVALID_NESTING = 4 +PARSE_INVALID_SYMBOL_SEQUENCE = 5 +PARSE_INVALID_OPERATOR_SEQUENCE = 6 + +PARSE_ERRORS = { + PARSE_UNKNOWN_TOKEN: 'Unknown token', + PARSE_UNBALANCED_CLOSING_PARENS: 'Unbalanced parenthesis', + PARSE_INVALID_EXPRESSION: 'Invalid expression', + PARSE_INVALID_NESTING: 'Invalid expression nesting such as (AND xx)', + PARSE_INVALID_SYMBOL_SEQUENCE: 'Invalid symbols sequence such as (A B)', + PARSE_INVALID_OPERATOR_SEQUENCE: 'Invalid operator sequence without symbols such as AND OR or OR OR', +} + + +class ParseError(Exception): + """ + Raised when the parser or tokenizer encounters a syntax error. Instances of + this class have attributes token_type, token_string, position, error_code to + access the details of the error. str() of the exception instance returns a + formatted message. + """ + def __init__(self, token_type=None, token_string='', position=-1, error_code=0): + self.token_type = token_type + self.token_string = token_string + self.position = position + self.error_code = error_code + + def __str__(self, *args, **kwargs): + emsg = PARSE_ERRORS.get(self.error_code, 'Unknown parsing error') + + tstr = '' + if self.token_string: + tstr = ' for token: "%s"' % self.token_string + + pos = '' + if self.position > 0: + pos = ' at position: %d' % self.position + + return '{emsg}{tstr}{pos}'.format(**locals()) + + +class BooleanAlgebra(object): + """ + An algebra is defined by: + - the types of its operations and Symbol. + - the tokenizer used when parsing expressions from strings. + + This class also serves as a base class for all boolean expressions, + including base elements, functions and variable symbols. + """ + + def __init__(self, TRUE_class=None, FALSE_class=None, Symbol_class=None, + NOT_class=None, AND_class=None, OR_class=None, + allowed_in_token=('.', ':', '_')): + """ + The types for TRUE, FALSE, NOT, AND, OR and Symbol define the boolean + algebra elements, operations and Symbol variable. They default to the + standard classes if not provided. + + You can customize an algebra by providing alternative subclasses of the + standard types. + """ + # TRUE and FALSE base elements are algebra-level "singleton" instances + self.TRUE = TRUE_class or _TRUE + self.TRUE = self.TRUE() + + self.FALSE = FALSE_class or _FALSE + self.FALSE = self.FALSE() + + # they cross-reference each other + self.TRUE.dual = self.FALSE + self.FALSE.dual = self.TRUE + + # boolean operation types, defaulting to the standard types + self.NOT = NOT_class or NOT + self.AND = AND_class or AND + self.OR = OR_class or OR + + # class used for Symbols + self.Symbol = Symbol_class or Symbol + + tf_nao = { + 'TRUE': self.TRUE, + 'FALSE': self.FALSE, + 'NOT': self.NOT, + 'AND': self.AND, + 'OR': self.OR, + 'Symbol': self.Symbol + } + + # setup cross references such that all algebra types and + # objects hold a named attribute for every other types and + # objects, including themselves. + for obj in tf_nao.values(): + for name, value in tf_nao.items(): + setattr(obj, name, value) + + # Set the set of characters allowed in tokens + self.allowed_in_token = allowed_in_token + + def definition(self): + """ + Return a tuple of this algebra defined elements and types as: + (TRUE, FALSE, NOT, AND, OR, Symbol) + """ + return self.TRUE, self.FALSE, self.NOT, self.AND, self.OR, self.Symbol + + def symbols(self, *args): + """ + Return a tuple of symbols building a new Symbol from each argument. + """ + return tuple(map(self.Symbol, args)) + + def parse(self, expr, simplify=False): + """ + Return a boolean expression parsed from `expr` either a unicode string + or tokens iterable. + + Optionally simplify the expression if `simplify` is True. + + Raise ParseError on errors. + + If `expr` is a string, the standard `tokenizer` is used for tokenization + and the algebra configured Symbol type is used to create Symbol + instances from Symbol tokens. + + If `expr` is an iterable, it should contain 3-tuples of: (token_type, + token_string, token_position). In this case, the `token_type` can be + a Symbol instance or one of the TOKEN_* constant types. + See the `tokenize()` method for detailed specification. + """ + + precedence = {self.NOT: 5, self.AND: 10, self.OR: 15, TOKEN_LPAR: 20} + + if isinstance(expr, basestring): + tokenized = self.tokenize(expr) + else: + tokenized = iter(expr) + + if TRACE_PARSE: + tokenized = list(tokenized) + print('tokens:') + for t in tokenized: + print(t) + tokenized = iter(tokenized) + + # the abstract syntax tree for this expression that will be build as we + # process tokens + # the first two items are None + # symbol items are appended to this structure + ast = [None, None] + + def is_sym(_t): + return isinstance(_t, Symbol) or _t in (TOKEN_TRUE, TOKEN_FALSE, TOKEN_SYMBOL) + + def is_operator(_t): + return _t in (TOKEN_AND, TOKEN_OR) + + prev_token = None + for token_type, token_string, token_position in tokenized: + if TRACE_PARSE: + print('\nprocessing token_type:', repr(token_type), 'token_string:', repr(token_string), 'token_position:', repr(token_position)) + + if prev_token: + prev_token_type, _prev_token_string, _prev_token_position = prev_token + if TRACE_PARSE: + print(' prev_token:', repr(prev_token)) + + if is_sym(prev_token_type) and (is_sym(token_type)): # or token_type == TOKEN_LPAR) : + raise ParseError(token_type, token_string, token_position, PARSE_INVALID_SYMBOL_SEQUENCE) + + if is_operator(prev_token_type) and (is_operator(token_type) or token_type == TOKEN_RPAR): + raise ParseError(token_type, token_string, token_position, PARSE_INVALID_OPERATOR_SEQUENCE) + + else: + if is_operator(token_type): + raise ParseError(token_type, token_string, token_position, PARSE_INVALID_OPERATOR_SEQUENCE) + + if token_type == TOKEN_SYMBOL: + ast.append(self.Symbol(token_string)) + if TRACE_PARSE: + print(' ast: token_type is TOKEN_SYMBOL: append new symbol', repr(ast)) + + elif isinstance(token_type, Symbol): + ast.append(token_type) + if TRACE_PARSE: + print(' ast: token_type is Symbol): append existing symbol', repr(ast)) + + elif token_type == TOKEN_TRUE: + ast.append(self.TRUE) + if TRACE_PARSE: print(' ast: token_type is TOKEN_TRUE:', repr(ast)) + + elif token_type == TOKEN_FALSE: + ast.append(self.FALSE) + if TRACE_PARSE: print(' ast: token_type is TOKEN_FALSE:', repr(ast)) + + elif token_type == TOKEN_NOT: + ast = [ast, self.NOT] + if TRACE_PARSE: print(' ast: token_type is TOKEN_NOT:', repr(ast)) + + elif token_type == TOKEN_AND: + # if not prev_token or not is_sym(prev_token_type): + # raise ParseError(token_type, token_string, token_position, PARSE_INVALID_OPERATOR_SEQUENCE) + + ast = self._start_operation(ast, self.AND, precedence) + if TRACE_PARSE: + print(' ast:token_type is TOKEN_AND: start_operation', ast) + + elif token_type == TOKEN_OR: + # if not prev_token or not is_sym(prev_token_type): + # raise ParseError(token_type, token_string, token_position, PARSE_INVALID_OPERATOR_SEQUENCE) + + ast = self._start_operation(ast, self.OR, precedence) + if TRACE_PARSE: + print(' ast:token_type is TOKEN_OR: start_operation', ast) + + elif token_type == TOKEN_LPAR: + if prev_token: + # Check that an opening parens is preceded by a function + # or an opening parens + if prev_token_type not in (TOKEN_NOT, TOKEN_AND, TOKEN_OR, TOKEN_LPAR): + raise ParseError(token_type, token_string, token_position, PARSE_INVALID_NESTING) + ast = [ast, TOKEN_LPAR] + + elif token_type == TOKEN_RPAR: + while True: + if ast[0] is None: + raise ParseError(token_type, token_string, token_position, PARSE_UNBALANCED_CLOSING_PARENS) + + if ast[1] is TOKEN_LPAR: + ast[0].append(ast[2]) + if TRACE_PARSE: print('ast9:', repr(ast)) + ast = ast[0] + if TRACE_PARSE: print('ast10:', repr(ast)) + break + + if isinstance(ast[1], int): + raise ParseError(token_type, token_string, token_position, PARSE_UNBALANCED_CLOSING_PARENS) + + # the parens are properly nested + # the top ast node should be a function subclass + if not (inspect.isclass(ast[1]) and issubclass(ast[1], Function)): + raise ParseError(token_type, token_string, token_position, PARSE_INVALID_NESTING) + + subex = ast[1](*ast[2:]) + ast[0].append(subex) + if TRACE_PARSE: print('ast11:', repr(ast)) + ast = ast[0] + if TRACE_PARSE: print('ast12:', repr(ast)) + else: + raise ParseError(token_type, token_string, token_position, PARSE_UNKNOWN_TOKEN) + + prev_token = (token_type, token_string, token_position) + + try: + while True: + if ast[0] is None: + if TRACE_PARSE: print('ast[0] is None:', repr(ast)) + if ast[1] is None: + if TRACE_PARSE: print(' ast[1] is None:', repr(ast)) + if len(ast) != 3: + raise ParseError(error_code=PARSE_INVALID_EXPRESSION) + parsed = ast[2] + if TRACE_PARSE: print(' parsed = ast[2]:', repr(parsed)) + + else: + # call the function in ast[1] with the rest of the ast as args + parsed = ast[1](*ast[2:]) + if TRACE_PARSE: print(' parsed = ast[1](*ast[2:]):', repr(parsed)) + break + else: + if TRACE_PARSE: print('subex = ast[1](*ast[2:]):', repr(ast)) + subex = ast[1](*ast[2:]) + ast[0].append(subex) + if TRACE_PARSE: print(' ast[0].append(subex):', repr(ast)) + ast = ast[0] + if TRACE_PARSE: print(' ast = ast[0]:', repr(ast)) + except TypeError: + raise ParseError(error_code=PARSE_INVALID_EXPRESSION) + + if simplify: + return parsed.simplify() + + if TRACE_PARSE: print('final parsed:', repr(parsed)) + return parsed + + def _start_operation(self, ast, operation, precedence): + """ + Returns an AST where all operations of lower precedence are finalized. + """ + if TRACE_PARSE: + print(' start_operation:', repr(operation), 'AST:', ast) + + op_prec = precedence[operation] + while True: + if ast[1] is None: + # [None, None, x] + if TRACE_PARSE: print(' start_op: ast[1] is None:', repr(ast)) + ast[1] = operation + if TRACE_PARSE: print(' --> start_op: ast[1] is None:', repr(ast)) + return ast + + prec = precedence[ast[1]] + if prec > op_prec: # op=&, [ast, |, x, y] -> [[ast, |, x], &, y] + if TRACE_PARSE: print(' start_op: prec > op_prec:', repr(ast)) + ast = [ast, operation, ast.pop(-1)] + if TRACE_PARSE: print(' --> start_op: prec > op_prec:', repr(ast)) + return ast + + if prec == op_prec: # op=&, [ast, &, x] -> [ast, &, x] + if TRACE_PARSE: print(' start_op: prec == op_prec:', repr(ast)) + return ast + + if not (inspect.isclass(ast[1]) and issubclass(ast[1], Function)): + # the top ast node should be a function subclass at this stage + raise ParseError(error_code=PARSE_INVALID_NESTING) + + if ast[0] is None: # op=|, [None, &, x, y] -> [None, |, x&y] + if TRACE_PARSE: print(' start_op: ast[0] is None:', repr(ast)) + subexp = ast[1](*ast[2:]) + new_ast = [ast[0], operation, subexp] + if TRACE_PARSE: print(' --> start_op: ast[0] is None:', repr(new_ast)) + return new_ast + + else: # op=|, [[ast, &, x], ~, y] -> [ast, &, x, ~y] + if TRACE_PARSE: print(' start_op: else:', repr(ast)) + ast[0].append(ast[1](*ast[2:])) + ast = ast[0] + if TRACE_PARSE: print(' --> start_op: else:', repr(ast)) + + def tokenize(self, expr): + """ + Return an iterable of 3-tuple describing each token given an expression + unicode string. + + This 3-tuple contains (token, token string, position): + - token: either a Symbol instance or one of TOKEN_* token types. + - token string: the original token unicode string. + - position: some simple object describing the starting position of the + original token string in the `expr` string. It can be an int for a + character offset, or a tuple of starting (row/line, column). + + The token position is used only for error reporting and can be None or + empty. + + Raise ParseError on errors. The ParseError.args is a tuple of: + (token_string, position, error message) + + You can use this tokenizer as a base to create specialized tokenizers + for your custom algebra by subclassing BooleanAlgebra. See also the + tests for other examples of alternative tokenizers. + + This tokenizer has these characteristics: + - The `expr` string can span multiple lines, + - Whitespace is not significant. + - The returned position is the starting character offset of a token. + + - A TOKEN_SYMBOL is returned for valid identifiers which is a string + without spaces. These are valid identifiers: + - Python identifiers. + - a string even if starting with digits + - digits (except for 0 and 1). + - dotted names : foo.bar consist of one token. + - names with colons: foo:bar consist of one token. + These are not identifiers: + - quoted strings. + - any punctuation which is not an operation + + - Recognized operators are (in any upper/lower case combinations): + - for and: '*', '&', 'and' + - for or: '+', '|', 'or' + - for not: '~', '!', 'not' + + - Recognized special symbols are (in any upper/lower case combinations): + - True symbols: 1 and True + - False symbols: 0, False and None + """ + if not isinstance(expr, basestring): + raise TypeError('expr must be string but it is %s.' % type(expr)) + + # mapping of lowercase token strings to a token type id for the standard + # operators, parens and common true or false symbols, as used in the + # default tokenizer implementation. + TOKENS = { + '*': TOKEN_AND, '&': TOKEN_AND, 'and': TOKEN_AND, + '+': TOKEN_OR, '|': TOKEN_OR, 'or': TOKEN_OR, + '~': TOKEN_NOT, '!': TOKEN_NOT, 'not': TOKEN_NOT, + '(': TOKEN_LPAR, ')': TOKEN_RPAR, + '[': TOKEN_LPAR, ']': TOKEN_RPAR, + 'true': TOKEN_TRUE, '1': TOKEN_TRUE, + 'false': TOKEN_FALSE, '0': TOKEN_FALSE, 'none': TOKEN_FALSE + } + + position = 0 + length = len(expr) + + while position < length: + tok = expr[position] + + sym = tok.isalpha() or tok == '_' + if sym: + position += 1 + while position < length: + char = expr[position] + if char.isalnum() or char in self.allowed_in_token: + position += 1 + tok += char + else: + break + position -= 1 + + try: + yield TOKENS[tok.lower()], tok, position + except KeyError: + if sym: + yield TOKEN_SYMBOL, tok, position + elif tok not in (' ', '\t', '\r', '\n'): + raise ParseError(token_string=tok, position=position, + error_code=PARSE_UNKNOWN_TOKEN) + + position += 1 + + # TODO: explain what this means exactly + def _rdistributive(self, expr, op_example): + """ + Recursively flatten the `expr` expression for the `op_example` + AND or OR operation instance exmaple. + """ + if expr.isliteral: + return expr + + expr_class = expr.__class__ + + args = (self._rdistributive(arg, op_example) for arg in expr.args) + args = tuple(arg.simplify() for arg in args) + if len(args) == 1: + return args[0] + + expr = expr_class(*args) + + dualoperation = op_example.dual + if isinstance(expr, dualoperation): + expr = expr.distributive() + return expr + + def normalize(self, expr, operation): + """ + Return a normalized expression transformed to its normal form in the + given AND or OR operation. + + The new expression arguments will satisfy these conditions: + - operation(*args) == expr (here mathematical equality is meant) + - the operation does not occur in any of its arg. + - NOT is only appearing in literals (aka. Negation normal form). + + The operation must be an AND or OR operation or a subclass. + """ + # ensure that the operation is not NOT + assert operation in (self.AND, self.OR,) + # Move NOT inwards. + expr = expr.literalize() + # Simplify first otherwise _rdistributive() may take forever. + expr = expr.simplify() + operation_example = operation(self.TRUE, self.FALSE) + expr = self._rdistributive(expr, operation_example) + # Canonicalize + expr = expr.simplify() + return expr + + def cnf(self, expr): + """ + Return a conjunctive normal form of the `expr` expression. + """ + return self.normalize(expr, self.AND) + + def dnf(self, expr): + """ + Return a disjunctive normal form of the `expr` expression. + """ + return self.normalize(expr, self.OR) + + +class Expression(object): + """ + Abstract base class for all boolean expressions, including functions and + variable symbols. + """ + # Defines sort and comparison order between expressions arguments + sort_order = None + + # Store arguments aka. subterms of this expressions. + # subterms are either literals or expressions. + args = tuple() + + # True is this is a literal expression such as a Symbol, TRUE or FALSE + isliteral = False + + # True if this expression has been simplified to in canonical form. + iscanonical = False + + # these class attributes are configured when a new BooleanAlgebra is created + TRUE = None + FALSE = None + NOT = None + AND = None + OR = None + Symbol = None + + @property + def objects(self): + """ + Return a set of all associated objects with this expression symbols. + Include recursively subexpressions objects. + """ + return set(s.obj for s in self.symbols) + + def get_literals(self): + """ + Return a list of all the literals contained in this expression. + Include recursively subexpressions symbols. + This includes duplicates. + """ + if self.isliteral: + return [self] + if not self.args: + return [] + return list(itertools.chain.from_iterable(arg.get_literals() for arg in self.args)) + + @property + def literals(self): + """ + Return a set of all literals contained in this expression. + Include recursively subexpressions literals. + """ + return set(self.get_literals()) + + def literalize(self): + """ + Return an expression where NOTs are only occurring as literals. + Applied recursively to subexpressions. + """ + if self.isliteral: + return self + args = tuple(arg.literalize() for arg in self.args) + if all(arg is self.args[i] for i, arg in enumerate(args)): + return self + + return self.__class__(*args) + + def get_symbols(self): + """ + Return a list of all the symbols contained in this expression. + Include recursively subexpressions symbols. + This includes duplicates. + """ + return [s if isinstance(s, Symbol) else s.args[0] for s in self.get_literals()] + + @property + def symbols(self,): + """ + Return a list of all the symbols contained in this expression. + Include recursively subexpressions symbols. + This includes duplicates. + """ + return set(self.get_symbols()) + + def subs(self, substitutions, default=None, simplify=False): + """ + Return an expression where the expression or all subterms equal to a key + expression are substituted with the corresponding value expression using + a mapping of: {expr->expr to substitute.} + + Return this expression unmodified if nothing could be substituted. + + Note that this can be used to tested for expression containment. + """ + # shortcut: check if we have our whole expression as a possible + # subsitution source + for expr, substitution in substitutions.items(): + if expr == self: + return substitution + + # otherwise, do a proper substitution of sub expressions + expr = self._subs(substitutions, default, simplify) + return self if expr is None else expr + + def _subs(self, substitutions, default, simplify): + """ + Return an expression where all subterms equal to a key expression are + substituted by the corresponding value expression using a mapping of: + {expr->expr to substitute.} + """ + # track the new list of unchanged args or replaced args through + # a substitution + new_arguments = [] + changed_something = False + + # shortcut for basic logic True or False + if self is self.TRUE or self is self.FALSE: + return self + + # if the expression has no elements, e.g. is empty, do not apply + # substitions + if not self.args: + return default + + # iterate the subexpressions: either plain symbols or a subexpressions + for arg in self.args: + # collect substitutions for exact matches + # break as soon as we have a match + for expr, substitution in substitutions.items(): + if arg == expr: + new_arguments.append(substitution) + changed_something = True + break + + # this will execute only if we did not break out of the + # loop, e.g. if we did not change anything and did not + # collect any substitutions + else: + # recursively call _subs on each arg to see if we get a + # substituted arg + new_arg = arg._subs(substitutions, default, simplify) + if new_arg is None: + # if we did not collect a substitution for this arg, + # keep the arg as-is, it is not replaced by anything + new_arguments.append(arg) + else: + # otherwise, we add the substitution for this arg instead + new_arguments.append(new_arg) + changed_something = True + + if not changed_something: + return + + # here we did some substitution: we return a new expression + # built from the new_arguments + newexpr = self.__class__(*new_arguments) + return newexpr.simplify() if simplify else newexpr + + def simplify(self): + """ + Return a new simplified expression in canonical form built from this + expression. The simplified expression may be exactly the same as this + expression. + + Subclasses override this method to compute actual simplification. + """ + return self + + def __hash__(self): + """ + Expressions are immutable and hashable. The hash of Functions is + computed by respecting the structure of the whole expression by mixing + the class name hash and the recursive hash of a frozenset of arguments. + Hash of elements is based on their boolean equivalent. Hash of symbols + is based on their object. + """ + if not self.args: + arghash = id(self) + else: + arghash = hash(frozenset(map(hash, self.args))) + return hash(self.__class__.__name__) ^ arghash + + def __eq__(self, other): + """ + Test if other element is structurally the same as itself. + + This method does not make any simplification or transformation, so it + will return False although the expression terms may be mathematically + equal. Use simplify() before testing equality. + + For literals, plain equality is used. + For functions, it uses the facts that operations are: + - commutative and considers different ordering as equal. + - idempotent, so args can appear more often in one term than in the other. + """ + if self is other: + return True + + if isinstance(other, self.__class__): + return frozenset(self.args) == frozenset(other.args) + + return NotImplemented + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if self.sort_order is not None and other.sort_order is not None: + if self.sort_order == other.sort_order: + return NotImplemented + return self.sort_order < other.sort_order + return NotImplemented + + def __gt__(self, other): + lt = other.__lt__(self) + if lt is NotImplemented: + return not self.__lt__(other) + return lt + + def __and__(self, other): + return self.AND(self, other) + + __mul__ = __and__ + + def __invert__(self): + return self.NOT(self) + + def __or__(self, other): + return self.OR(self, other) + + __add__ = __or__ + + def __bool__(self): + raise TypeError('Cannot evaluate expression as a Python Boolean.') + + __nonzero__ = __bool__ + + +class BaseElement(Expression): + """ + Abstract base class for the base elements TRUE and FALSE of the boolean + algebra. + """ + sort_order = 0 + + def __init__(self): + super(BaseElement, self).__init__() + self.iscanonical = True + + # The dual Base Element class for this element: TRUE.dual returns + # _FALSE() and FALSE.dual returns _TRUE(). This is a cyclic reference + # and therefore only assigned after creation of the singletons, + self.dual = None + + def __lt__(self, other): + if isinstance(other, BaseElement): + return self == self.FALSE + return NotImplemented + + __nonzero__ = __bool__ = lambda s: None + + def pretty(self, indent=0, debug=False): + """ + Return a pretty formatted representation of self. + """ + return (' ' * indent) + repr(self) + + +class _TRUE(BaseElement): + """ + Boolean base element TRUE. + Not meant to be subclassed nor instantiated directly. + """ + + def __init__(self): + super(_TRUE, self).__init__() + # assigned at singleton creation: self.dual = FALSE + + def __hash__(self): + return hash(True) + + def __eq__(self, other): + return self is other or other is True or isinstance(other, _TRUE) + + def __str__(self): + return '1' + + def __repr__(self): + return 'TRUE' + + __nonzero__ = __bool__ = lambda s: True + + +class _FALSE(BaseElement): + """ + Boolean base element FALSE. + Not meant to be subclassed nor instantiated directly. + """ + + def __init__(self): + super(_FALSE, self).__init__() + # assigned at singleton creation: self.dual = TRUE + + def __hash__(self): + return hash(False) + + def __eq__(self, other): + return self is other or other is False or isinstance(other, _FALSE) + + def __str__(self): + return '0' + + def __repr__(self): + return 'FALSE' + + __nonzero__ = __bool__ = lambda s: False + + +class Symbol(Expression): + """ + Boolean variable. + + A Symbol can hold an object used to determine equality between symbols. + """ + + # FIXME: the statement below in the original docstring is weird: Symbols do + # not have a value assigned, so how could they ever be FALSE + """ + Symbols (also called boolean variables) can only take on the values TRUE or + FALSE. + """ + + sort_order = 5 + + def __init__(self, obj): + super(Symbol, self).__init__() + # Store an associated object. This object determines equality + self.obj = obj + self.iscanonical = True + self.isliteral = True + + def __hash__(self): + if self.obj is None: # Anonymous Symbol. + return id(self) + return hash(self.obj) + + def __eq__(self, other): + if self is other: + return True + if isinstance(other, self.__class__): + return self.obj == other.obj + return NotImplemented + + def __lt__(self, other): + comparator = Expression.__lt__(self, other) + if comparator is not NotImplemented: + return comparator + if isinstance(other, Symbol): + return self.obj < other.obj + return NotImplemented + + def __str__(self): + return str(self.obj) + + def __repr__(self): + obj = "'%s'" % self.obj if isinstance(self.obj, basestring) else repr(self.obj) + return '%s(%s)' % (self.__class__.__name__, obj) + + def pretty(self, indent=0, debug=False): + """ + Return a pretty formatted representation of self. + """ + debug_details = '' + if debug: + debug_details += '' % (self.isliteral, self.iscanonical) + + obj = "'%s'" % self.obj if isinstance(self.obj, basestring) else repr(self.obj) + return (' ' * indent) + ('%s(%s%s)' % (self.__class__.__name__, debug_details, obj)) + + +class Function(Expression): + """ + Boolean function. + + A boolean function takes n (one or more) boolean expressions as arguments + where n is called the order of the function and maps them to one of the base + elements TRUE or FALSE. Implemented functions are AND, OR and NOT. + """ + + def __init__(self, *args): + super(Function, self).__init__() + + # Specifies an infix notation of an operator for printing such as | or &. + self.operator = None + + assert all(isinstance(arg, Expression) for arg in args), \ + 'Bad arguments: all arguments must be an Expression: %r' % (args,) + self.args = tuple(args) + + def __str__(self): + args = self.args + if len(args) == 1: + if self.isliteral: + return '%s%s' % (self.operator, args[0]) + return '%s(%s)' % (self.operator, args[0]) + + args_str = [] + for arg in args: + if arg.isliteral: + args_str.append(str(arg)) + else: + args_str.append('(%s)' % arg) + + return self.operator.join(args_str) + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, ', '.join(map(repr, self.args))) + + def pretty(self, indent=0, debug=False): + """ + Return a pretty formatted representation of self as an indented tree. + + If debug is True, also prints debug information for each expression arg. + + For example: + >>> print Expression().parse(u'not a and not b and not (a and ba and c) and c or c').pretty() + OR( + AND( + NOT(Symbol('a')), + NOT(Symbol('b')), + NOT( + AND( + Symbol('a'), + Symbol('ba'), + Symbol('c') + ) + ), + Symbol('c') + ), + Symbol('c') + ) + """ + debug_details = '' + if debug: + debug_details += '>> class NOT2(NOT): + def __init__(self, *args): + super(NOT2, self).__init__(*args) + self.operator = '!' + """ + + def __init__(self, arg1): + super(NOT, self).__init__(arg1) + self.isliteral = isinstance(self.args[0], Symbol) + self.operator = '~' + + def literalize(self): + """ + Return an expression where NOTs are only occurring as literals. + """ + expr = self.demorgan() + if isinstance(expr, self.__class__): + return expr + return expr.literalize() + + def simplify(self): + """ + Return a simplified expr in canonical form. + + This means double negations are canceled out and all contained boolean + objects are in their canonical form. + """ + if self.iscanonical: + return self + + expr = self.cancel() + if not isinstance(expr, self.__class__): + return expr.simplify() + + if expr.args[0] in (self.TRUE, self.FALSE,): + return expr.args[0].dual + + expr = self.__class__(expr.args[0].simplify()) + expr.iscanonical = True + return expr + + def cancel(self): + """ + Cancel itself and following NOTs as far as possible. + Returns the simplified expression. + """ + expr = self + while True: + arg = expr.args[0] + if not isinstance(arg, self.__class__): + return expr + expr = arg.args[0] + if not isinstance(expr, self.__class__): + return expr + + def demorgan(self): + """ + Return a expr where the NOT function is moved inward. + This is achieved by canceling double NOTs and using De Morgan laws. + """ + expr = self.cancel() + if expr.isliteral or not isinstance(expr, self.NOT): + return expr + op = expr.args[0] + return op.dual(*(self.__class__(arg).cancel() for arg in op.args)) + + def __lt__(self, other): + return self.args[0] < other + + def pretty(self, indent=1, debug=False): + """ + Return a pretty formatted representation of self. + Include additional debug details if `debug` is True. + """ + debug_details = '' + if debug: + debug_details += '' % (self.isliteral, self.iscanonical) + if self.isliteral: + pretty_literal = self.args[0].pretty(indent=0, debug=debug) + return (' ' * indent) + '%s(%s%s)' % (self.__class__.__name__, debug_details, pretty_literal) + else: + return super(NOT, self).pretty(indent=indent, debug=debug) + + +class DualBase(Function): + """ + Base class for AND and OR function. + + This class uses the duality principle to combine similar methods of AND + and OR. Both operations take 2 or more arguments and can be created using + "|" for OR and "&" for AND. + """ + + def __init__(self, arg1, arg2, *args): + super(DualBase, self).__init__(arg1, arg2, *args) + + # identity element for the specific operation. + # This will be TRUE for the AND operation and FALSE for the OR operation. + self.identity = None + + # annihilator element for this function. + # This will be FALSE for the AND operation and TRUE for the OR operation. + self.annihilator = None + + # dual class of this function. + # This means OR.dual returns AND and AND.dual returns OR. + self.dual = None + + def __contains__(self, expr): + """ + Test if expr is a subterm of this expression. + """ + if expr in self.args: + return True + + if isinstance(expr, self.__class__): + return all(arg in self.args for arg in expr.args) + + def simplify(self, sort=True): + """ + Return a new simplified expression in canonical form from this + expression. + + For simplification of AND and OR fthe ollowing rules are used + recursively bottom up: + - Associativity (output does not contain same operations nested) + - Annihilation + - Idempotence + - Identity + - Complementation + - Elimination + - Absorption + - Commutativity (output is always sorted) + + Other boolean objects are also in their canonical form. + """ + # TODO: Refactor DualBase.simplify into different "sub-evals". + + # If self is already canonical do nothing. + if self.iscanonical: + return self + + # Otherwise bring arguments into canonical form. + args = [arg.simplify() for arg in self.args] + + # Create new instance of own class with canonical args. + # TODO: Only create new class if some args changed. + expr = self.__class__(*args) + + # Literalize before doing anything, this also applies De Morgan's Law + expr = expr.literalize() + + # Associativity: + # (A & B) & C = A & (B & C) = A & B & C + # (A | B) | C = A | (B | C) = A | B | C + expr = expr.flatten() + + # Annihilation: A & 0 = 0, A | 1 = 1 + if self.annihilator in expr.args: + return self.annihilator + + # Idempotence: A & A = A, A | A = A + # this boils down to removing duplicates + args = [] + for arg in expr.args: + if arg not in args: + args.append(arg) + if len(args) == 1: + return args[0] + + # Identity: A & 1 = A, A | 0 = A + if self.identity in args: + args.remove(self.identity) + if len(args) == 1: + return args[0] + + # Complementation: A & ~A = 0, A | ~A = 1 + for arg in args: + if self.NOT(arg) in args: + return self.annihilator + + # Elimination: (A & B) | (A & ~B) = A, (A | B) & (A | ~B) = A + i = 0 + while i < len(args) - 1: + j = i + 1 + ai = args[i] + if not isinstance(ai, self.dual): + i += 1 + continue + while j < len(args): + aj = args[j] + if not isinstance(aj, self.dual) or len(ai.args) != len(aj.args): + j += 1 + continue + + # Find terms where only one arg is different. + negated = None + for arg in ai.args: + # FIXME: what does this pass Do? + if arg in aj.args: + pass + elif self.NOT(arg).cancel() in aj.args: + if negated is None: + negated = arg + else: + negated = None + break + else: + negated = None + break + + # If the different arg is a negation simplify the expr. + if negated is not None: + # Cancel out one of the two terms. + del args[j] + aiargs = list(ai.args) + aiargs.remove(negated) + if len(aiargs) == 1: + args[i] = aiargs[0] + else: + args[i] = self.dual(*aiargs) + + if len(args) == 1: + return args[0] + else: + # Now the other simplifications have to be redone. + return self.__class__(*args).simplify() + j += 1 + i += 1 + + # Absorption: A & (A | B) = A, A | (A & B) = A + # Negative absorption: A & (~A | B) = A & B, A | (~A & B) = A | B + args = self.absorb(args) + if len(args) == 1: + return args[0] + + # Commutativity: A & B = B & A, A | B = B | A + if sort: + args.sort() + + # Create new (now canonical) expression. + expr = self.__class__(*args) + expr.iscanonical = True + return expr + + def flatten(self): + """ + Return a new expression where nested terms of this expression are + flattened as far as possible. + + E.g. A & (B & C) becomes A & B & C. + """ + args = list(self.args) + i = 0 + for arg in self.args: + if isinstance(arg, self.__class__): + args[i:i + 1] = arg.args + i += len(arg.args) + else: + i += 1 + + return self.__class__(*args) + + def absorb(self, args): + """ + Given an `args` sequence of expressions, return a new list of expression + applying absorption and negative absorption. + + See https://en.wikipedia.org/wiki/Absorption_law + + Absorption: A & (A | B) = A, A | (A & B) = A + Negative absorption: A & (~A | B) = A & B, A | (~A & B) = A | B + """ + args = list(args) + if not args: + args = list(self.args) + i = 0 + while i < len(args): + absorber = args[i] + j = 0 + while j < len(args): + if j == i: + j += 1 + continue + target = args[j] + if not isinstance(target, self.dual): + j += 1 + continue + + # Absorption + if absorber in target: + del args[j] + if j < i: + i -= 1 + continue + + # Negative absorption + neg_absorber = self.NOT(absorber).cancel() + if neg_absorber in target: + b = target.subtract(neg_absorber, simplify=False) + if b is None: + del args[j] + if j < i: + i -= 1 + continue + else: + args[j] = b + j += 1 + continue + + if isinstance(absorber, self.dual): + remove = None + for arg in absorber.args: + narg = self.NOT(arg).cancel() + if arg in target.args: + pass + elif narg in target.args: + if remove is None: + remove = narg + else: + remove = None + break + else: + remove = None + break + if remove is not None: + args[j] = target.subtract(remove, simplify=True) + j += 1 + i += 1 + + return args + + def subtract(self, expr, simplify): + """ + Return a new expression where the `expr` expression has been removed + from this expression if it exists. + """ + args = self.args + if expr in self.args: + args = list(self.args) + args.remove(expr) + elif isinstance(expr, self.__class__): + if all(arg in self.args for arg in expr.args): + args = tuple(arg for arg in self.args if arg not in expr) + if len(args) == 0: + return None + if len(args) == 1: + return args[0] + + newexpr = self.__class__(*args) + if simplify: + newexpr = newexpr.simplify() + return newexpr + + def distributive(self): + """ + Return a term where the leading AND or OR terms are switched. + + This is done by applying the distributive laws: + A & (B|C) = (A&B) | (A&C) + A | (B&C) = (A|B) & (A|C) + """ + dual = self.dual + args = list(self.args) + for i, arg in enumerate(args): + if isinstance(arg, dual): + args[i] = arg.args + else: + args[i] = (arg,) + + prod = itertools.product(*args) + args = tuple(self.__class__(*arg).simplify() for arg in prod) + + if len(args) == 1: + return args[0] + else: + return dual(*args) + + def __lt__(self, other): + comparator = Expression.__lt__(self, other) + if comparator is not NotImplemented: + return comparator + + if isinstance(other, self.__class__): + lenself = len(self.args) + lenother = len(other.args) + for i in range(min(lenself, lenother)): + if self.args[i] == other.args[i]: + continue + + comparator = self.args[i] < other.args[i] + if comparator is not NotImplemented: + return comparator + + if lenself != lenother: + return lenself < lenother + return NotImplemented + + +class AND(DualBase): + """ + Boolean AND operation, taking 2 or more arguments. + + It can also be created by using "&" between two boolean expressions. + + You can subclass to define alternative string representation. + For example:: + >>> class AND2(AND): + def __init__(self, *args): + super(AND2, self).__init__(*args) + self.operator = 'AND' + """ + + sort_order = 10 + + def __init__(self, arg1, arg2, *args): + super(AND, self).__init__(arg1, arg2, *args) + self.identity = self.TRUE + self.annihilator = self.FALSE + self.dual = self.OR + self.operator = '&' + + +class OR(DualBase): + """ + Boolean OR operation, taking 2 or more arguments + + It can also be created by using "|" between two boolean expressions. + + You can subclass to define alternative string representation. + For example:: + >>> class OR2(OR): + def __init__(self, *args): + super(OR2, self).__init__(*args) + self.operator = 'OR' + """ + + sort_order = 25 + + def __init__(self, arg1, arg2, *args): + super(OR, self).__init__(arg1, arg2, *args) + self.identity = self.FALSE + self.annihilator = self.TRUE + self.dual = self.AND + self.operator = '|' diff --git a/pex/vendor/_vendored/boolean/boolean/test_boolean.py b/pex/vendor/_vendored/boolean/boolean/test_boolean.py new file mode 100644 index 000000000..654e4ad07 --- /dev/null +++ b/pex/vendor/_vendored/boolean/boolean/test_boolean.py @@ -0,0 +1,1278 @@ +""" +Boolean Algebra. + +Tests + +Copyright (c) 2009-2017 Sebastian Kraemer, basti.kr@gmail.com and others +Released under revised BSD license. +""" + +from __future__ import absolute_import +from __future__ import unicode_literals +from __future__ import print_function +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import PARSE_UNKNOWN_TOKEN # vendor:skip +else: + from pex.third_party.boolean.boolean import PARSE_UNKNOWN_TOKEN + + +# Python 2 and 3 +try: + basestring # NOQA +except NameError: + basestring = str # NOQA + +import unittest +from unittest.case import expectedFailure + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import BooleanAlgebra # vendor:skip +else: + from pex.third_party.boolean import BooleanAlgebra + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import ParseError # vendor:skip +else: + from pex.third_party.boolean import ParseError + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import Symbol # vendor:skip +else: + from pex.third_party.boolean import Symbol + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import TOKEN_NOT # vendor:skip +else: + from pex.third_party.boolean import TOKEN_NOT + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import TOKEN_AND # vendor:skip +else: + from pex.third_party.boolean import TOKEN_AND + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import TOKEN_OR # vendor:skip +else: + from pex.third_party.boolean import TOKEN_OR + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import TOKEN_TRUE # vendor:skip +else: + from pex.third_party.boolean import TOKEN_TRUE + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import TOKEN_FALSE # vendor:skip +else: + from pex.third_party.boolean import TOKEN_FALSE + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import TOKEN_SYMBOL # vendor:skip +else: + from pex.third_party.boolean import TOKEN_SYMBOL + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import TOKEN_LPAR # vendor:skip +else: + from pex.third_party.boolean import TOKEN_LPAR + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean import TOKEN_RPAR # vendor:skip +else: + from pex.third_party.boolean import TOKEN_RPAR + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import PARSE_INVALID_SYMBOL_SEQUENCE # vendor:skip +else: + from pex.third_party.boolean.boolean import PARSE_INVALID_SYMBOL_SEQUENCE + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import PARSE_INVALID_EXPRESSION # vendor:skip +else: + from pex.third_party.boolean.boolean import PARSE_INVALID_EXPRESSION + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import PARSE_INVALID_NESTING # vendor:skip +else: + from pex.third_party.boolean.boolean import PARSE_INVALID_NESTING + +if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import PARSE_INVALID_OPERATOR_SEQUENCE # vendor:skip +else: + from pex.third_party.boolean.boolean import PARSE_INVALID_OPERATOR_SEQUENCE + + + +class BooleanAlgebraTestCase(unittest.TestCase): + + def test_creation(self): + algebra = BooleanAlgebra() + expr_str = '(a|b|c)&d&(~e|(f&g))' + expr = algebra.parse(expr_str) + self.assertEqual(expr_str, str(expr)) + + def test_parse_with_mixed_operators_multilines_and_custom_symbol(self): + + class MySymbol(Symbol): + pass + + expr_str = '''(a or ~ b +_c ) and + d & ( ! e_ + | (my * g OR 1 or 0) ) AND that ''' + + algebra = BooleanAlgebra(Symbol_class=MySymbol) + expr = algebra.parse(expr_str) + + expected = algebra.AND( + algebra.OR( + algebra.Symbol('a'), + algebra.NOT(algebra.Symbol('b')), + algebra.Symbol('_c'), + ), + algebra.Symbol('d'), + algebra.OR( + algebra.NOT(algebra.Symbol('e_')), + algebra.OR( + algebra.AND( + algebra.Symbol('my'), + algebra.Symbol('g'), + ), + algebra.TRUE, + algebra.FALSE, + ), + ), + algebra.Symbol('that'), + ) + + self.assertEqual(expected.pretty(), expr.pretty()) + self.assertEqual(expected, expr) + + def test_parse_recognizes_trueish_and_falsish_symbol_tokens(self): + expr_str = 'True or False or None or 0 or 1 or TRue or FalSE or NONe' + algebra = BooleanAlgebra() + expr = algebra.parse(expr_str) + expected = algebra.OR( + algebra.TRUE, + algebra.FALSE, + algebra.FALSE, + algebra.FALSE, + algebra.TRUE, + algebra.TRUE, + algebra.FALSE, + algebra.FALSE, + ) + self.assertEqual(expected, expr) + + def test_parse_can_use_iterable_from_alternative_tokenizer(self): + + class CustomSymbol(Symbol): + pass + + class CustomAlgebra(BooleanAlgebra): + def __init__(self, Symbol_class=CustomSymbol): + super(CustomAlgebra, self).__init__(Symbol_class=CustomSymbol) + + def tokenize(self, s): + "Sample tokenizer using custom operators and symbols" + ops = { + 'WHY_NOT': TOKEN_OR, + 'ALSO': TOKEN_AND, + 'NEITHER': TOKEN_NOT, + '(': TOKEN_LPAR, + ')': TOKEN_RPAR, + } + + for row, line in enumerate(s.splitlines(False)): + for col, tok in enumerate(line.split()): + if tok in ops: + yield ops[tok], tok, (row, col) + elif tok == 'Custom': + yield self.Symbol(tok), tok, (row, col) + else: + yield TOKEN_SYMBOL, tok, (row, col) + + expr_str = '''( Custom WHY_NOT regular ) ALSO NEITHER ( + not_custom ALSO standard ) + ''' + + algebra = CustomAlgebra() + expr = algebra.parse(expr_str) + expected = algebra.AND( + algebra.OR( + algebra.Symbol('Custom'), + algebra.Symbol('regular'), + ), + algebra.NOT( + algebra.AND( + algebra.Symbol('not_custom'), + algebra.Symbol('standard'), + ), + ), + ) + self.assertEqual(expected, expr) + + def test_parse_with_advanced_tokenizer_example(self): + import tokenize + + try: + from io import StringIO + except ImportError: + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO + + + class PlainVar(Symbol): + "Plain boolean variable" + + class ColonDotVar(Symbol): + "Colon and dot-separated string boolean variable" + + class AdvancedAlgebra(BooleanAlgebra): + def tokenize(self, expr): + """ + Example custom tokenizer derived from the standard Python tokenizer + with a few extra features: #-style comments are supported and a + colon- and dot-separated string is recognized and stored in custom + symbols. In contrast with the standard tokenizer, only these + boolean operators are recognized : & | ! and or not. + + For more advanced tokenization you could also consider forking the + `tokenize` standard library module. + """ + + if not isinstance(expr, basestring): + raise TypeError('expr must be string but it is %s.' % type(expr)) + + # mapping of lowercase token strings to a token object instance for + # standard operators, parens and common true or false symbols + TOKENS = { + '&': TOKEN_AND, + 'and': TOKEN_AND, + '|': TOKEN_OR, + 'or': TOKEN_OR, + '!': TOKEN_NOT, + 'not': TOKEN_NOT, + '(': TOKEN_LPAR, + ')': TOKEN_RPAR, + 'true': TOKEN_TRUE, + '1': TOKEN_TRUE, + 'false': TOKEN_FALSE, + '0': TOKEN_FALSE, + 'none': TOKEN_FALSE, + } + + ignored_token_types = ( + tokenize.NL, tokenize.NEWLINE, tokenize.COMMENT, + tokenize.INDENT, tokenize.DEDENT, + tokenize.ENDMARKER + ) + + # note: an unbalanced expression may raise a TokenError here. + tokens = ((toktype, tok, row, col,) for toktype, tok, (row, col,), _, _ + in tokenize.generate_tokens(StringIO(expr).readline) + if tok and tok.strip()) + + COLON_DOT = (':', '.',) + + def build_symbol(current_dotted): + if current_dotted: + if any(s in current_dotted for s in COLON_DOT): + sym = ColonDotVar(current_dotted) + else: + sym = PlainVar(current_dotted) + return sym + + # accumulator for dotted symbols that span several `tokenize` tokens + dotted, srow, scol = '', None, None + + for toktype, tok, row, col in tokens: + if toktype in ignored_token_types: + # we reached a break point and should yield the current dotted + symbol = build_symbol(dotted) + if symbol is not None: + yield symbol, dotted, (srow, scol) + dotted, srow, scol = '', None, None + + continue + + std_token = TOKENS.get(tok.lower()) + if std_token is not None: + # we reached a break point and should yield the current dotted + symbol = build_symbol(dotted) + if symbol is not None: + yield symbol, dotted, (srow, scol) + dotted, srow, scol = '', 0, 0 + + yield std_token, tok, (row, col) + + continue + + if toktype == tokenize.NAME or (toktype == tokenize.OP and tok in COLON_DOT): + if not dotted: + srow = row + scol = col + dotted += tok + + else: + raise TypeError('Unknown token: %(tok)r at line: %(row)r, column: %(col)r' % locals()) + + test_expr = ''' + (colon1:dot1.dot2 or colon2_name:col_on3:do_t1.do_t2.do_t3 ) + and + ( plain_symbol & !Custom ) + ''' + + algebra = AdvancedAlgebra() + expr = algebra.parse(test_expr) + expected = algebra.AND( + algebra.OR( + ColonDotVar('colon1:dot1.dot2'), + ColonDotVar('colon2_name:col_on3:do_t1.do_t2.do_t3') + ), + algebra.AND( + PlainVar('plain_symbol'), + algebra.NOT(PlainVar('Custom')) + ) + ) + self.assertEqual(expected, expr) + + def test_allowing_additional_characters_in_tokens(self): + algebra = BooleanAlgebra(allowed_in_token=('.', '_', '-', '+')) + test_expr = 'l-a AND b+c' + + expr = algebra.parse(test_expr) + expected = algebra.AND( + algebra.Symbol('l-a'), + algebra.Symbol('b+c') + ) + self.assertEqual(expected, expr) + + def test_parse_raise_ParseError1(self): + algebra = BooleanAlgebra() + expr = 'l-a AND none' + + try: + algebra.parse(expr) + self.fail("Exception should be raised when parsing '%s'" % expr) + except ParseError as pe: + assert pe.error_code == PARSE_UNKNOWN_TOKEN + + def test_parse_raise_ParseError2(self): + algebra = BooleanAlgebra() + expr = '(l-a + AND l-b' + try: + algebra.parse(expr) + self.fail("Exception should be raised when parsing '%s'" % expr) + except ParseError as pe: + assert pe.error_code == PARSE_UNKNOWN_TOKEN + + def test_parse_raise_ParseError3(self): + algebra = BooleanAlgebra() + expr = '(l-a + AND l-b)' + try: + algebra.parse(expr) + self.fail("Exception should be raised when parsing '%s'" % expr) + except ParseError as pe: + assert pe.error_code == PARSE_UNKNOWN_TOKEN + + def test_parse_raise_ParseError4(self): + algebra = BooleanAlgebra() + expr = '(l-a AND l-b' + try: + algebra.parse(expr) + self.fail("Exception should be raised when parsing '%s'" % expr) + except ParseError as pe: + assert pe.error_code == PARSE_UNKNOWN_TOKEN + + def test_parse_raise_ParseError5(self): + algebra = BooleanAlgebra() + expr = '(l-a + AND l-b))' + try: + algebra.parse(expr) + self.fail("Exception should be raised when parsing '%s'" % expr) + except ParseError as pe: + assert pe.error_code == PARSE_UNKNOWN_TOKEN + + def test_parse_raise_ParseError6(self): + algebra = BooleanAlgebra() + expr = '(l-a AND l-b))' + try: + algebra.parse(expr) + self.fail("Exception should be raised when parsing '%s'" % expr) + except ParseError as pe: + assert pe.error_code == PARSE_UNKNOWN_TOKEN + + def test_parse_raise_ParseError7(self): + algebra = BooleanAlgebra() + expr = 'l-a AND' + try: + algebra.parse(expr) + self.fail("Exception should be raised when parsing '%s'" % expr) + except ParseError as pe: + assert pe.error_code == PARSE_UNKNOWN_TOKEN + + def test_parse_raise_ParseError8(self): + algebra = BooleanAlgebra() + expr = 'OR l-a' + try: + algebra.parse(expr) + self.fail("Exception should be raised when parsing '%s'" % expr) + except ParseError as pe: + assert pe.error_code == PARSE_INVALID_OPERATOR_SEQUENCE + + def test_parse_raise_ParseError9(self): + algebra = BooleanAlgebra() + expr = '+ l-a' + try: + algebra.parse(expr) + self.fail("Exception should be raised when parsing '%s'" % expr) + except ParseError as pe: + assert pe.error_code == PARSE_INVALID_OPERATOR_SEQUENCE + + def test_parse_side_by_side_symbols_should_raise_exception_but_not(self): + algebra = BooleanAlgebra() + expr_str = 'a or b c' + try: + algebra.parse(expr_str) + except ParseError as pe: + assert pe.error_code == PARSE_INVALID_SYMBOL_SEQUENCE + + def test_parse_side_by_side_symbols_should_raise_exception_but_not2(self): + algebra = BooleanAlgebra() + expr_str = '(a or b) c' + try: + algebra.parse(expr_str) + except ParseError as pe: + assert pe.error_code == PARSE_INVALID_EXPRESSION + + def test_parse_side_by_side_symbols_raise_exception(self): + algebra = BooleanAlgebra() + expr_str = 'a b' + try: + algebra.parse(expr_str) + except ParseError as pe: + assert pe.error_code == PARSE_INVALID_SYMBOL_SEQUENCE + + def test_parse_side_by_side_symbols_with_parens_raise_exception(self): + algebra = BooleanAlgebra() + expr_str = '(a) (b)' + try: + algebra.parse(expr_str) + except ParseError as pe: + assert pe.error_code == PARSE_INVALID_NESTING + +class BaseElementTestCase(unittest.TestCase): + + def test_creation(self): + if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import BaseElement # vendor:skip + else: + from pex.third_party.boolean.boolean import BaseElement + + algebra = BooleanAlgebra() + self.assertEqual(algebra.TRUE, algebra.TRUE) + BaseElement() + self.assertRaises(TypeError, BaseElement, 2) + self.assertRaises(TypeError, BaseElement, 'a') + self.assertTrue(algebra.TRUE is algebra.TRUE) + self.assertTrue(algebra.TRUE is not algebra.FALSE) + self.assertTrue(algebra.FALSE is algebra.FALSE) + self.assertTrue(bool(algebra.TRUE) is True) + self.assertTrue(bool(algebra.FALSE) is False) + self.assertEqual(algebra.TRUE, True) + self.assertEqual(algebra.FALSE, False) + + def test_literals(self): + algebra = BooleanAlgebra() + self.assertEqual(algebra.TRUE.literals, set()) + self.assertEqual(algebra.FALSE.literals, set()) + + def test_literalize(self): + algebra = BooleanAlgebra() + self.assertEqual(algebra.TRUE.literalize(), algebra.TRUE) + self.assertEqual(algebra.FALSE.literalize(), algebra.FALSE) + + def test_simplify(self): + algebra = BooleanAlgebra() + self.assertEqual(algebra.TRUE.simplify(), algebra.TRUE) + self.assertEqual(algebra.FALSE.simplify(), algebra.FALSE) + + def test_simplify_two_algebra(self): + algebra1 = BooleanAlgebra() + algebra2 = BooleanAlgebra() + self.assertEqual(algebra1.TRUE.simplify(), algebra2.TRUE) + self.assertEqual(algebra1.FALSE.simplify(), algebra2.FALSE) + + def test_dual(self): + algebra = BooleanAlgebra() + self.assertEqual(algebra.TRUE.dual, algebra.FALSE) + self.assertEqual(algebra.FALSE.dual, algebra.TRUE) + + def test_equality(self): + algebra = BooleanAlgebra() + self.assertEqual(algebra.TRUE, algebra.TRUE) + self.assertEqual(algebra.FALSE, algebra.FALSE) + self.assertNotEqual(algebra.TRUE, algebra.FALSE) + + def test_order(self): + algebra = BooleanAlgebra() + self.assertTrue(algebra.FALSE < algebra.TRUE) + self.assertTrue(algebra.TRUE > algebra.FALSE) + + def test_printing(self): + algebra = BooleanAlgebra() + self.assertEqual(str(algebra.TRUE), '1') + self.assertEqual(str(algebra.FALSE), '0') + self.assertEqual(repr(algebra.TRUE), 'TRUE') + self.assertEqual(repr(algebra.FALSE), 'FALSE') + + +class SymbolTestCase(unittest.TestCase): + + def test_init(self): + Symbol(1) + Symbol('a') + Symbol(None) + Symbol(sum) + Symbol((1, 2, 3)) + Symbol([1, 2]) + + def test_isliteral(self): + self.assertTrue(Symbol(1).isliteral is True) + + def test_literals(self): + l1 = Symbol(1) + l2 = Symbol(1) + self.assertTrue(l1 in l1.literals) + self.assertTrue(l1 in l2.literals) + self.assertTrue(l2 in l1.literals) + self.assertTrue(l2 in l2.literals) + self.assertRaises(AttributeError, setattr, l1, 'literals', 1) + + def test_literalize(self): + s = Symbol(1) + self.assertEqual(s.literalize(), s) + + def test_simplify(self): + s = Symbol(1) + self.assertEqual(s.simplify(), s) + + def test_simplify_different_instances(self): + s1 = Symbol(1) + s2 = Symbol(1) + self.assertEqual(s1.simplify(), s2.simplify()) + + def test_equal_symbols(self): + algebra = BooleanAlgebra() + a = algebra.Symbol('a') + a2 = algebra.Symbol('a') + + c = algebra.Symbol('b') + d = algebra.Symbol('d') + e = algebra.Symbol('e') + + # Test __eq__. + self.assertTrue(a == a) + self.assertTrue(a == a2) + self.assertFalse(a == c) + self.assertFalse(a2 == c) + self.assertTrue(d == d) + self.assertFalse(d == e) + self.assertFalse(a == d) + # Test __ne__. + self.assertFalse(a != a) + self.assertFalse(a != a2) + self.assertTrue(a != c) + self.assertTrue(a2 != c) + + def test_order(self): + S = Symbol + self.assertTrue(S('x') < S('y')) + self.assertTrue(S('y') > S('x')) + self.assertTrue(S(1) < S(2)) + self.assertTrue(S(2) > S(1)) + + def test_printing(self): + self.assertEqual('a', str(Symbol('a'))) + self.assertEqual('1', str(Symbol(1))) + self.assertEqual("Symbol('a')", repr(Symbol('a'))) + self.assertEqual('Symbol(1)', repr(Symbol(1))) + + +class NOTTestCase(unittest.TestCase): + + def test_init(self): + algebra = BooleanAlgebra() + self.assertRaises(TypeError, algebra.NOT) + self.assertRaises(TypeError, algebra.NOT, 'a', 'b') + algebra.NOT(algebra.Symbol('a')) + self.assertEqual(algebra.FALSE, (algebra.NOT(algebra.TRUE)).simplify()) + self.assertEqual(algebra.TRUE, (algebra.NOT(algebra.FALSE)).simplify()) + + def test_isliteral(self): + algebra = BooleanAlgebra() + s = algebra.Symbol(1) + self.assertTrue(algebra.NOT(s).isliteral) + self.assertFalse(algebra.parse('~(a|b)').isliteral) + + def test_literals(self): + algebra = BooleanAlgebra() + a = algebra.Symbol('a') + l = ~a + self.assertTrue(l.isliteral) + self.assertTrue(l in l.literals) + self.assertEqual(len(l.literals), 1) + + l = algebra.parse('~(a&a)') + self.assertFalse(l.isliteral) + self.assertTrue(a in l.literals) + self.assertEqual(len(l.literals), 1) + + l = algebra.parse('~(a&a)', simplify=True) + self.assertTrue(l.isliteral) + + def test_literalize(self): + parse = BooleanAlgebra().parse + self.assertEqual(parse('~a').literalize(), parse('~a')) + self.assertEqual(parse('~(a&b)').literalize(), parse('~a|~b')) + self.assertEqual(parse('~(a|b)').literalize(), parse('~a&~b')) + + def test_simplify(self): + algebra = BooleanAlgebra() + a = algebra.Symbol('a') + self.assertEqual(~a, ~a) + assert algebra.Symbol('a') == algebra.Symbol('a') + self.assertNotEqual(a, algebra.parse('~~a')) + self.assertEqual(a, (~~a).simplify()) + self.assertEqual(~a, (~~ ~a).simplify()) + self.assertEqual(a, (~~ ~~a).simplify()) + self.assertEqual((~(a & a & a)).simplify(), (~(a & a & a)).simplify()) + self.assertEqual(a, algebra.parse('~~a', simplify=True)) + algebra2 = BooleanAlgebra() + self.assertEqual(a, algebra2.parse('~~a', simplify=True)) + + def test_cancel(self): + algebra = BooleanAlgebra() + a = algebra.Symbol('a') + self.assertEqual(~a, (~a).cancel()) + self.assertEqual(a, algebra.parse('~~a').cancel()) + self.assertEqual(~a, algebra.parse('~~~a').cancel()) + self.assertEqual(a, algebra.parse('~~~~a').cancel()) + + def test_demorgan(self): + algebra = BooleanAlgebra() + a = algebra.Symbol('a') + b = algebra.Symbol('b') + c = algebra.Symbol('c') + self.assertEqual(algebra.parse('~(a&b)').demorgan(), ~a | ~b) + self.assertEqual(algebra.parse('~(a|b|c)').demorgan(), algebra.parse('~a&~b&~c')) + self.assertEqual(algebra.parse('~(~a&b)').demorgan(), a | ~b) + self.assertEqual((~~(a&b|c)).demorgan(), a&b|c) + self.assertEqual((~~~(a&b|c)).demorgan(), ~(a&b)&~c) + self.assertEqual(algebra.parse('~'*10 + '(a&b|c)').demorgan(), a&b|c) + self.assertEqual(algebra.parse('~'*11 + '(a&b|c)').demorgan(), (~(a&b|c)).demorgan()) + + def test_order(self): + algebra = BooleanAlgebra() + x = algebra.Symbol(1) + y = algebra.Symbol(2) + self.assertTrue(x < ~x) + self.assertTrue(~x > x) + self.assertTrue(~x < y) + self.assertTrue(y > ~x) + + def test_printing(self): + algebra = BooleanAlgebra() + a = algebra.Symbol('a') + self.assertEqual(str(~a), '~a') + self.assertEqual(repr(~a), "NOT(Symbol('a'))") + expr = algebra.parse('~(a&a)') + self.assertEqual(str(expr), '~(a&a)') + self.assertEqual(repr(expr), "NOT(AND(Symbol('a'), Symbol('a')))") + + +class DualBaseTestCase(unittest.TestCase): + + maxDiff = None + + def test_init(self): + if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import DualBase # vendor:skip + else: + from pex.third_party.boolean.boolean import DualBase + + a, b, c = Symbol('a'), Symbol('b'), Symbol('c') + t1 = DualBase(a, b) + t2 = DualBase(a, b, c) + t3 = DualBase(a, a) + t4 = DualBase(a, b, c) + + self.assertRaises(TypeError, DualBase) + for term in (t1, t2, t3, t4): + self.assertTrue(isinstance(term, DualBase)) + + def test_isliteral(self): + if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import DualBase # vendor:skip + else: + from pex.third_party.boolean.boolean import DualBase + + a, b, c = Symbol('a'), Symbol('b'), Symbol('c') + t1 = DualBase(a, b) + t2 = DualBase(a, b, c) + + self.assertFalse(t1.isliteral) + self.assertFalse(t2.isliteral) + + def test_literals(self): + if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import DualBase # vendor:skip + else: + from pex.third_party.boolean.boolean import DualBase + + a, b, c = Symbol('a'), Symbol('b'), Symbol('c') + t1 = DualBase(a, b) + t2 = DualBase(a, b, c) + t3 = DualBase(a, a) + t4 = DualBase(a, b, c) + + for term in (t1, t2, t3, t4): + self.assertTrue(a in term.literals) + for term in (t1, t2, t4): + self.assertTrue(b in term.literals) + for term in (t2, t4): + self.assertTrue(c in term.literals) + + def test_literalize(self): + parse = BooleanAlgebra().parse + self.assertEqual(parse('a|~(b|c)').literalize(), parse('a|(~b&~c)')) + + def test_annihilator(self): + algebra = BooleanAlgebra() + self.assertEqual(algebra.parse('a&a').annihilator, algebra.FALSE) + self.assertEqual(algebra.parse('a|a').annihilator, algebra.TRUE) + + def test_identity(self): + algebra = BooleanAlgebra() + self.assertEqual(algebra.parse('a|b').identity, algebra.FALSE) + self.assertEqual(algebra.parse('a&b').identity, algebra.TRUE) + + def test_dual(self): + algebra = BooleanAlgebra() + self.assertEqual(algebra.AND(algebra.Symbol('a'), algebra.Symbol('b')).dual, algebra.OR) + self.assertEqual(algebra.OR(algebra.Symbol('a'), algebra.Symbol('b')).dual, algebra.AND) + + self.assertEqual(algebra.parse('a|b').dual, algebra.AND) + self.assertEqual(algebra.parse('a&b').dual, algebra.OR) + + def test_simplify(self): + algebra1 = BooleanAlgebra() + algebra2 = BooleanAlgebra() + a = algebra1.Symbol('a') + b = algebra1.Symbol('b') + c = algebra1.Symbol('c') + + _0 = algebra1.FALSE + _1 = algebra1.TRUE + # Idempotence + self.assertEqual(a, (a & a).simplify()) + # Idempotence + Associativity + self.assertEqual(a | b, (a | (a | b)).simplify()) + # Annihilation + self.assertEqual(_0, (a & _0).simplify()) + self.assertEqual(_1, (a | _1).simplify()) + # Identity + self.assertEqual(a, (a & _1).simplify()) + self.assertEqual(a, (a | _0).simplify()) + # Complementation + self.assertEqual(_0, (a & ~a).simplify()) + self.assertEqual(_1, (a | ~a).simplify()) + # Absorption + self.assertEqual(a, (a & (a | b)).simplify()) + self.assertEqual(a, (a | (a & b)).simplify()) + self.assertEqual(b & a, ((b & a) | (b & a & c)).simplify()) + + # Elimination + self.assertEqual(a, ((a & ~b) | (a & b)).simplify()) + + # Commutativity + Non-Commutativity + sorted_expression = (b & b & a).simplify() + unsorted_expression = (b & b & a).simplify(sort=False) + self.assertEqual(sorted_expression, unsorted_expression) + self.assertNotEqual(sorted_expression.pretty(), unsorted_expression.pretty()) + + sorted_expression = (b | b | a).simplify() + unsorted_expression = (b | b | a).simplify(sort=False) + self.assertEqual(sorted_expression, unsorted_expression) + self.assertNotEqual(sorted_expression.pretty(), unsorted_expression.pretty()) + + expected = algebra1.parse('(a&b)|(b&c)|(a&c)') + result = algebra1.parse('(~a&b&c) | (a&~b&c) | (a&b&~c) | (a&b&c)', simplify=True) + self.assertEqual(expected, result) + + expected = algebra1.parse('(a&b)|(b&c)|(a&c)') + result = algebra2.parse('(~a&b&c) | (a&~b&c) | (a&b&~c) | (a&b&c)', simplify=True) + self.assertEqual(expected, result) + + expected = algebra1.parse('b&d') + result = algebra1.parse('(a&b&c&d) | (b&d)', simplify=True) + self.assertEqual(expected, result) + + expected = algebra1.parse('b&d') + result = algebra2.parse('(a&b&c&d) | (b&d)', simplify=True) + self.assertEqual(expected, result) + + expected = algebra1.parse('(~b&~d&a) | (~c&~d&b) | (a&c&d)', simplify=True) + result = algebra1.parse('''(~a&b&~c&~d) | (a&~b&~c&~d) | (a&~b&c&~d) | + (a&~b&c&d) | (a&b&~c&~d) | (a&b&c&d)''', simplify=True) + self.assertEqual(expected.pretty(), result.pretty()) + + expected = algebra1.parse('(~b&~d&a) | (~c&~d&b) | (a&c&d)', simplify=True) + result = algebra2.parse('''(~a&b&~c&~d) | (a&~b&~c&~d) | (a&~b&c&~d) | + (a&~b&c&d) | (a&b&~c&~d) | (a&b&c&d)''', simplify=True) + self.assertEqual(expected.pretty(), result.pretty()) + + @expectedFailure + def test_parse_complex_expression_should_create_same_expression_as_python(self): + algebra = BooleanAlgebra() + a, b, c = algebra.symbols(*'abc') + + test_expression_str = '''(~a | ~b | ~c)''' + parsed = algebra.parse(test_expression_str) + test_expression = (~a | ~b | ~c) # & ~d + # print() + # print('parsed') + # print(parsed.pretty()) + # print('python') + # print(test_expression.pretty()) + # we have a different behavior for expressions built from python expressions + # vs. expression built from an object tree vs. expression built from a parse + self.assertEqual(parsed.pretty(), test_expression.pretty()) + self.assertEqual(parsed, test_expression) + + @expectedFailure + def test_simplify_complex_expression_parsed_with_simplify(self): + # FIXME: THIS SHOULD NOT FAIL + algebra = BooleanAlgebra() + a = algebra.Symbol('a') + b = algebra.Symbol('b') + c = algebra.Symbol('c') + d = algebra.Symbol('d') + + test_expression_str = ''' + (~a&~b&~c&~d) | (~a&~b&~c&d) | (~a&b&~c&~d) | + (~a&b&c&d) | (~a&b&~c&d) | (~a&b&c&~d) | + (a&~b&~c&d) | (~a&b&c&d) | (a&~b&c&d) | (a&b&c&d) + ''' + + parsed = algebra.parse(test_expression_str, simplify=True) + + test_expression = ( + (~a & ~b & ~c & ~d) | (~a & ~b & ~c & d) | (~a & b & ~c & ~d) | + (~a & b & c & d) | (~a & b & ~c & d) | (~a & b & c & ~d) | + (a & ~b & ~c & d) | (~a & b & c & d) | (a & ~b & c & d) | (a & b & c & d) + ).simplify() + + # we have a different simplify behavior for expressions built from python expressions + # vs. expression built from an object tree vs. expression built from a parse + self.assertEqual(parsed.pretty(), test_expression.pretty()) + + @expectedFailure + def test_complex_expression_without_parens_parsed_or_built_in_python_should_be_identical(self): + # FIXME: THIS SHOULD NOT FAIL + algebra = BooleanAlgebra() + a = algebra.Symbol('a') + b = algebra.Symbol('b') + c = algebra.Symbol('c') + d = algebra.Symbol('d') + + test_expression_str = ''' + ~a&~b&~c&~d | ~a&~b&~c&d | ~a&b&~c&~d | + ~a&b&c&d | ~a&b&~c&d | ~a&b&c&~d | + a&~b&~c&d | ~a&b&c&d | a&~b&c&d | a&b&c&d + ''' + + parsed = algebra.parse(test_expression_str) + + test_expression = ( + ~a & ~b & ~c & ~d | ~a & ~b & ~c & d | ~a & b & ~c & ~d | + ~ a & b & c & d | ~a & b & ~c & d | ~a & b & c & ~d | + a & ~b & ~c & d | ~a & b & c & d | a & ~b & c & d | a & b & c & d + ) + + self.assertEqual(parsed.pretty(), test_expression.pretty()) + + @expectedFailure + def test_simplify_complex_expression_parsed_then_simplified(self): + # FIXME: THIS SHOULD NOT FAIL + + algebra = BooleanAlgebra() + a = algebra.Symbol('a') + b = algebra.Symbol('b') + c = algebra.Symbol('c') + d = algebra.Symbol('d') + parse = algebra.parse + + test_expression_str = ''.join(''' + (~a&~b&~c&~d) | (~a&~b&~c&d) | (~a&b&~c&~d) | + (~a&b&c&d) | (~a&b&~c&d) | (~a&b&c&~d) | + (a&~b&~c&d) | (~a&b&c&d) | (a&~b&c&d) | (a&b&c&d) + '''.split()) + + test_expression = ( + (~a & ~b & ~c & ~d) | (~a & ~b & ~c & d) | (~a & b & ~c & ~d) | + (~a & b & c & d) | (~a & b & ~c & d) | (~a & b & c & ~d) | + (a & ~b & ~c & d) | (~a & b & c & d) | (a & ~b & c & d) | (a & b & c & d) + ) + + parsed = parse(test_expression_str) + self.assertEqual(test_expression_str, str(parsed)) + + expected = (a & ~b & d) | (~a & b) | (~a & ~c) | (b & c & d) + self.assertEqual(expected.pretty(), test_expression.simplify().pretty()) + + parsed = parse(test_expression_str, simplify=True) + + # FIXME: THIS SHOULD NOT FAIL + # we have a different simplify behavior for expressions built from python expressions + # vs. expression built from an object tree vs. expression built from a parse + self.assertEqual(expected.simplify().pretty(), parsed.simplify().pretty()) + + expected_str = '(a&~b&d)|(~a&b)|(~a&~c)|(b&c&d)' + self.assertEqual(expected_str, str(parsed)) + + parsed2 = parse(test_expression_str) + self.assertEqual(expected.pretty(), parsed2.simplify().pretty()) + + self.assertEqual(expected_str, str(parsed2.simplify())) + + expected = algebra.OR( + algebra.AND( + algebra.NOT(algebra.Symbol('a')), + algebra.NOT(algebra.Symbol('b')), + algebra.NOT(algebra.Symbol('c')), + algebra.NOT(algebra.Symbol('d')) + ), + algebra.AND( + algebra.NOT(algebra.Symbol('a')), + algebra.NOT(algebra.Symbol('b')), + algebra.NOT(algebra.Symbol('c')), + algebra.Symbol('d') + ), + algebra.AND( + algebra.NOT(algebra.Symbol('a')), + algebra.Symbol('b'), + algebra.NOT(algebra.Symbol('c')), + algebra.NOT(algebra.Symbol('d')) + ), + algebra.AND( + algebra.NOT(algebra.Symbol('a')), + algebra.Symbol('b'), + algebra.Symbol('c'), + algebra.Symbol('d')), + algebra.AND( + algebra.NOT(algebra.Symbol('a')), + algebra.Symbol('b'), + algebra.NOT(algebra.Symbol('c')), + algebra.Symbol('d') + ), + algebra.AND( + algebra.NOT(algebra.Symbol('a')), + algebra.Symbol('b'), + algebra.Symbol('c'), + algebra.NOT(algebra.Symbol('d')) + ), + algebra.AND( + algebra.Symbol('a'), + algebra.NOT(algebra.Symbol('b')), + algebra.NOT(algebra.Symbol('c')), + algebra.Symbol('d') + ), + algebra.AND( + algebra.NOT(algebra.Symbol('a')), + algebra.Symbol('b'), + algebra.Symbol('c'), + algebra.Symbol('d') + ), + algebra.AND( + algebra.Symbol('a'), + algebra.NOT(algebra.Symbol('b')), + algebra.Symbol('c'), + algebra.Symbol('d') + ), + algebra.AND( + algebra.Symbol('a'), + algebra.Symbol('b'), + algebra.Symbol('c'), + algebra.Symbol('d') + ) + ) + + result = parse(test_expression_str) + result = result.simplify() + self.assertEqual(expected, result) + + def test_parse_invalid_nested_and_should_raise_a_proper_exception(self): + algebra = BooleanAlgebra() + expr = '''a (and b)''' + + with self.assertRaises(ParseError) as context: + algebra.parse(expr) + + self.assertEqual( + context.exception.error_code, PARSE_INVALID_NESTING + ) + + def test_subtract(self): + parse = BooleanAlgebra().parse + expr = parse('a&b&c') + p1 = parse('b&d') + p2 = parse('a&c') + result = parse('b') + self.assertEqual(expr.subtract(p1, simplify=True), expr) + self.assertEqual(expr.subtract(p2, simplify=True), result) + + def test_flatten(self): + parse = BooleanAlgebra().parse + + t1 = parse('a & (b&c)') + t2 = parse('a&b&c') + self.assertNotEqual(t1, t2) + self.assertEqual(t1.flatten(), t2) + + t1 = parse('a | ((b&c) | (a&c)) | b') + t2 = parse('a | (b&c) | (a&c) | b') + self.assertNotEqual(t1, t2) + self.assertEqual(t1.flatten(), t2) + + def test_distributive(self): + algebra = BooleanAlgebra() + a = algebra.Symbol('a') + b = algebra.Symbol('b') + c = algebra.Symbol('c') + d = algebra.Symbol('d') + e = algebra.Symbol('e') + self.assertEqual((a & (b | c)).distributive(), (a & b) | (a & c)) + t1 = algebra.AND(a, (b | c), (d | e)) + t2 = algebra.OR(algebra.AND(a, b, d), algebra.AND(a, b, e), algebra.AND(a, c, d), algebra.AND(a, c, e)) + self.assertEqual(t1.distributive(), t2) + + def test_equal(self): + if "__PEX_UNVENDORED__" in __import__("os").environ: + from boolean.boolean import DualBase # vendor:skip + else: + from pex.third_party.boolean.boolean import DualBase + + a, b, c = Symbol('a'), Symbol('b'), Symbol('c') + t1 = DualBase(a, b) + t1_2 = DualBase(b, a) + + t2 = DualBase(a, b, c) + t2_2 = DualBase(b, c, a) + + # Test __eq__. + self.assertTrue(t1 == t1) + self.assertTrue(t1_2 == t1) + self.assertTrue(t2_2 == t2) + self.assertFalse(t1 == t2) + self.assertFalse(t1 == 1) + self.assertFalse(t1 is True) + self.assertFalse(t1 is None) + + # Test __ne__. + self.assertFalse(t1 != t1) + self.assertFalse(t1_2 != t1) + self.assertFalse(t2_2 != t2) + self.assertTrue(t1 != t2) + self.assertTrue(t1 != 1) + self.assertTrue(t1 is not True) + self.assertTrue(t1 is not None) + + def test_order(self): + algebra = BooleanAlgebra() + x, y, z = algebra.Symbol(1), algebra.Symbol(2), algebra.Symbol(3) + self.assertTrue(algebra.AND(x, y) < algebra.AND(x, y, z)) + self.assertTrue(not algebra.AND(x, y) > algebra.AND(x, y, z)) + self.assertTrue(algebra.AND(x, y) < algebra.AND(x, z)) + self.assertTrue(not algebra.AND(x, y) > algebra.AND(x, z)) + self.assertTrue(algebra.AND(x, y) < algebra.AND(y, z)) + self.assertTrue(not algebra.AND(x, y) > algebra.AND(y, z)) + self.assertTrue(not algebra.AND(x, y) < algebra.AND(x, y)) + self.assertTrue(not algebra.AND(x, y) > algebra.AND(x, y)) + + def test_printing(self): + parse = BooleanAlgebra().parse + self.assertEqual(str(parse('a&a')), 'a&a') + self.assertEqual(repr(parse('a&a')), "AND(Symbol('a'), Symbol('a'))") + self.assertEqual(str(parse('a|a')), 'a|a') + self.assertEqual(repr(parse('a|a')), "OR(Symbol('a'), Symbol('a'))") + self.assertEqual(str(parse('(a|b)&c')), '(a|b)&c') + self.assertEqual(repr(parse('(a|b)&c')), "AND(OR(Symbol('a'), Symbol('b')), Symbol('c'))") + + +class OtherTestCase(unittest.TestCase): + + def test_class_order(self): + # FIXME: this test is cryptic: what does it do? + algebra = BooleanAlgebra() + order = ( + (algebra.TRUE, algebra.FALSE), + (algebra.Symbol('y'), algebra.Symbol('x')), + (algebra.parse('x&y'),), + (algebra.parse('x|y'),), + ) + for i, tests in enumerate(order): + for case1 in tests: + for j in range(i + 1, len(order)): + for case2 in order[j]: + + self.assertTrue(case1 < case2) + self.assertTrue(case2 > case1) + + def test_parse(self): + algebra = BooleanAlgebra() + a, b, c = algebra.Symbol('a'), algebra.Symbol('b'), algebra.Symbol('c') + self.assertEqual(algebra.parse('0'), algebra.FALSE) + self.assertEqual(algebra.parse('(0)'), algebra.FALSE) + self.assertEqual(algebra.parse('1') , algebra.TRUE) + self.assertEqual(algebra.parse('(1)'), algebra.TRUE) + self.assertEqual(algebra.parse('a'), a) + self.assertEqual(algebra.parse('(a)'), a) + self.assertEqual(algebra.parse('(a)'), a) + self.assertEqual(algebra.parse('~a'), algebra.parse('~(a)')) + self.assertEqual(algebra.parse('~(a)'), algebra.parse('(~a)')) + self.assertEqual(algebra.parse('~a'), ~a) + self.assertEqual(algebra.parse('(~a)'), ~a) + self.assertEqual(algebra.parse('~~a', simplify=True), (~~a).simplify()) + self.assertEqual(algebra.parse('a&b'), a & b) + self.assertEqual(algebra.parse('~a&b'), ~a & b) + self.assertEqual(algebra.parse('a&~b'), a & ~b) + self.assertEqual(algebra.parse('a&b&c'), algebra.parse('a&b&c')) + self.assertEqual(algebra.parse('a&b&c'), algebra.AND(a, b, c)) + self.assertEqual(algebra.parse('~a&~b&~c'), algebra.parse('~a&~b&~c')) + self.assertEqual(algebra.parse('~a&~b&~c'), algebra.AND(~a, ~b, ~c)) + self.assertEqual(algebra.parse('a|b'), a | b) + self.assertEqual(algebra.parse('~a|b'), ~a | b) + self.assertEqual(algebra.parse('a|~b'), a | ~b) + self.assertEqual(algebra.parse('a|b|c'), algebra.parse('a|b|c')) + self.assertEqual(algebra.parse('a|b|c'), algebra.OR(a, b, c)) + self.assertEqual(algebra.parse('~a|~b|~c'), algebra.OR(~a, ~b, ~c)) + self.assertEqual(algebra.parse('(a|b)'), a | b) + self.assertEqual(algebra.parse('a&(a|b)', simplify=True), (a & (a | b)).simplify()) + self.assertEqual(algebra.parse('a&(a|~b)', simplify=True), (a & (a | ~b)).simplify()) + self.assertEqual(algebra.parse('(a&b)|(b&((c|a)&(b|(c&a))))', simplify=True), ((a & b) | (b & ((c | a) & (b | (c & a))))).simplify()) + self.assertEqual(algebra.parse('(a&b)|(b&((c|a)&(b|(c&a))))', simplify=True), algebra.parse('a&b | b&(c|a)&(b|c&a)', simplify=True)) + + def test_subs(self): + algebra = BooleanAlgebra() + a, b, c = algebra.Symbol('a'), algebra.Symbol('b'), algebra.Symbol('c') + expr = a & b | c + self.assertEqual(expr.subs({a: b}).simplify(), b | c) + self.assertEqual(expr.subs({a: a}).simplify(), expr) + self.assertEqual(expr.subs({a: b | c}).simplify(), algebra.parse('(b|c)&b|c').simplify()) + self.assertEqual(expr.subs({a & b: a}).simplify(), a | c) + self.assertEqual(expr.subs({c: algebra.TRUE}).simplify(), algebra.TRUE) + + def test_subs_default(self): + algebra = BooleanAlgebra() + a, b, c = algebra.Symbol('a'), algebra.Symbol('b'), algebra.Symbol('c') + expr = a & b | c + self.assertEqual(expr.subs({}, default=algebra.TRUE).simplify(), algebra.TRUE) + self.assertEqual(expr.subs({a: algebra.FALSE, c: algebra.FALSE}, default=algebra.TRUE).simplify(), algebra.FALSE) + self.assertEqual(algebra.TRUE.subs({}, default=algebra.FALSE).simplify(), algebra.TRUE) + self.assertEqual(algebra.FALSE.subs({}, default=algebra.TRUE).simplify(), algebra.FALSE) + + def test_normalize(self): + algebra = BooleanAlgebra() + + expr = algebra.parse("a&b") + self.assertEqual(algebra.dnf(expr), expr) + self.assertEqual(algebra.cnf(expr), expr) + + expr = algebra.parse("a|b") + self.assertEqual(algebra.dnf(expr), expr) + self.assertEqual(algebra.cnf(expr), expr) + + expr = algebra.parse("(a&b)|(c&b)") + result_dnf = algebra.parse("(a&b)|(b&c)") + result_cnf = algebra.parse("b&(a|c)") + self.assertEqual(algebra.dnf(expr), result_dnf) + self.assertEqual(algebra.cnf(expr), result_cnf) + + expr = algebra.parse("(a|b)&(c|b)") + result_dnf = algebra.parse("b|(a&c)") + result_cnf = algebra.parse("(a|b)&(b|c)") + self.assertEqual(algebra.dnf(expr), result_dnf) + self.assertEqual(algebra.cnf(expr), result_cnf) + + expr = algebra.parse('((s|a)&(s|b)&(s|c)&(s|d)&(e|c|d))|(a&e&d)') + result = algebra.normalize(expr, expr.AND) + expected = algebra.parse('(a|s)&(b|e|s)&(c|d|e)&(c|e|s)&(d|s)') + self.assertEqual(result, expected) + + def test_get_literals_return_all_literals_in_original_order(self): + alg = BooleanAlgebra() + exp = alg.parse('a and b or a and c') + assert [alg.Symbol('a'), alg.Symbol('b'), alg.Symbol('a'), alg.Symbol('c')] == exp.get_literals() + + def test_get_symbols_return_all_symbols_in_original_order(self): + alg = BooleanAlgebra() + exp = alg.parse('a and b or True and a and c') + assert [alg.Symbol('a'), alg.Symbol('b'), alg.Symbol('a'), alg.Symbol('c')] == exp.get_symbols() + + def test_literals_return_set_of_unique_literals(self): + alg = BooleanAlgebra() + exp = alg.parse('a and b or a and c') + assert set([alg.Symbol('a'), alg.Symbol('b'), alg.Symbol('c')]) == exp.literals + + def test_literals_and_negation(self): + alg = BooleanAlgebra() + exp = alg.parse('a and not b and not not c') + assert set([alg.Symbol('a'), alg.parse('not b'), alg.parse('not c')]) == exp.literals + + def test_symbols_and_negation(self): + alg = BooleanAlgebra() + exp = alg.parse('a and not b and not not c') + assert set([alg.Symbol('a'), alg.Symbol('b'), alg.Symbol('c')]) == exp.symbols + + def test_objects_return_set_of_unique_Symbol_objs(self): + alg = BooleanAlgebra() + exp = alg.parse('a and b or a and c') + assert set(['a', 'b', 'c']) == exp.objects + + +class BooleanBoolTestCase(unittest.TestCase): + + def test_bool(self): + algebra = BooleanAlgebra() + a, b, c = algebra.Symbol('a'), algebra.Symbol('b'), algebra.Symbol('c') + expr = a & b | c + self.assertRaises(TypeError, bool, expr.subs({a: algebra.TRUE})) + self.assertRaises(TypeError, bool, expr.subs({b: algebra.TRUE})) + self.assertRaises(TypeError, bool, expr.subs({c: algebra.TRUE})) + self.assertRaises(TypeError, bool, expr.subs({a: algebra.TRUE, b: algebra.TRUE})) + result = expr.subs({c: algebra.TRUE}, simplify=True) + result = result.simplify() + self.assertEqual(algebra.TRUE, result) + + result = expr.subs({a: algebra.TRUE, b: algebra.TRUE}, simplify=True) + result = result.simplify() + self.assertEqual(algebra.TRUE, result) + + +class CustomSymbolTestCase(unittest.TestCase): + + def test_custom_symbol(self): + class CustomSymbol(Symbol): + def __init__(self, name, value='value'): + self.var = value + super(CustomSymbol, self).__init__(name) + try: + CustomSymbol('a', value='This is A') + except TypeError as e: + self.fail(e) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_pex_bootstrapper.py b/tests/test_pex_bootstrapper.py index 24c6b7237..93a1452b6 100644 --- a/tests/test_pex_bootstrapper.py +++ b/tests/test_pex_bootstrapper.py @@ -6,6 +6,7 @@ from textwrap import dedent from pex.interpreter import PythonInterpreter +from pex.interpreter_constraints import match_interpreter_constraint from pex.pex_bootstrapper import iter_compatible_interpreters from pex.testing import PY27, PY35, PY36, ensure_python_interpreter @@ -16,6 +17,59 @@ def find_interpreters(path, *constraints): compatibility_constraints=constraints)] +def test_match_interpreter_constraint(): + def identity(version): + return PythonInterpreter.from_binary(ensure_python_interpreter(version)).identity + py27 = identity(PY27) + py35 = identity(PY35) + py36 = identity(PY36) + + expr = 'CPython>=2.7,<3' + assert match_interpreter_constraint(py27, expr) + assert not match_interpreter_constraint(py35, expr) + assert not match_interpreter_constraint(py36, expr) + + expr = 'CPython>=3.5' + assert not match_interpreter_constraint(py27, expr) + assert match_interpreter_constraint(py35, expr) + assert match_interpreter_constraint(py36, expr) + + expr = 'CPython>=3.6' + assert not match_interpreter_constraint(py27, expr) + assert not match_interpreter_constraint(py35, expr) + assert match_interpreter_constraint(py36, expr) + + expr = 'CPython>=2.7,<3 | CPython>=3.5' + assert match_interpreter_constraint(py27, expr) + assert match_interpreter_constraint(py35, expr) + assert match_interpreter_constraint(py36, expr) + + expr = '(CPython>=2.7,<3 | CPython>=3.5) & CPython>=3.6' + assert not match_interpreter_constraint(py27, expr) + assert not match_interpreter_constraint(py35, expr) + assert match_interpreter_constraint(py36, expr) + + expr = '(CPython>=2.7 , <3 | CPython >= 3.5) & ~CPython>=3.6' + assert match_interpreter_constraint(py27, expr) + assert match_interpreter_constraint(py35, expr) + assert not match_interpreter_constraint(py36, expr) + + expr = '(CPython>=2.7,<3 | CPython>=3.5) & (CPython>=3.6 | PyPy>=3.6)' + assert not match_interpreter_constraint(py27, expr) + assert not match_interpreter_constraint(py35, expr) + assert match_interpreter_constraint(py36, expr) + + expr = '>=3.5' + assert not match_interpreter_constraint(py27, expr) + assert match_interpreter_constraint(py35, expr) + assert match_interpreter_constraint(py36, expr) + + expr = '~CPython==3.6.6' + assert match_interpreter_constraint(py27, expr) + assert match_interpreter_constraint(py35, expr) + assert ~match_interpreter_constraint(py36, expr) + + def test_find_compatible_interpreters(): py27 = ensure_python_interpreter(PY27) py35 = ensure_python_interpreter(PY35)