pax_global_header00006660000000000000000000000064147203741240014516gustar00rootroot0000000000000052 comment=a55625523dbf0046fe3e03f5ac612d6d699a8490 tag-expressions-2.0.1/000077500000000000000000000000001472037412400146515ustar00rootroot00000000000000tag-expressions-2.0.1/.github/000077500000000000000000000000001472037412400162115ustar00rootroot00000000000000tag-expressions-2.0.1/.github/workflows/000077500000000000000000000000001472037412400202465ustar00rootroot00000000000000tag-expressions-2.0.1/.github/workflows/build.yml000066400000000000000000000021071472037412400220700ustar00rootroot00000000000000name: Python application on: push: branches: [ "master" ] pull_request: branches: [ "master" ] permissions: contents: read jobs: build: strategy: fail-fast: false max-parallel: 8 matrix: python-version: ['3.9', '3.13'] os: [ubuntu-latest, windows-latest, macOS-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | pytest tests/ tag-expressions-2.0.1/.github/workflows/deploy.yml000066400000000000000000000012331472037412400222640ustar00rootroot00000000000000name: Upload Python Package on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest permissions: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 tag-expressions-2.0.1/.gitignore000066400000000000000000000002161472037412400166400ustar00rootroot00000000000000# vim swap files *.swp # python bytecode files *.pyc __pycache__/ # python virtualenv and build .cache/ .tox/ env/ tagexpressions.egg-info/ tag-expressions-2.0.1/LICENSE000066400000000000000000000020661472037412400156620ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Timo Furrer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tag-expressions-2.0.1/MANIFEST.in000066400000000000000000000001311472037412400164020ustar00rootroot00000000000000include *.rst include *.txt include LICENSE include tox.ini recursive-include tests *.py tag-expressions-2.0.1/README.rst000066400000000000000000000042161472037412400163430ustar00rootroot00000000000000tag-expressions =============== Package to evaluate logical tag expressions by using a modified version of the `Shunting Yard algorithm `_. This package is a Python port of cucumbers tag expression. It's also used by `radish `_. |Build Status| |PyPI package version| |PyPI python versions| Installing ---------- .. code:: bash $ pip install tag-expressions Here is a tease --------------- .. code:: python >>> from tagexpressions import parse >>> >>> expression = '( a and b ) or ( c and d )' >>> compiled_expression = parse(expression) >>> print(compiled_expression) ( ( a and b ) or ( c and d ) ) >>> >>> data = ['a', 'b', 'c', 'd'] >>> assert compiled_expression.evaluate(data) == True >>> >>> data = ['a', 'c'] >>> assert compiled_expression.evaluate(data) == False >>> >>> >>> expression = 'not a or b and not c or not d or e and f' >>> compiled_expression = parse(expression) >>> print(compiled_expression) ( ( ( not ( a ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) ) >>> >>> data = ['b', 'e', 'f'] >>> assert compiled_expression.evaluate(data) == True >>> >>> data = ['a', 'c', 'd'] >>> assert compiled_expression.evaluate(data) == False Usage ----- Available operators ~~~~~~~~~~~~~~~~~~~ * **or** - "or" conjunction of two given variables * **and** - "and" conjunction of two given variables * **not** - negation of a single variable Every other token given in an *infix* is considered a variable. Operator precedence ~~~~~~~~~~~~~~~~~~~ From high to low: * () * or * and * not .. |Build Status| image:: https://github.com/timofurrer/tag-expressions/actions/workflows/build.yml/badge.svg :target: https://github.com/timofurrer/tag-expressions/actions/workflows/build.yml .. |PyPI package version| image:: https://badge.fury.io/py/tag-expressions.svg :target: https://badge.fury.io/py/tag-expressions .. |PyPI python versions| image:: https://img.shields.io/pypi/pyversions/tag-expressions.svg :target: https://pypi.python.org/pypi/tag-expressions tag-expressions-2.0.1/development.txt000066400000000000000000000000071472037412400177310ustar00rootroot00000000000000pytest tag-expressions-2.0.1/setup.py000077500000000000000000000056461472037412400164010ustar00rootroot00000000000000# -*- coding: utf-8 -*- import ast import os import codecs from setuptools import setup, find_packages """ Package to parse logical tag expressions """ PROJECT_ROOT = os.path.dirname(__file__) class VersionFinder(ast.NodeVisitor): def __init__(self): self.version = None def visit_Assign(self, node): try: if node.targets[0].id == '__VERSION__': self.version = node.value.s except: pass def read_version(): """Read version from tagexpressions/__init__.py without loading any files""" finder = VersionFinder() path = os.path.join(PROJECT_ROOT, 'tagexpressions', '__init__.py') with codecs.open(path, 'r', encoding='utf-8') as fp: file_data = fp.read().encode('utf-8') finder.visit(ast.parse(file_data)) return finder.version def local_text_file(*f): path = os.path.join(PROJECT_ROOT, *f) with open(path, 'rt') as fp: file_data = fp.read() return file_data def read_readme(): """Read README content. If the README.rst file does not exist yet (this is the case when not releasing) only the short description is returned. """ try: return local_text_file('README.rst') except IOError: return __doc__ setup(name='tag-expressions', version=read_version(), description=__doc__, long_description=read_readme(), url='http://github.com/timofurrer/tag-expressions', author='Timo Furrer', author_email='tuxtimo@gmail.com', include_package_data=True, packages=find_packages(exclude=['*tests*']), classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Testing' ] ) tag-expressions-2.0.1/tagexpressions/000077500000000000000000000000001472037412400177275ustar00rootroot00000000000000tag-expressions-2.0.1/tagexpressions/__init__.py000066400000000000000000000003101472037412400220320ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Package to parse logical tag expressions """ from .parser import parse, evaluate __VERSION__ = '2.0.1' # only expose the parse function __all__ = ['parse', 'evaluate'] tag-expressions-2.0.1/tagexpressions/models.py000066400000000000000000000037361472037412400215750ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Package to parse logical tag expressions """ class Literal(object): # pylint: disable=too-few-public-methods """ Represents a literal expression """ def __init__(self, value): self.value = value def evaluate(self, values): """Evaluate the expression Check if the literal is in the given set of values. """ return self.value in values def __str__(self): return self.value class Or(object): # pylint: disable=too-few-public-methods """ Represents an "OR" expression """ def __init__(self, left, right): self.left = left self.right = right def evaluate(self, values): """Evaluate the "OR" expression Check if the left "or" right expression evaluate to True. """ return self.left.evaluate(values) or self.right.evaluate(values) def __str__(self): return '( {0} or {1} )'.format(self.left, self.right) class And(object): # pylint: disable=too-few-public-methods """ Represent an "AND" expression """ def __init__(self, left, right): self.left = left self.right = right def evaluate(self, values): """Evaluate the "AND" expression Check if the left "and" right expression evaluate to True. """ return self.left.evaluate(values) and self.right.evaluate(values) def __str__(self): return '( {0} and {1} )'.format(self.left, self.right) class Not(object): # pylint: disable=too-few-public-methods """ Represents a "NOT" expression """ def __init__(self, expression): self.expression = expression def evaluate(self, values): """ Evaluate the "NOT" expression Check if the given expression does not evaluate to True. """ return not self.expression.evaluate(values) def __str__(self): return 'not ( {0} )'.format(self.expression) tag-expressions-2.0.1/tagexpressions/parser.py000066400000000000000000000102271472037412400215770ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Package to parse logical tag expressions """ from collections import deque from uuid import uuid4 from re import compile as compile_regex from .models import Literal, Or, And, Not #: Holds the possible token associativities ASSOC_LEFT = 0 ASSOC_RIGHT = 1 TOKEN_ASSOCS = {'or': ASSOC_LEFT, 'and': ASSOC_LEFT, 'not': ASSOC_RIGHT} #: Holds the token precedences TOKEN_PRECS = {'(': -2, ')': -1, 'or': 0, 'and': 1, 'not': 2} class TagExpressionError(Exception): """Raised for tag expression specific errors""" pass def parse(infix): """Parse the given infix string to an expression which can be evaluated. Known operators are: * or * and * not With the following precedences (from high to low): * not * and * or * ) * ( :param str infix: the input string expression :returns: an expression which can be evaluated with some input values """ # this regex should match most variations of sometag(somevar,someOtherVar). i refer to them as a complex literal because # they will not change the evaluation. a should act the same as a(x) literal_re = compile_regex(r'(?:\w|\d)+\((?:\w|\d|,)+\)') # To support tags with variables, replace all tags containing variables with a unique identifier complex_literals = {str(uuid4()): complex_literal for complex_literal in literal_re.findall(infix)} # now for each complex literal, replace it with the unique_id into the infix for unique_id, complex_literal in complex_literals.items(): infix = infix.replace(complex_literal, unique_id) tokens = infix.replace('(', ' ( ').replace(')', ' ) ').strip().split() #: Holds the parsed operations ops = deque() #: Holds the parsed expressions expressions = deque() for token in tokens: if token in TOKEN_ASSOCS: while len(ops) > 0 and ops[-1] in TOKEN_ASSOCS and ( (TOKEN_ASSOCS[token] == ASSOC_LEFT and TOKEN_PRECS[token] <= TOKEN_PRECS[ops[-1]]) or \ (TOKEN_ASSOCS[token] == ASSOC_RIGHT and TOKEN_PRECS[token] < TOKEN_PRECS[ops[-1]])): create_and_push_expression(ops.pop(), expressions) ops.append(token) elif token == '(': ops.append(token) elif token == ')': while len(ops) > 0 and ops[-1] != '(': create_and_push_expression(ops.pop(), expressions) if len(ops) == 0: raise TagExpressionError('Unclosed (') if ops[-1] == '(': ops.pop() else: # if we replaced the token earlier, then revert that replacement now. create_and_push_expression(token if token not in complex_literals else complex_literals[token], expressions) while len(ops) > 0: if ops[-1] == '(': raise TagExpressionError('Unclosed )') create_and_push_expression(ops.pop(), expressions) expression = expressions.pop() if len(expressions) > 0: raise TagExpressionError('Not empty') return expression def evaluate(infix, values): """Parse the given infix string and evaluate with the given values. :param str infix: the input string expression :param list values: a list of values with variables to match against the infix expression. :returns: if the given values match the infix expression :rtype: bool """ expression = parse(infix) return expression.evaluate(values) def create_and_push_expression(token, expressions): """Creates an expression from the given token and adds it to the stack of the given expression. In the case of "and" and "or" expressions the last expression is poped from the expression stack to link it to the new created one. """ if token == 'and': right_expr = expressions.pop() expressions.append(And(expressions.pop(), right_expr)) elif token == 'or': right_expr = expressions.pop() expressions.append(Or(expressions.pop(), right_expr)) elif token == 'not': expressions.append(Not(expressions.pop())) else: expressions.append(Literal(token)) tag-expressions-2.0.1/tests/000077500000000000000000000000001472037412400160135ustar00rootroot00000000000000tag-expressions-2.0.1/tests/__init__.py000066400000000000000000000000001472037412400201120ustar00rootroot00000000000000tag-expressions-2.0.1/tests/test_parser.py000066400000000000000000000060331472037412400207220ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Package to parse logical tag expressions """ import pytest from tagexpressions import parse, evaluate @pytest.mark.parametrize('infix, expected', [ ('a and b', '( a and b )'), ('a or b', '( a or b )'), ('not a', 'not ( a )'), ('( a and b ) or ( c and d )', '( ( a and b ) or ( c and d ) )'), ('not a or b and not c or not d or e and f', '( ( ( not ( a ) or ( b and not ( c ) ) ) or not ( d ) ) or ( e and f ) )') ]) def test_parser(infix, expected): """Test the tag expression parser""" assert str(parse(infix)) == expected @pytest.mark.parametrize('infix, values, expected', [ ('a and b', ['a', 'b'], True), ('a and b', ['b', 'a'], True), ('a and b', ['a'], False), ('a or b', ['a', 'b'], True), ('a or b', ['b', 'a'], True), ('a or b', ['a'], True), ('a or b', ['b'], True), ('a or b', ['c'], False), ('not a', ['b'], True), ('not a', ['a'], False), ('not a', [], True), ('not a(x)', [], True), ('not a(x)', ['a(x)'], False), ('a(x) or b', ['a(x)'], True), ('a(x) or b', ['b'], True), ('a(x) or b', [''], False), ('a(x,y) or b', [''], False), ('sometag(someValue,y) or b', [''], False), ('sometag(someValue,y) or b(x)', ['b(x)'], True), ('a or (sometag(someValue,y) and not b(x))', ['b(x)', 'sometag(someValue, y)'], False), ('c(y) and (author(inquisitev) or (sometag(someValue,y) and not b(x)))', ['c(y)', 'sometag(someValue,y)'], True), ('c(y) and (author(inquisitev) or (sometag(someValue,y) and not b(x)))', ['c(y)', 'author(inquisitev)'], True), ('c(y) and not (author(inquisitev) or (sometag(someValue,y) and not b(x)))', ['c(y)', "b(x)"], True) ]) def test_basic_evaluation(infix, values, expected): """Test basic tag expression evaluation""" assert parse(infix).evaluate(values) == expected @pytest.mark.parametrize('infix, values, expected', [ ('( a and b ) or ( c and d )', ['a', 'b', 'c', 'd'], True), ('( a and b ) or ( c and d )', ['c', 'd'], True), ('( a and b ) or ( c and d )', ['a', 'b'], True), ('( a and b ) or ( c and d )', ['a', 'c'], False), ('not a or b and not c or not d or e and f', ['b', 'e', 'f'], True), ('not a or b and not c or not d or e and f', ['a', 'b', 'e', 'f'], True), ('not a or b and not c or not d or e and f', ['a', 'b', 'c'], True), ('not a or b and not c or not d or e and f', ['a', 'c', 'd'], False), ]) def test_complex_evaluation(infix, values, expected): """Test complex tag expression evaluation""" assert parse(infix).evaluate(values) == expected @pytest.mark.parametrize('infix, values, expected', [ ('a and b', ['a', 'b'], True), ('a and b', ['a'], False), ('a or b', ['a', 'b'], True), ('a or b', ['a'], True), ('not a', ['b'], True), ('sometag(someValue,y) or b', ['b'], True), ('sometag(someValue,y) or b', ['sometag(someValue,y)'], True) ]) def test_direct_evaluation(infix, values, expected): """Test direct evaluation of an infix against some values""" assert evaluate(infix, values) == expected tag-expressions-2.0.1/tox.ini000066400000000000000000000005471472037412400161720ustar00rootroot00000000000000# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py27, pypy, py33, py34, py35, py36 [testenv] commands = pytest tests/ deps = -rdevelopment.txt