Skip to main content
Code Review

Return to Question

added 2 characters in body
Source Link
InSync
  • 439
  • 3
  • 10

I have a small package that provides some API for playing around with the <An+B> CSS microsyntax. It is 100% type hinted and tested, but doesn't actually have any real/useful features and this question is not forabout the main code anyway.

I have a small package that provides some API for playing around with the <An+B> CSS microsyntax. It is 100% type hinted and tested, but doesn't actually have any real/useful features and this question is not for the main code anyway.

I have a small package that provides some API for playing around with the <An+B> CSS microsyntax. It is 100% type hinted and tested, but doesn't actually have any real/useful features and this question is not about the main code anyway.

Source Link
InSync
  • 439
  • 3
  • 10

Testing code for An+B microsyntax parsing library

I have a small package that provides some API for playing around with the <An+B> CSS microsyntax. It is 100% type hinted and tested, but doesn't actually have any real/useful features and this question is not for the main code anyway.

I was introduced to the Hypothesis testing library in a previous question and have taken a liking to it, so this package is a way for me to practice using it. Therefore, I'm asking for a review of my testing code. Only test files are presented here; other files can be found at the package's repository.

Here's the project structure (irrelevant files removed):

.
├── pyproject.toml
├── src
│ └── a_n_plus_b
│ ├── __init__.py
│ ├── _grammar.py
│ └── py.typed
├── tests
│ ├── __init__.py
│ ├── test_alternate_constructors.py
│ ├── test_indices.py
│ └── test_other_methods.py
└── tox.ini

tests/__init__.py

from collections.abc import Callable, Iterable
from typing import Any, ParamSpec, TypeVar
from _pytest.mark import ParameterSet
from hypothesis import example
from hypothesis.strategies import composite, DrawFn, integers, SearchStrategy
from a_n_plus_b import ANPlusB
_T = TypeVar('_T')
_P = ParamSpec('_P')
_Decorator = Callable[[Callable[_P, _T]], Callable[_P, _T]]
newlines = ['\r\n', '\r', '\n', '\f']
whitespace = ['\t', '\x20'] + newlines
blank = [''] + whitespace
def join(whatever: Iterable[Any]) -> str:
 return ''.join(map(str, whatever))
# Originally from https://stackoverflow.com/a/70312417
def examples(
 parameter_sets: Iterable[ParameterSet | tuple[Any, ...] | Any]
) -> _Decorator[_P, _T]:
 parameter_sets = list(parameter_sets)
 
 def inner(test_case: Callable[_P, _T]) -> Callable[_P, _T]:
 for parameter_set in reversed(parameter_sets):
 if isinstance(parameter_set, ParameterSet):
 parameter_set = parameter_set.values
 
 if not isinstance(parameter_set, tuple):
 parameter_set = tuple([parameter_set])
 
 test_case = example(*parameter_set)(test_case)
 
 return test_case
 
 return inner
@composite
def a_n_plus_b_instances(
 draw: DrawFn,
 steps: SearchStrategy[int] = integers(),
 offsets: SearchStrategy[int] = integers()
) -> SearchStrategy[ANPlusB]:
 return ANPlusB(draw(steps), draw(offsets))

tests/test_alternate_constructors.py

from collections.abc import Callable
from typing import Any, TypeVar
import pytest
from hypothesis import given
from hypothesis.strategies import (
 composite, DrawFn, floats, integers, just, lists,
 one_of, sampled_from, SearchStrategy, tuples
)
from a_n_plus_b import (
 ANPlusB, ComplexWithNonIntegerPart,
 EmptyInput, InputIsNotParsable
)
from . import examples, join, whitespace
_E = TypeVar('_E')
_T = TypeVar('_T')
def _make_complex(example: tuple[int | float, int | float]) -> complex:
 return complex(*example)
def _starts_with_sign(example: tuple[str, Any]) -> bool:
 text, _ = example
 
 return text[0] in ('+', '-')
def _casing_scrambled(text: str) -> SearchStrategy[str]:
 lowercase = text.lower()
 uppercase = text.upper()
 
 substrategies = (sampled_from(chars) for chars in zip(lowercase, uppercase))
 
 return tuples(*substrategies).map(''.join)
