diff --git a/MANIFEST.in b/MANIFEST.in index dae87d1..75c64dd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ graft docs +graft tests graft boolean include LICENSE.txt diff --git a/README.md b/README.md deleted file mode 100644 index e37ac51..0000000 --- a/README.md +++ /dev/null @@ -1,69 +0,0 @@ -boolean.py -========== - -"boolean.py" is a small library implementing a boolean algebra. It -defines two base elements, TRUE and FALSE, and a Symbol class that can take -on one of these two values. Calculations are done in terms of AND, OR and -NOT - other compositions like XOR and NAND are not implemented but can be -emulated with AND or and NOT. -Expressions are constructed from parsed strings or in Python. - -It runs on Python 2.7 and Python 3. - -https://github.com/bastikr/boolean.py - -Build status: [![Build Status](https://travis-ci.org/bastikr/boolean.py.svg?branch=master)](https://travis-ci.org/bastikr/boolean.py) - -Example -------- -``` - >>> import boolean - >>> algebra = boolean.BooleanAlgebra() - >>> expression1 = algebra.parse(u'apple and (oranges or banana) and not banana', simplify=False) - >>> expression1 - AND(Symbol('apple'), OR(Symbol('oranges'), Symbol('banana')), NOT(Symbol('banana'))) - - >>> expression2 = algebra.parse(u'(oranges | banana) and not banana & apple', simplify=True) - >>> expression2 - AND(Symbol('apple'), NOT(Symbol('banana')), Symbol('oranges')) - - >>> expression1 == expression2 - False - >>> expression1.simplify() == expression2 - True -``` - -Documentation -------------- - -http://readthedocs.org/docs/booleanpy/en/latest/ - -Installation ------------- - -`pip install boolean.py` - -Testing -------- - -Test `boolean.py` with your current Python environment: - -`python setup.py test` - -Test with all of the supported Python environments using `tox`: - -``` -pip install -r test-requirements.txt -tox -``` - -If `tox` throws `InterpreterNotFound`, limit it to python interpreters that are actually installed on your machine: - -``` -tox -e py27,py36 -``` - -License -------- - -Simplified BSD License diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..5efc9f8 --- /dev/null +++ b/README.rst @@ -0,0 +1,83 @@ +========== +boolean.py +========== + +.. image:: https://img.shields.io/travis/bastikr/boolean.py.svg + :target: https://travis-ci.org/bastikr/boolean.py +.. image:: https://img.shields.io/pypi/wheel/boolean.py.svg + :target: https://pypi.python.org/pypi/boolean.py/ +.. image:: https://img.shields.io/pypi/v/boolean.py.svg + :target: https://pypi.python.org/pypi/boolean.py/ +.. image:: https://img.shields.io/pypi/pyversions/boolean.py.svg + :target: https://pypi.python.org/pypi/boolean.py/ +.. image:: https://img.shields.io/badge/license-BSD-blue.svg + :target: https://raw.githubusercontent.com/bastikr/boolean.py/master/LICENSE.txt + +This python package implements `Boolean algebra`_. It defines two base elements, +:code:`TRUE` and :code:`FALSE`, and a :code:`Symbol` class. Expressions are +built in terms of :code:`AND`, :code:`OR` and :code:`NOT`. Other functions, like +:code:`XOR` and :code:`NAND`, are not implemented but can be emulated with +:code:`AND` or and :code:`NOT`. Expressions are constructed from parsed strings +or in Python. + +.. _`Boolean algebra`: https://en.wikipedia.org/wiki/Boolean_algebra + +Example +======= + +.. code-block:: python + + >>> import boolean + >>> algebra = boolean.BooleanAlgebra() + >>> expression1 = algebra.parse(u'apple and (oranges or banana) and not banana', simplify=False) + >>> expression1 + AND(Symbol('apple'), OR(Symbol('oranges'), Symbol('banana')), NOT(Symbol('banana'))) + + >>> expression2 = algebra.parse(u'(oranges | banana) and not banana & apple', simplify=True) + >>> expression2 + AND(Symbol('apple'), NOT(Symbol('banana')), Symbol('oranges')) + + >>> expression1 == expression2 + False + >>> expression1.simplify() == expression2 + True + +Documentation +============= + +http://readthedocs.org/docs/booleanpy/en/latest/ + +Installation +============ + +.. code-block:: shell + + pip install boolean.py + +Testing +======= + +Test :code:`boolean.py` with your current Python environment: + +.. code-block:: shell + + python setup.py test + +Test with all of the supported Python environments using :code:`tox`: + +.. code-block:: shell + + pip install -r test-requirements.txt + tox + +If :code:`tox` throws :code:`InterpreterNotFound`, limit it to python +interpreters that are actually installed on your machine: + +.. code-block:: shell + + tox -e py27,py36 + +License +======= + +Simplified BSD License diff --git a/setup.cfg b/setup.cfg index 49eae28..d037fcc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,7 @@ universal = 1 [aliases] +test = pytest release = clean --all sdist --formats=gztar bdist_wheel register upload [metadata] diff --git a/setup.py b/setup.py index 371b9f3..3cedbc2 100644 --- a/setup.py +++ b/setup.py @@ -5,40 +5,23 @@ from setuptools import find_packages from setuptools import setup - -long_desc = ''' -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. -''' - +with open('README.rst') as readme: + long_description = readme.read() setup( name='boolean.py', version='3.4', - license='revised BSD license', + license='Simplified BSD license', description='Define boolean algebras, create and parse boolean expressions and create custom boolean DSL.', - long_description=long_desc, + long_description=long_description, author='Sebastian Kraemer', author_email='basti.kr@gmail.com', url='https://github.com/bastikr/boolean.py', packages=find_packages(), include_package_data=True, zip_safe=False, - test_loader='unittest:TestLoader', - test_suite='boolean.test_boolean', + setup_requires=['pytest-runner'], + tests_require=['pytest'], keywords='boolean expression, boolean algebra, logic, expression parser', classifiers=[ 'Development Status :: 4 - Beta', diff --git a/test-requirements.txt b/test-requirements.txt index a9be04b..fd153a6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,4 @@ -tox==2.7.0 +tox +pytest +pytest-cov +pytest-xdist diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/mock_advanced_algebra.py b/tests/mock_advanced_algebra.py new file mode 100644 index 0000000..67b6963 --- /dev/null +++ b/tests/mock_advanced_algebra.py @@ -0,0 +1,116 @@ +import tokenize + +try: + # Python 2 + basestring +except NameError: + # Python 3 + basestring = str + +try: + from io import StringIO +except ImportError: + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO + +from boolean import BooleanAlgebra, Symbol +from boolean import TOKEN_LPAR, TOKEN_RPAR +from boolean import TOKEN_TRUE, TOKEN_FALSE +from boolean import TOKEN_AND, TOKEN_OR, TOKEN_NOT + +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()) diff --git a/tests/mock_custom_algebra.py b/tests/mock_custom_algebra.py new file mode 100644 index 0000000..c6ac644 --- /dev/null +++ b/tests/mock_custom_algebra.py @@ -0,0 +1,31 @@ +from boolean import BooleanAlgebra, Symbol + +from boolean import TOKEN_SYMBOL +from boolean import TOKEN_LPAR, TOKEN_RPAR +from boolean import TOKEN_AND, TOKEN_OR, TOKEN_NOT + +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) diff --git a/boolean/test_boolean.py b/tests/test_boolean.py similarity index 72% rename from boolean/test_boolean.py rename to tests/test_boolean.py index fa9fbf8..a7e7222 100644 --- a/boolean/test_boolean.py +++ b/tests/test_boolean.py @@ -10,12 +10,12 @@ from __future__ import absolute_import from __future__ import unicode_literals from __future__ import print_function -from boolean.boolean import PARSE_UNKNOWN_TOKEN -try: - basestring # Python 2 -except NameError: - basestring = str # Python 3 +import copy +import pytest + +from boolean.boolean import BaseElement +from boolean.boolean import PARSE_UNKNOWN_TOKEN import unittest from unittest.case import expectedFailure @@ -35,6 +35,10 @@ from boolean.boolean import PARSE_INVALID_EXPRESSION from boolean.boolean import PARSE_INVALID_NESTING +from tests.mock_custom_algebra import CustomAlgebra + +from tests.mock_advanced_algebra import AdvancedAlgebra +from tests.mock_advanced_algebra import PlainVar, ColonDotVar class BooleanAlgebraTestCase(unittest.TestCase): @@ -97,33 +101,6 @@ def test_parse_recognizes_trueish_and_falsish_symbol_tokens(self): 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 ) ''' @@ -145,112 +122,6 @@ def tokenize(self, s): 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 @@ -324,217 +195,436 @@ def test_parse_side_by_side_symbols_with_parens_raise_exception(self): except ParseError as pe: assert pe.error_code == PARSE_INVALID_NESTING -class BaseElementTestCase(unittest.TestCase): +class TestBaseElement: - def test_creation(self): - from boolean.boolean import BaseElement + def test_base_element_works(self): + try: + BaseElement() + except Exception as e: + pytest.fail('Unexpected exception: ' + str(e)) + + def test_base_element_raises(self): + with pytest.raises(TypeError): + BaseElement(2) + + with pytest.raises(TypeError): + BaseElement('a') + + def test_true_and_false(self): 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) + + assert algebra.TRUE is algebra.TRUE + assert algebra.FALSE is algebra.FALSE + + assert algebra.TRUE == algebra.TRUE + assert algebra.FALSE == algebra.FALSE + + assert algebra.TRUE is not algebra.FALSE + assert algebra.FALSE is not algebra.TRUE + + assert algebra.TRUE != algebra.FALSE + assert algebra.FALSE != algebra.TRUE + + assert bool(algebra.TRUE) is True + assert bool(algebra.FALSE) is False + + assert algebra.TRUE == True + assert algebra.FALSE == False def test_literals(self): algebra = BooleanAlgebra() - self.assertEqual(algebra.TRUE.literals, set()) - self.assertEqual(algebra.FALSE.literals, set()) + + assert algebra.TRUE.literals == set() + assert algebra.FALSE.literals == set() def test_literalize(self): algebra = BooleanAlgebra() - self.assertEqual(algebra.TRUE.literalize(), algebra.TRUE) - self.assertEqual(algebra.FALSE.literalize(), algebra.FALSE) + + assert algebra.TRUE.literalize() == algebra.TRUE + assert algebra.FALSE.literalize() == algebra.FALSE + + assert algebra.TRUE.literalize() != algebra.FALSE + assert algebra.FALSE.literalize() != algebra.TRUE def test_simplify(self): algebra = BooleanAlgebra() - self.assertEqual(algebra.TRUE.simplify(), algebra.TRUE) - self.assertEqual(algebra.FALSE.simplify(), algebra.FALSE) + + assert algebra.TRUE.simplify() == algebra.TRUE + assert algebra.FALSE.simplify() == algebra.FALSE def test_dual(self): algebra = BooleanAlgebra() - self.assertEqual(algebra.TRUE.dual, algebra.FALSE) - self.assertEqual(algebra.FALSE.dual, algebra.TRUE) + + assert algebra.TRUE.dual == algebra.FALSE + assert 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) + + assert algebra.TRUE == algebra.TRUE + assert algebra.FALSE == algebra.FALSE + + assert algebra.TRUE != algebra.FALSE + assert not (algebra.TRUE == algebra.FALSE) def test_order(self): algebra = BooleanAlgebra() - self.assertTrue(algebra.FALSE < algebra.TRUE) - self.assertTrue(algebra.TRUE > algebra.FALSE) + + assert algebra.FALSE < algebra.TRUE + assert 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') + assert str(algebra.TRUE) == '1' + assert str(algebra.FALSE) == '0' -class SymbolTestCase(unittest.TestCase): + assert repr(algebra.TRUE) == 'TRUE' + assert repr(algebra.FALSE) == 'FALSE' - def test_init(self): - Symbol(1) - Symbol('a') - Symbol(None) - Symbol(sum) - Symbol((1, 2, 3)) - Symbol([1, 2]) +class TestSymbolCase: + + def test_symbol_works(self): + try: + Symbol(1) + Symbol('a') + Symbol(None) + Symbol(sum) + Symbol((1, 2, 3)) + Symbol([1, 2]) + except Exception as e: + pytest.fail('Unexpected exception: ' + str(e)) def test_isliteral(self): - self.assertTrue(Symbol(1).isliteral is True) + assert 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) + + assert l1 in l1.literals + assert l1 in l2.literals + assert l2 in l1.literals + assert l2 in l2.literals + + for symbol in [l1, l2]: + with pytest.raises(AttributeError): + symbol.setattr('literals', 1) def test_literalize(self): s = Symbol(1) - self.assertEqual(s.literalize(), s) + + assert s.literalize() == s def test_simplify(self): s = Symbol(1) - self.assertEqual(s.simplify(), s) - def test_equal_symbols(self): + assert s.simplify() == s + + def test_symbols_eq_0(self): algebra = BooleanAlgebra() + a = algebra.Symbol('a') - a2 = algebra.Symbol('a') - c = algebra.Symbol('b') - d = algebra.Symbol('d') - e = algebra.Symbol('e') + assert a == a - # 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_symbols_eq_1(self): + algebra = BooleanAlgebra() + + a0 = algebra.Symbol('a') + a1 = algebra.Symbol('a') + + assert a0 == a1 + assert a1 == a0 + + def test_symbols_eq_2(self): + algebra = BooleanAlgebra() + + a = algebra.Symbol('a') + b = algebra.Symbol('b') + + assert not (a == b) + assert not (b == a) + + def test_symbols_eq_3(self): + algebra = BooleanAlgebra() + + assert algebra.Symbol('a') == algebra.Symbol('a') + assert not (algebra.Symbol('a') == algebra.Symbol('b')) + + def test_symbols_ne_0(self): + algebra = BooleanAlgebra() + + a = algebra.Symbol('a') + + assert not (a != a) + + def test_symbols_ne_1(self): + algebra = BooleanAlgebra() + + a0 = algebra.Symbol('a') + a1 = algebra.Symbol('a') + + assert not (a0 != a1) + assert not (a1 != a0) + + def test_symbols_ne_2(self): + algebra = BooleanAlgebra() + + a = algebra.Symbol('a') + b = algebra.Symbol('b') + + assert a != b + assert b != a + + def test_symbols_ne_3(self): + algebra = BooleanAlgebra() + + assert not (algebra.Symbol('a') != algebra.Symbol('a')) + assert algebra.Symbol('a') != algebra.Symbol('b') + + def test_symbols_eq_ne(self): + algebra = BooleanAlgebra() + + symbols0 = [ + algebra.Symbol('knights'), + algebra.Symbol('who'), + algebra.Symbol('say'), + algebra.Symbol('ni!'), + algebra.Symbol('Beautiful is better than ugly.'), + algebra.Symbol('Explicit is better than implicit.'), + algebra.Symbol('0'), + algebra.Symbol('1'), + algebra.Symbol('^'), + algebra.Symbol(-1), + algebra.Symbol(0), + algebra.Symbol(1), + algebra.Symbol('123'), + algebra.Symbol('!!!'), + ] + + symbols1 = copy.deepcopy(symbols0) + + for symbol in symbols0: + assert symbol == symbol + assert not (symbol != symbol) + + for symbol0, symbol1 in zip(symbols0, symbols1): + assert symbol0 == symbol1 + assert symbol1 == symbol0 + + assert not (symbol0 != symbol1) + assert not (symbol1 != symbol0) + + for i in range(len(symbols0)): + for j in range(i + 1, len(symbols0)): + assert not (symbols0[i] == symbols1[j]) + assert not (symbols1[j] == symbols0[i]) + + assert symbols0[i] != symbols1[j] + assert symbols1[j] != symbols0[i] 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)) + assert Symbol(-1) < Symbol(0) + assert Symbol(0) > Symbol(-1) + + assert Symbol(1) < Symbol(2) + assert Symbol(2) > Symbol(1) + + assert Symbol('x') < Symbol('y') + assert Symbol('y') > Symbol('x') 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))) + assert 'a' == str(Symbol('a')) + assert "Symbol('a')" == repr(Symbol('a')) + assert '1' == str(Symbol(1)) + assert 'Symbol(1)' == repr(Symbol(1)) -class NOTTestCase(unittest.TestCase): + assert '-1' == str(Symbol(-1)) + assert 'Symbol(-1)' == repr(Symbol(-1)) - def test_init(self): +class TestNOT: + + def test_raises(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()) + + with pytest.raises(TypeError): + algebra.NOT() + + with pytest.raises(TypeError): + algebra.NOT('a', 'b') + + def test_true_and_false(self): + algebra = BooleanAlgebra() + + assert algebra.TRUE is (algebra.NOT(algebra.FALSE)).simplify() + assert algebra.FALSE is (algebra.NOT(algebra.TRUE)).simplify() + + assert algebra.TRUE == (algebra.NOT(algebra.FALSE)).simplify() + assert algebra.FALSE == (algebra.NOT(algebra.TRUE)).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): + # negation of a literal is still a literal + assert algebra.NOT(s).isliteral + # negation of a non-literal is still a non-literal + assert not algebra.parse('~(a|b)').isliteral + + def test_literals_0(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) + b = ~a + + assert a.isliteral + assert b.isliteral + + assert a in a.literals + assert b in b.literals + + assert len(a.literals) == 1 + assert len(b.literals) == 1 + + def test_literals_1(self): + algebra = BooleanAlgebra() + + expression = algebra.parse('~(a&a)') + + assert not expression.isliteral - l = algebra.parse('~(a&a)') - self.assertFalse(l.isliteral) - self.assertTrue(a in l.literals) - self.assertEqual(len(l.literals), 1) + assert algebra.Symbol('a') in expression.literals + assert len(expression.literals) == 1 + + def test_literals_2(self): + algebra = BooleanAlgebra() - l = algebra.parse('~(a&a)', simplify=True) - self.assertTrue(l.isliteral) + expression = algebra.parse('~(a&a)', simplify=True) + + assert expression.isliteral + assert expression == algebra.NOT(algebra.Symbol('a')) 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): + assert parse('~a').literalize() == parse('~a') + assert parse('~(a&b)').literalize() == parse('~a|~b') + assert parse('~(a|b)').literalize() == parse('~a&~b') + + def test_invert_eq_not(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)) - - def test_cancel(self): + + assert ~a == ~a + assert ~a == algebra.NOT(a) + + def test_simplify(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): + assert a == a.simplify() + assert a == (~~a).simplify() + assert a == (~~~~a).simplify() + + assert ~a == (~a).simplify() + assert ~a == (~~~a).simplify() + assert ~a == (~~~~~a).simplify() + + assert (~(a & a & a)).simplify() == (~(a & a & a)).simplify() + assert (~(a | a | a)).simplify() == (~(a | a | a)).simplify() + + def test_cancel_0(self): + """ + Test .cancel() on python variables + """ algebra = BooleanAlgebra() + a = algebra.Symbol('a') - b = algebra.Symbol('b') - 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) + + assert a == (~~a).cancel() + assert a == (~~~~a).cancel() + + assert ~a == (~a).cancel() + assert ~a == (~~~a).cancel() + assert ~a == (~~~~~a).cancel() + + def test_cancel_1(self): + """ + Test .cancel() on .parse() results + """ + parse = BooleanAlgebra().parse + + assert parse('a') == parse('~~a').cancel() + assert parse('a') == parse('~~~~a').cancel() + + assert parse('~a') == parse('~a').cancel() + assert parse('~a') == parse('~~~a').cancel() + assert parse('~a') == parse('~~~~~a').cancel() + + def test_cancel_2(self): + """ + Test .cancel() on both Python variables and .parse() results + """ + algebra = BooleanAlgebra() + + a, parse = algebra.Symbol('a'), algebra.parse + + assert a == parse('~~a').cancel() + assert a == parse('~~~~a').cancel() + + assert ~a == parse('~a').cancel() + assert ~a == parse('~~~a').cancel() + assert ~a == parse('~~~~~a').cancel() + + def test_demorgan(self): + parse = BooleanAlgebra().parse + + assert parse('~(a & a)').demorgan() == parse('~a | ~a') + + assert parse('~(a & b)').demorgan() == parse('~a | ~b') + assert parse('~(a & b & c)').demorgan() == parse('~a | ~b | ~c') + + assert parse('~(~a & b)').demorgan() == parse('a | ~b') + assert parse('~(a & ~b)').demorgan() == parse('~a | b') def test_order(self): + # TODO: enforced order is obscure, must explain 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) + + assert x < y + assert y > x + + assert x < ~x + assert ~x > x + + assert ~x < y + assert y > ~x + + assert ~y > x + assert x < ~y 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')))") + assert str(~a) == '~a' + assert repr(~a) == "NOT(Symbol('a'))" -class DualBaseTestCase(unittest.TestCase): + expression = algebra.parse('~(a&a)') - maxDiff = None + assert str(expression) == '~(a&a)' + assert repr(expression) == "NOT(AND(Symbol('a'), Symbol('a')))" + +class DualBaseTestCase(unittest.TestCase): def test_init(self): from boolean.boolean import DualBase diff --git a/tests/test_parse.py b/tests/test_parse.py new file mode 100644 index 0000000..95139c7 --- /dev/null +++ b/tests/test_parse.py @@ -0,0 +1,167 @@ +import pytest + +from boolean import BooleanAlgebra +from boolean import ParseError + +class TestParse: + def test_constant(self): + algebra = BooleanAlgebra() + + for x in ['0', 'false']: + assert algebra.parse(x) == algebra.FALSE + assert algebra.parse('(' + x + ')') == algebra.FALSE + assert algebra.parse('((' + x + '))') == algebra.FALSE + assert algebra.parse('(((' + x + ')))') == algebra.FALSE + + for x in ['1', 'true']: + assert algebra.parse(x) == algebra.TRUE + assert algebra.parse('(' + x + ')') == algebra.TRUE + assert algebra.parse('((' + x + '))') == algebra.TRUE + assert algebra.parse('(((' + x + ')))') == algebra.TRUE + + @pytest.mark.xfail(reason='IndexError indicates parsing problem') + def test_wrong_braces_0(self): + parse = BooleanAlgebra().parse + + for expression in ['(', '((', '(((']: + with pytest.raises(ParseError): + parse(expression) + + for expression in [')', '))', ')))']: + with pytest.raises(ParseError): + parse(expression) + + for expression in ['()', '(())', '((()))']: + with pytest.raises(ParseError): + parse(expression) + + for expression in [')(', '))((', ')))(((']: + with pytest.raises(ParseError): + parse(expression) + + for expression in ['()', '()()', '()()()']: + with pytest.raises(ParseError): + parse(expression) + + @pytest.mark.xfail(reason='IndexError indicates parsing problem') + def test_wrong_braces_1(self): + parse = BooleanAlgebra().parse + + for s in ['(a', 'a(', '((a', '(a(', 'a((']: + with pytest.raises(ParseError): + parse(s) + + for s in ['a)', ')a', '))a', ')a)', 'a))']: + with pytest.raises(ParseError): + parse(s) + + for s in ['a()', '()a', 'a(())', '(a())', '(()a)', '(())a']: + with pytest.raises(ParseError): + parse(s) + + for s in ['a)(', ')a(', ')(a']: + with pytest.raises(ParseError): + parse(s) + + for s in ['a))((', ')a)((', '))(a(', '))((a']: + with pytest.raises(ParseError): + parse(s) + + for s in ['(a)()', '()(a)', '(a)(a)']: + with pytest.raises(ParseError): + parse(s) + + def test_one_symbol_0(self): + algebra = BooleanAlgebra() + + a = algebra.Symbol('a') + + parse = algebra.parse + + assert parse('a') == a + assert parse('(a)') == a + assert parse('((a))') == a + assert parse('(((a)))') == a + + for neg in ['~', '!']: + for space in ['', ' ', ' ']: + assert parse(neg + space + 'a') == ~a + assert parse(neg + space + '(a)') == ~a + assert parse(neg + space + '((a))') == ~a + + assert parse('(' + neg + space + 'a)') == ~a + assert parse('(' + neg + space + '(a))') == ~a + assert parse('((' + neg + space + 'a))') == ~a + + # test `not` separately because it needs extra space + assert parse('not a') == ~a + + assert parse('not(a)') == ~a + assert parse('not (a)') == ~a + + assert parse('not((a))') == ~a + assert parse('not ((a))') == ~a + + assert parse('(not(a))') == ~a + assert parse('(not (a))') == ~a + + assert parse('((not a))') == ~a + + def test_one_symbol_1(self): + algebra = BooleanAlgebra() + + for neg in ['~', '!', 'not']: + with pytest.raises(ParseError): + algebra.parse(neg) + + def test_one_symbol_2(self): + algebra = BooleanAlgebra() + + for x in ['&', 'and', '*']: + with pytest.raises(ParseError): + algebra.parse(x) + + for x in ['|', 'or', '+']: + with pytest.raises(ParseError): + algebra.parse(x) + + def test_one_symbol_3(self): + algebra = BooleanAlgebra() + + a = algebra.Symbol('a') + + invalids = [ + 'a a', + 'a a a', + 'a not', + 'a!', + 'a~', + 'a !', + 'a ~', + 'a not a', + 'a!a', + 'a! a', + 'a !a', + 'a ! a', + 'a~a', + 'a~ a', + 'a ~a', + 'a ~ a', + 'not a a', + '!a a', + '! a a', + '~a a', + '~ a a', + 'a not not a', + 'a!!a', + 'a!! a', + 'a !!a', + 'a !! a', + 'a ! ! a', + 'a! ! a', + 'a ! !a', + ] + + for invalid in invalids: + with pytest.raises(ParseError): + algebra.parse(invalid) diff --git a/tox.ini b/tox.ini index 17a0a12..c5fc7ad 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,4 @@ [tox] -envlist=py27,py36 +envlist=py27,py361 [testenv] commands=python setup.py test \ No newline at end of file