From ef466361ce178c8d677978565e33c3c79fca68f4 Mon Sep 17 00:00:00 2001 From: Avi Kaplan Date: Thu, 30 Jul 2026 16:04:34 +0200 Subject: [PATCH 1/2] Refactor integer validation utilities Replaces the separate optional integer validator with configurable integer validation, and updates range validation to use the new API. --- calculus/utils.py | 67 +++++++++++++++++---------------- examples/rademacher_sequence.py | 4 +- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/calculus/utils.py b/calculus/utils.py index 4ab2cd2..021ada2 100644 --- a/calculus/utils.py +++ b/calculus/utils.py @@ -1,16 +1,12 @@ """Common utility functions for validating inputs across the project. Functions: - validate_int: Validate that a value is an int and not a boolean. - validate_optional_int: Validate int or None, rejecting booleans. + validate_callable: Validate that a value is callable. + validate_int: Validate integer values, with configurable options. + validate_range: Validate range arguments. """ -__all__ = [ - "validate_callable", - "validate_int", - "validate_optional_int", - "validate_range", -] +__all__ = ["validate_callable", "validate_int", "validate_range"] def validate_callable(value: object) -> None: @@ -26,38 +22,43 @@ def validate_callable(value: object) -> None: raise TypeError(f"'{type(value).__name__}' object is not callable") -def validate_int(value: int, name: str = "value") -> None: - """Validate that a value is an integer and not a boolean. +def validate_int( + value: int | None, + name: str = "value", + allow_none: bool = False, + allow_bool: bool = False, +) -> None: + """Validate that a value is an integer. + + By default, only integers are accepted. The accepted values can be + extended to include None or boolean values using the corresponding + flags. Args: - value (int): The value to validate. + value (int | None): The value to validate. name (str): The variable name for error messages. + allow_none (bool): Whether None is accepted. Defaults to False. + allow_bool (bool): Whether boolean values are accepted. Defaults + to False. Raises: - TypeError: If ``value`` is a bool or not an instance of int. + TypeError: If ``value`` is not of an accepted type. """ - if not isinstance(value, int) or isinstance(value, bool): + if value is None: + if not allow_none: + raise TypeError( + f"'{name}' must be an integer, but got NoneType." + ) + elif isinstance(value, bool): + if not allow_bool: + raise TypeError( + f"'{name}' must be an integer, but got bool." + ) + elif not isinstance(value, int): raise TypeError( f"'{name}' must be an integer, but got {type(value).__name__}." ) - -def validate_optional_int(value: int | None, name: str = "value") -> None: - """Validate that a value is an integer or None, rejecting booleans. - - If not None, it delegates to validate_int for strict checking. - - Args: - value (int | None): The value to check. - name (str): The variable name for error messages. - - Raises: - TypeError: If ``value`` is neither None nor a valid integer. - """ - if value is not None: - validate_int(value, name=name) - - def validate_range( start: int | None, stop: int | None, @@ -75,8 +76,8 @@ def validate_range( integer or None. ValueError: If ``step`` is zero. """ - validate_optional_int(start, "start") - validate_optional_int(stop, "stop") - validate_optional_int(step, "step") + validate_int(start, "start", allow_none=True, allow_bool=True) + validate_int(stop, "stop", allow_none=True, allow_bool=True) + validate_int(step, "step", allow_none=True, allow_bool=True) if step is not None and step == 0: raise ValueError(f"step ({step}) cannot be zero") diff --git a/examples/rademacher_sequence.py b/examples/rademacher_sequence.py index 7ddc36a..0873eda 100644 --- a/examples/rademacher_sequence.py +++ b/examples/rademacher_sequence.py @@ -13,7 +13,7 @@ from calculus.sequence import INFINITY, Intfinity, Rule from calculus.numeric_sequence import NumericSequence, Real -from calculus.utils import validate_callable, validate_optional_int +from calculus.utils import validate_callable, validate_int #======================================================================= # Rademacher Sequence @@ -79,7 +79,7 @@ def __init__( is not in ``sequence.FIRST_INDEX_OPTIONS``. """ if random_rule is None: - validate_optional_int(seed, "seed") + validate_int(seed, "seed", allow_none=True) self._random_rule = self._Rule(random.Random(seed)) else: validate_callable(random_rule) From fb6de08150f538017a0a835ba488447fc595032b Mon Sep 17 00:00:00 2001 From: Avi Kaplan Date: Thu, 30 Jul 2026 16:35:53 +0200 Subject: [PATCH 2/2] Add utility functions test suite --- README.md | 3 +- TODO.md | 4 --- tests/test_utils.py | 81 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 tests/test_utils.py diff --git a/README.md b/README.md index bf5506b..568d4ee 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,8 @@ print(4 * leibniz[1000]) │ ├── test_numeric_sequence.py # Pytest test suite for NumericSequence │ ├── test_recurrence.py # Pytest test suite for Recurrence │ ├── test_sequence.py # Pytest test suite for Sequence -│ └── test_series.py # Pytest test suite for Series +│ ├── test_series.py # Pytest test suite for Series +│ └── test_utils.py # Pytest test suite for utility functions ├── .gitignore ├── LICENSE ├── README.md diff --git a/TODO.md b/TODO.md index 5fa682d..67c1610 100644 --- a/TODO.md +++ b/TODO.md @@ -13,10 +13,6 @@ - Identify opportunities to use decorators. -## Testing - -- Add a unit test suite for `utils.py`. - ## Documentation - Add `CHANGELOG.md`. diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..da9dff3 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,81 @@ +"""Tests for utils. + +Run with: + pytest tests/test_utils.py -v +""" +import pytest + +from calculus.utils import validate_callable, validate_int, validate_range + +# -- CALLABLE VALIDATION + +def test_validate_callable_accepts_callable() -> None: + validate_callable(lambda n: n) + + +def test_validate_callable_rejects_non_callable() -> None: + with pytest.raises(TypeError): + validate_callable("not callable") + + +# -- INTEGER VALIDATION + +def test_validate_int_accepts_int() -> None: + validate_int(5) + + +def test_validate_int_rejects_none() -> None: + with pytest.raises(TypeError): + validate_int(None) + + +def test_validate_int_rejects_bool() -> None: + with pytest.raises(TypeError): + validate_int(True) + + +def test_validate_int_rejects_noninteger() -> None: + with pytest.raises(TypeError): + validate_int("5") + + +def test_validate_int_accepts_none_when_allowed() -> None: + validate_int(None, allow_none=True) + + +def test_validate_int_accepts_bool_when_allowed() -> None: + validate_int(True, allow_bool=True) + + +# -- RANGE VALIDATION + +def test_validate_range_accepts_valid_range() -> None: + validate_range(1, 10, 2) + + +def test_validate_range_accepts_all_none() -> None: + validate_range(None, None, None) + + +def test_validate_range_accepts_bool() -> None: + validate_range(True, False, True) + + +def test_validate_range_rejects_noninteger_start() -> None: + with pytest.raises(TypeError): + validate_range("1", 10, 2) + + +def test_validate_range_rejects_noninteger_stop() -> None: + with pytest.raises(TypeError): + validate_range(1, "10", 2) + + +def test_validate_range_rejects_noninteger_step() -> None: + with pytest.raises(TypeError): + validate_range(1, 10, "2") + + +def test_validate_range_rejects_zero_step() -> None: + with pytest.raises(ValueError): + validate_range(1, 10, 0)