@composite
def _variations(draw: DrawFn, text: str) -> str:
 scrambled_text = draw(_casing_scrambled(text))
 
 return draw(with_surrounding_whitespace(scrambled_text))
def _tupled_with(value: _T) -> Callable[[_E], tuple[_E, _T]]:
 def tupled_with_given_value(example: _E) -> tuple[_E, _T]:
 return example, value
 
 return tupled_with_given_value
def _whitespace_sequences() -> SearchStrategy[str]:
 return lists(
 sampled_from(whitespace),
 min_size = 1, max_size = 10
 ).map(''.join)
def whitespace_sequences_or_empty() -> SearchStrategy[str]:
 return _whitespace_sequences() | just('')
def with_surrounding_whitespace(text: str) -> SearchStrategy[str]:
 return tuples(
 whitespace_sequences_or_empty(),
 just(text),
 whitespace_sequences_or_empty()
 ) \
 .map(join)
def _non_integral_floats() -> SearchStrategy[float]:
 return floats().filter(lambda example: not example.is_integer())
def _operators() -> SearchStrategy[str]:
 return sampled_from(['+', '-'])
def _signs() -> SearchStrategy[str]:
 return just('') | _operators()
def _digits() -> SearchStrategy[int]:
 return integers(min_value = 0, max_value = 9)
@composite
def _integers_with_potentially_superfluous_sign(draw: DrawFn) -> str:
 sign = draw(_signs())
 digits = draw(lists(_digits(), min_size = 1, max_size = 10).map(join))
 
 return draw(with_surrounding_whitespace(sign + digits))
@composite
def _a_values(draw: DrawFn) -> str:
 sign = draw(_signs())
 digits = draw(lists(_digits(), max_size = 10).map(join))
 
 return sign + digits
def _b_values() -> SearchStrategy[str]:
 return lists(_digits(), min_size = 1, max_size = 10).map(join)
class ParseANPlusBTestCases:
 
 @staticmethod
 @composite
 def valid(draw: DrawFn) -> tuple[str, tuple[int, int]]:
 a = draw(_a_values())
 n = draw(sampled_from(['n', 'N']))
 operator = draw(_signs())
 
 if not a:
 step = 0
 elif a == '+':
 step = 1
 elif a == '-':
 step = -1
 else:
 step = int(a)
 
 if operator:
 b = draw(_b_values())
 offset = int(f'{operator}{b}')
 else:
 b = ''
 offset = 0
 
 operator = draw(with_surrounding_whitespace(operator))
 text = draw(with_surrounding_whitespace(f'{a}{n}{operator}{b}'))
 
 return text, (step, offset)
 
 @staticmethod
 @composite
 def whitespace_after_a_sign(draw: DrawFn) -> str:
 valid_cases = ParseANPlusBTestCases.valid().filter(_starts_with_sign)
 valid_input = draw(valid_cases)[0]
 
 a_sign, rest = valid_input[0], valid_input[1:]
 invalid_whitespace = draw(_whitespace_sequences())
 
 return f'{a_sign}{invalid_whitespace}{rest}'
 
 @staticmethod
 @composite
 def missing_b(draw: DrawFn) -> str:
 a = draw(_a_values())
 n = draw(sampled_from(['n', 'N']))
 operator = draw(_operators())
 
 operator = draw(with_surrounding_whitespace(operator))
 
 return draw(with_surrounding_whitespace(f'{a}{n}{operator}'))
 
 @staticmethod
 @composite
 def missing_operator(draw: DrawFn) -> str:
 a = draw(_a_values())
 n = draw(sampled_from(['n', 'N']))
 b = draw(_b_values())
 
 b = draw(with_surrounding_whitespace(b))
 
 return draw(with_surrounding_whitespace(f'{a}{n}{b}'))
@given(
 one_of([
 _variations('odd').map(_tupled_with((2, 1))),
 _variations('even').map(_tupled_with((2, 0)))
 ])
)
def test_parse_odd_even(text_and_expected):
 text, expected = text_and_expected
 instance = ANPlusB.parse(text)
 
 assert (instance.step, instance.offset) == expected
@given(_integers_with_potentially_superfluous_sign())
def test_parse_integer(text):
 expected = int(text)
 
 instance = ANPlusB.parse(text)
 
 assert instance.step == 0
 assert instance.offset == expected
