diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 6a25df79d..686323a5d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -30,7 +30,7 @@ jobs: sudo apt update sudo apt-get install -y libopenslide-dev openslide-tools libopenjp2-7 libopenjp2-tools python -m pip install --upgrade pip - python -m pip install ruff==0.5.7 "pytest<8.3.0" pytest-cov pytest-runner + python -m pip install ruff==0.5.7 pytest pytest-cov pytest-runner pip install -r requirements/requirements.txt - name: Cache tiatoolbox static assets uses: actions/cache@v3 diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index cf5165ea7..0ea61cc16 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -7,7 +7,7 @@ mypy>=1.6.1 pip>=22.3 poetry-bumpversion>=0.3.1 pre-commit>=2.20.0 -pytest>=7.2.0, <8.3.0 +pytest>=7.2.0 pytest-cov>=4.0.0 pytest-runner>=6.0 pytest-xdist[psutil] diff --git a/tests/test_dsl.py b/tests/test_dsl.py index 1657db1b6..3f246ca1b 100644 --- a/tests/test_dsl.py +++ b/tests/test_dsl.py @@ -5,7 +5,7 @@ import json import sqlite3 from numbers import Number -from typing import TYPE_CHECKING, Callable, ClassVar +from typing import Callable, ClassVar import pytest @@ -19,9 +19,6 @@ py_regexp, ) -if TYPE_CHECKING: # pragma: no cover - from collections.abc import Mapping - BINARY_OP_STRINGS = [ "+", "-", @@ -115,35 +112,47 @@ def test_prop_or_prop() -> None: ) +py_variables: dict = { + "eval_globals": PY_GLOBALS, + "eval_locals": {"props": SAMPLE_PROPERTIES}, + "check": lambda x: x, +} + +sqlite_variables: dict = { + "eval_globals": SQL_GLOBALS, + "eval_locals": {"props": SQLJSONDictionary()}, + "check": sqlite_eval, +} + +scenario_python: tuple = ( + "Python", + {"scenario_variables": py_variables}, +) +scenario_sqlite = ( + "SQLite", + {"scenario_variables": sqlite_variables}, +) + + +def extract_variables(scenario_variables: dict) -> tuple[dict, dict, Callable]: + """Extract variables from scenario variables.""" + eval_globals = scenario_variables["eval_globals"] + eval_locals = scenario_variables["eval_locals"] + check = scenario_variables["check"] + return eval_globals, eval_locals, check + + class TestPredicate: - """Test predicate statments with various backends.""" - - scenarios: ClassVar[list[str, dict]] = [ - ( - "Python", - { - "eval_globals": PY_GLOBALS, - "eval_locals": {"props": SAMPLE_PROPERTIES}, - "check": lambda x: x, - }, - ), - ( - "SQLite", - { - "eval_globals": SQL_GLOBALS, - "eval_locals": {"props": SQLJSONDictionary()}, - "check": sqlite_eval, - }, - ), - ] + """Test predicate statements with various backends.""" + + scenarios: ClassVar[list[str, dict]] = [scenario_python, scenario_sqlite] @staticmethod def test_number_binary_operations( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Check that binary operations between ints does not error.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) for op in BINARY_OP_STRINGS: query = f"2 {op} 2" result = eval( # skipcq: PYL-W0123 @@ -155,11 +164,10 @@ def test_number_binary_operations( @staticmethod def test_property_binary_operations( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Check that binary operations between properties does not error.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) for op in BINARY_OP_STRINGS: query = f"props['int'] {op} props['int']" result = eval( # skipcq: PYL-W0123 @@ -171,11 +179,10 @@ def test_property_binary_operations( @staticmethod def test_r_binary_operations( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test right hand binary operations between numbers and properties.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) for op in BINARY_OP_STRINGS: query = f"2 {op} props['int']" result = eval( # skipcq: PYL-W0123 @@ -187,11 +194,10 @@ def test_r_binary_operations( @staticmethod def test_number_prefix_operations( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test prefix operations on numbers.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) for op in PREFIX_OP_STRINGS: query = f"{op}1" result = eval( # skipcq: PYL-W0123 @@ -203,11 +209,10 @@ def test_number_prefix_operations( @staticmethod def test_property_prefix_operations( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test prefix operations on properties.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) for op in PREFIX_OP_STRINGS: query = f"{op}props['int']" result = eval( # skipcq: PYL-W0123 @@ -219,11 +224,10 @@ def test_property_prefix_operations( @staticmethod def test_regex_nested_props( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test regex on nested properties.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "props['nesting']['fib'][4]" result = eval( # skipcq: PYL-W0123 query, @@ -234,11 +238,10 @@ def test_regex_nested_props( @staticmethod def test_regex_str_props( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test regex on string properties.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "regexp('Hello', props['string'])" result = eval( # skipcq: PYL-W0123 query, @@ -249,11 +252,10 @@ def test_regex_str_props( @staticmethod def test_regex_str_str( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test regex on string and string.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "regexp('Hello', 'Hello world!')" result = eval( # skipcq: PYL-W0123 query, @@ -264,11 +266,10 @@ def test_regex_str_str( @staticmethod def test_regex_props_str( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test regex on property and string.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "regexp(props['string'], 'Hello world!')" result = eval( # skipcq: PYL-W0123 query, @@ -279,11 +280,10 @@ def test_regex_props_str( @staticmethod def test_regex_ignore_case( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test regex with ignorecase flag.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "regexp('hello', props['string'], re.IGNORECASE)" result = eval( # skipcq: PYL-W0123 query, @@ -294,11 +294,10 @@ def test_regex_ignore_case( @staticmethod def test_regex_no_match( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test regex with no match.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "regexp('Yello', props['string'])" result = eval( # skipcq: PYL-W0123 query, @@ -309,11 +308,10 @@ def test_regex_no_match( @staticmethod def test_has_key( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test has_key function.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "has_key(props, 'foo')" result = eval( # skipcq: PYL-W0123 query, @@ -324,11 +322,10 @@ def test_has_key( @staticmethod def test_is_none( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test is_none function.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "is_none(props['null'])" result = eval( # skipcq: PYL-W0123 query, @@ -339,11 +336,10 @@ def test_is_none( @staticmethod def test_is_not_none( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test is_not_none function.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "is_not_none(props['int'])" result = eval( # skipcq: PYL-W0123 query, @@ -354,11 +350,10 @@ def test_is_not_none( @staticmethod def test_nested_has_key( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test nested has_key function.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "has_key(props['dict'], 'a')" result = eval( # skipcq: PYL-W0123 query, @@ -369,11 +364,10 @@ def test_nested_has_key( @staticmethod def test_list_sum( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test sum function on a list.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "sum(props['list'])" result = eval( # skipcq: PYL-W0123 query, @@ -384,11 +378,10 @@ def test_list_sum( @staticmethod def test_abs( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test abs function.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "abs(props['neg'])" result = eval( # skipcq: PYL-W0123 query, @@ -399,11 +392,10 @@ def test_abs( @staticmethod def test_not( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test not operator.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "not props['bool']" result = eval( # skipcq: PYL-W0123 query, @@ -414,11 +406,10 @@ def test_not( @staticmethod def test_props_int_keys( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test props with int keys.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "props['list'][1]" result = eval( # skipcq: PYL-W0123 query, @@ -429,11 +420,10 @@ def test_props_int_keys( @staticmethod def test_props_get( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test props.get function.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "is_none(props.get('foo'))" result = eval( # skipcq: PYL-W0123 query, @@ -444,11 +434,10 @@ def test_props_get( @staticmethod def test_props_get_default( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test props.get function with default.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "props.get('foo', 42)" result = eval( # skipcq: PYL-W0123 query, @@ -459,11 +448,10 @@ def test_props_get_default( @staticmethod def test_in_list( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test in operator for list.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "1 in props.get('list')" result = eval( # skipcq: PYL-W0123 query, @@ -474,11 +462,10 @@ def test_in_list( @staticmethod def test_has_key_exception( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, # noqa: ARG004 + scenario_variables: dict[str, dict], ) -> None: """Test has_key function with exception.""" + eval_globals, eval_locals, _ = extract_variables(scenario_variables) query = "has_key(1, 'a')" with pytest.raises(TypeError, match="(not iterable)|(Unsupported type)"): _ = eval( # skipcq: PYL-W0123 @@ -489,12 +476,11 @@ def test_has_key_exception( @staticmethod def test_logical_and( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test logical and operator.""" query = "props['bool'] & is_none(props['null'])" + eval_globals, eval_locals, check = extract_variables(scenario_variables) result = eval( # skipcq: PYL-W0123 query, eval_globals, @@ -504,12 +490,11 @@ def test_logical_and( @staticmethod def test_logical_or( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test logical or operator.""" query = "props['bool'] | (props['int'] < 2)" + eval_globals, eval_locals, check = extract_variables(scenario_variables) result = eval( # skipcq: PYL-W0123 query, eval_globals, @@ -519,11 +504,10 @@ def test_logical_or( @staticmethod def test_nested_logic( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test nested logical operators.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "(props['bool'] | (props['int'] < 2)) & abs(props['neg'])" result = eval( # skipcq: PYL-W0123 query, @@ -534,11 +518,10 @@ def test_nested_logic( @staticmethod def test_contains_list( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test contains operator for list.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "1 in props['list']" result = eval( # skipcq: PYL-W0123 query, @@ -549,11 +532,10 @@ def test_contains_list( @staticmethod def test_contains_dict( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test contains operator for dict.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "'a' in props['dict']" result = eval( # skipcq: PYL-W0123 query, @@ -564,11 +546,10 @@ def test_contains_dict( @staticmethod def test_contains_str( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test contains operator for str.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "'Hello' in props['string']" result = eval( # skipcq: PYL-W0123 query, @@ -579,11 +560,10 @@ def test_contains_str( @staticmethod def test_key_with_period( - eval_globals: dict[str, object], - eval_locals: Mapping[str, object], - check: Callable, + scenario_variables: dict[str, dict], ) -> None: """Test key with period.""" + eval_globals, eval_locals, check = extract_variables(scenario_variables) query = "props['dot.key']" result = eval( # skipcq: PYL-W0123 query,