@given(whitespace_sequences_or_empty())
def test_parse_empty(text):
 with pytest.raises(EmptyInput):
 ANPlusB.parse(text)
@given(ParseANPlusBTestCases.valid())
@examples([
 (['+3n+2', (3, 2)]),
 (['+4n+0', (4, 0)]),
 (['+6n', (6, 0)]),
 (['+5n-0', (5, 0)]),
 (['+7n-1', (7, -1)]),
 
 (['3n+2', (3, 2)]),
 (['4n+0', (4, 0)]),
 (['6n', (6, 0)]),
 (['5n-0', (5, 0)]),
 (['7n-1', (7, -1)]),
 
 (['+0n+2', (0, 2)]),
 (['+0n+0', (0, 0)]),
 (['+0n', (0, 0)]),
 (['+0n-0', (0, 0)]),
 (['+0n-1', (0, -1)]),
 
 (['0n+2', (0, 2)]),
 (['0n+0', (0, 0)]),
 (['0n', (0, 0)]),
 (['0n-0', (0, 0)]),
 (['0n-1', (0, -1)]),
 
 (['-0n+2', (0, 2)]),
 (['-0n+0', (0, 0)]),
 (['-0n', (0, 0)]),
 (['-0n-0', (0, 0)]),
 (['-0n-1', (0, -1)]),
 
 (['-3n+2', (-3, 2)]),
 (['-4n+0', (-4, 0)]),
 (['-6n', (-6, 0)]),
 (['-5n-0', (-5, 0)]),
 (['-7n-1', (-7, -1)]),
])
def test_parse_a_n_plus_b(text_and_expected):
 text, expected = text_and_expected
 instance = ANPlusB.parse(text)
 
 assert (instance.step, instance.offset) == expected
@given(
 one_of([
 ParseANPlusBTestCases.whitespace_after_a_sign(),
 ParseANPlusBTestCases.missing_b(),
 ParseANPlusBTestCases.missing_operator()
 ])
)
def test_parse_invalid(text):
 with pytest.raises(InputIsNotParsable):
 ANPlusB.parse(text)
@given(
 one_of([
 tuples(integers(), integers()).map(_make_complex),
 integers(),
 integers().map(float)
 ])
)
def test_from_complex(value):
 instance = ANPlusB.from_complex(value)
 expected = (int(value.imag), int(value.real))
 
 assert (instance.step, instance.offset) == expected
@given(
 one_of([
 tuples(_non_integral_floats(), integers()),
 tuples(integers(), _non_integral_floats()),
 tuples(_non_integral_floats(), _non_integral_floats())
 ]) \
 .map(_make_complex)
)
def test_from_complex_invalid(value):
 with pytest.raises(ComplexWithNonIntegerPart):
 ANPlusB.from_complex(value)

tests/test_indices.py

from collections.abc import Iterable
from itertools import product
from typing import cast, Literal
import pytest
from _pytest.mark import ParameterSet
from hypothesis import assume, given
from hypothesis.strategies import (
 booleans, composite, DrawFn, from_type, integers, just,
 one_of, sampled_from, SearchStrategy, tuples
)
from a_n_plus_b import ANPlusB, InvalidNumberOfChildren, InvalidOrder
from . import a_n_plus_b_instances, examples
_Order = Literal['ascending', 'descending', 'default']
_TestCase = tuple[ANPlusB, tuple[int, bool, _Order], Iterable[int]]
def _ascending(values: Iterable[int]) -> Iterable[int]:
 return sorted(values)
def _descending(indices: Iterable[int]) -> Iterable[int]:
 return sorted(indices, reverse = True)
def _from_last(indices: Iterable[int], population: int) -> Iterable[int]:
 return [population - index + 1 for index in indices]
def _sign(value: int, /) -> Literal['negative', 'positive', 'zero']:
 return 'negative' if value < 0 else 'positive' if value > 0 else 'zero'
def _describe(test_case: _TestCase) -> str:
 instance, (population, from_last, order), _ = test_case
 step, offset = instance.step, instance.offset
 
 descriptions = [
 f'{_sign(step)} step',
 f'{_sign(offset)} offset',
 f'from last' if from_last else 'from first',
 f'{order}'
 ]
 
 return ', '.join(descriptions)
def _human_integers(
 min_value: int = -(2 ** 16),
 max_value: int = 2 ** 16,
) -> SearchStrategy[int]:
 return integers(
 min_value = max(-(2 ** 16), min_value),
 max_value = min(2 ** 16, max_value)
 )
def _orders() -> SearchStrategy[_Order]:
 return cast(
 SearchStrategy[_Order],
 sampled_from(['ascending', 'descending', 'default'])
 )
def _non_positive_step_and_offset_test_cases() -> SearchStrategy[_TestCase]:
 return tuples(
 a_n_plus_b_instances(
 integers(max_value = 0),
 integers(max_value = 0)
 ),
 tuples(integers(min_value = 0), booleans(), _orders()),
 just(list[int]())
 )
@composite
def _zero_step_test_cases(draw: DrawFn) -> _TestCase:
 argument_sets = tuples(integers(min_value = 0), booleans(), _orders())
 
 instance = draw(a_n_plus_b_instances(just(0), integers()))
 population, from_last, order = draw(argument_sets)
 
 b = instance.offset
 
 index = population - b + 1 if from_last else b
 expected = [index] if 1 <= b <= population else list[int]()
 
 return instance, (population, from_last, order), expected
@composite
def _non_positive_step_zero_offset_test_cases(draw: DrawFn) -> _TestCase:
 argument_sets = tuples(integers(min_value = 0), booleans(), _orders())
 
 instance = draw(a_n_plus_b_instances(integers(max_value = -1), just(0)))
 arguments = draw(argument_sets)
 
 return instance, arguments, list[int]()
def _test_case_group(
 instance: ANPlusB,
 population: int,
 base_case_expected: Iterable[int]
) -> list[ParameterSet]:
 test_cases = []
 from_last_and_order_matrix = product(
 [False, True],
 ['default', 'ascending', 'descending']
 )
 
 for from_last, order in from_last_and_order_matrix:
 expected = base_case_expected
 
 if from_last:
 expected = _from_last(base_case_expected, population)
 
 if order == 'ascending':
 expected = _ascending(expected)
 elif order == 'descending':
 expected = _descending(expected)
 
 test_case = (instance, (population, from_last, order), expected)
 test_cases.append(
 pytest.param(test_case, id = _describe(test_case))
 )
 
 return test_cases
@given(
 one_of([
 _non_positive_step_and_offset_test_cases(),
 _zero_step_test_cases(),
 _non_positive_step_zero_offset_test_cases()
 ])
)
@examples([
 *_test_case_group(ANPlusB(3, 0), 100, list(range(3, 101, 3))),
 
 *_test_case_group(ANPlusB(-2, 6), 10, [6, 4, 2]),
 *_test_case_group(ANPlusB(-1, 4), 8, [4, 3, 2, 1]),
 *_test_case_group(ANPlusB(-3, 8), 18, [8, 5, 2]),
 
 *_test_case_group(ANPlusB(4, -5), 20, [3, 7, 11, 15, 19]),
 *_test_case_group(ANPlusB(5, -2), 12, [3, 8]),
 
 *_test_case_group(ANPlusB(2, 1), 15, [1, 3, 5, 7, 9, 11, 13, 15]),
 *_test_case_group(ANPlusB(3, 1), 10, [1, 4, 7, 10]),
 *_test_case_group(ANPlusB(1, 4), 11, [4, 5, 6, 7, 8, 9, 10, 11])
])
def test_indices(instance_arguments_expected: _TestCase):
 instance, arguments, expected = instance_arguments_expected
 population, from_last, order = arguments
 
 indices = instance.indices(population, from_last = from_last, order = order)
 
 assert list(indices) == expected
@given(
 a_n_plus_b_instances(),
 integers(), booleans(), from_type(str)
)
def test_indices_invalid_order(instance, population, from_last, order):
 assume(order not in ('ascending', 'descending', 'default'))
 
 with pytest.raises(InvalidOrder):
 instance.indices(population, from_last = from_last, order = order)
@given(
 a_n_plus_b_instances(),
 integers(max_value = -1), booleans(), _orders()
)
def test_indices_invalid_number_of_children(
 instance, population, from_last, order
):
 with pytest.raises(InvalidNumberOfChildren):
 instance.indices(population, from_last = from_last, order = order)

tests/test_other_methods.py

import pytest
from hypothesis import given, infer
from hypothesis.strategies import from_type, integers
from a_n_plus_b import ANPlusB, IncorrectUseOfConstructor
from . import a_n_plus_b_instances
_EqTestCase = tuple[ANPlusB, '_ANPlusBSubclass', bool]
class _ANPlusBSubclass(ANPlusB):
 pass
@given(integers(), integers())
def test_construction(step, offset):
 instance = ANPlusB(step, offset)
 
 assert instance.step == step
 assert instance.offset == offset
@given(integers())
def test_construction_single_argument(offset):
 instance = ANPlusB(offset)
 
 assert instance.step == 0
 assert instance.offset == offset
@given(infer)
def test_construction_invalid(text: str):
 with pytest.raises(IncorrectUseOfConstructor):
 ANPlusB(text) # noqa
@pytest.mark.parametrize(('instance', 'expected'), [
 (ANPlusB(0, -2), '-2'),
 (ANPlusB(0, 0), '0'),
 (ANPlusB(0, 2), '2'),
 
 (ANPlusB(1, -3), 'n-3'),
 (ANPlusB(1, 0), 'n'),
 (ANPlusB(1, 3), 'n+3'),
 
 (ANPlusB(-1, -4), '-n-4'),
 (ANPlusB(-1, 0), '-n'),
 (ANPlusB(-1, 4), '-n+4'),
 
 (ANPlusB(3, 4), '3n+4'),
 (ANPlusB(3, 0), '3n'),
 (ANPlusB(3, -5), '3n-5'),
 
 (ANPlusB(-4, 5), '-4n+5'),
 (ANPlusB(-4, 0), '-4n'),
 (ANPlusB(-4, -6), '-4n-6')
])
def test_str(instance, expected):
 assert str(instance) == expected
 assert repr(instance) == f'{ANPlusB.__name__}({expected})'
@given(a_n_plus_b_instances())
def test_values(instance):
 a, b = instance.step, instance.offset
 
 for index, value in zip(range(10), instance.values()):
 assert value == a * index + b
@given(a_n_plus_b_instances(), integers(min_value = 0))
def test_values_contain_getitem(instance, index):
 a, b = instance.step, instance.offset
 value = instance.values()[index]
 
 assert value == a * index + b
 assert value in instance.values()
@given(
 a_n_plus_b_instances(),
 from_type(object).filter(lambda o: not isinstance(o, int))
)
def test_values_not_contain(instance, item):
 assert item not in instance.values()
@given(a_n_plus_b_instances(), integers(max_value = -1))
def test_getitem_invalid(instance, index):
 with pytest.raises(IndexError):
 _ = instance.values()[index]
@given(a_n_plus_b_instances())
def test_eq(this):
 a, b = this.step, this.offset
 that = ANPlusB(a, b)
 
 assert this == that
 assert hash(this) == hash(that) == hash((a, b))
@pytest.mark.parametrize(('this', 'that', 'expected'), [
 (ANPlusB(-2, -3), ANPlusB(-2, -3), True),
 (ANPlusB(-1, 0), ANPlusB(-1, 0), True),
 (ANPlusB(0, -5), ANPlusB(0, -5), True),
 (_ANPlusBSubclass(-4, -3), _ANPlusBSubclass(-4, -3), True),
 (_ANPlusBSubclass(-1, -2), _ANPlusBSubclass(-1, -2), True),
 (_ANPlusBSubclass(0, 0), _ANPlusBSubclass(0, 0), True),
 (ANPlusB(0, 0), _ANPlusBSubclass(0, 0), True),
 (_ANPlusBSubclass(0, 0), ANPlusB(0, 0), True),
 (ANPlusB(3, 4), ANPlusB(4, 3), False),
 (_ANPlusBSubclass(3, 4), _ANPlusBSubclass(4, 3), False),
 (ANPlusB(-2, -3), ANPlusB(-4, -5), False),
 (_ANPlusBSubclass(-1, -2), _ANPlusBSubclass(-3, -4), False),
])
def test_eq_subclass(this, that, expected):
 assert (this == that) is expected
lang-py

AltStyle によって変換されたページ (->オリジナル) /