punit 1.7.3__tar.gz → 1.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {punit-1.7.3/src/pUnit.egg-info → punit-1.9.0}/PKG-INFO +1 -1
  2. {punit-1.7.3 → punit-1.9.0}/pyproject.toml +1 -1
  3. {punit-1.7.3 → punit-1.9.0/src/pUnit.egg-info}/PKG-INFO +1 -1
  4. {punit-1.7.3 → punit-1.9.0}/src/pUnit.egg-info/SOURCES.txt +2 -0
  5. {punit-1.7.3 → punit-1.9.0}/src/punit/TestResult.py +23 -3
  6. {punit-1.7.3 → punit-1.9.0}/src/punit/__init__.py +7 -4
  7. {punit-1.7.3 → punit-1.9.0}/src/punit/__main__.py +23 -7
  8. punit-1.9.0/src/punit/assertions/__init__.py +28 -0
  9. {punit-1.7.3 → punit-1.9.0}/src/punit/assertions/collections.py +70 -11
  10. punit-1.9.0/src/punit/assertions/exceptions.py +87 -0
  11. {punit-1.7.3 → punit-1.9.0}/src/punit/assertions/numeric.py +103 -19
  12. {punit-1.7.3 → punit-1.9.0}/src/punit/assertions/strings.py +54 -6
  13. {punit-1.7.3 → punit-1.9.0}/src/punit/cli.py +21 -1
  14. {punit-1.7.3 → punit-1.9.0}/src/punit/facts/Fact.py +49 -0
  15. {punit-1.7.3 → punit-1.9.0}/src/punit/facts/FactManager.py +8 -8
  16. punit-1.9.0/src/punit/facts/__init__.py +35 -0
  17. {punit-1.7.3 → punit-1.9.0}/src/punit/filters/FilterManager.py +6 -6
  18. {punit-1.7.3 → punit-1.9.0}/src/punit/reports/HtmlReportGenerator.py +8 -1
  19. {punit-1.7.3 → punit-1.9.0}/src/punit/reports/JUnitReportGenerator.py +7 -2
  20. {punit-1.7.3 → punit-1.9.0}/src/punit/reports/JsonReportGenerator.py +5 -1
  21. punit-1.9.0/src/punit/results/__init__.py +22 -0
  22. punit-1.9.0/src/punit/results/fails.py +87 -0
  23. {punit-1.7.3 → punit-1.9.0}/src/punit/runner.py +40 -6
  24. {punit-1.7.3 → punit-1.9.0}/src/punit/setups/Setup.py +66 -2
  25. {punit-1.7.3 → punit-1.9.0}/src/punit/setups/SetupManager.py +2 -2
  26. punit-1.9.0/src/punit/setups/__init__.py +43 -0
  27. {punit-1.7.3 → punit-1.9.0}/src/punit/teardowns/Teardown.py +58 -2
  28. {punit-1.7.3 → punit-1.9.0}/src/punit/teardowns/TeardownManager.py +2 -2
  29. punit-1.9.0/src/punit/teardowns/__init__.py +42 -0
  30. {punit-1.7.3 → punit-1.9.0}/src/punit/theories/Theory.py +62 -0
  31. {punit-1.7.3 → punit-1.9.0}/src/punit/theories/TheoryManager.py +3 -3
  32. punit-1.9.0/src/punit/theories/__init__.py +33 -0
  33. punit-1.9.0/src/punit/traits/Trait.py +88 -0
  34. punit-1.9.0/src/punit/traits/__init__.py +51 -0
  35. punit-1.7.3/src/punit/assertions/__init__.py +0 -12
  36. punit-1.7.3/src/punit/assertions/exceptions.py +0 -37
  37. punit-1.7.3/src/punit/facts/__init__.py +0 -11
  38. punit-1.7.3/src/punit/setups/__init__.py +0 -11
  39. punit-1.7.3/src/punit/teardowns/__init__.py +0 -11
  40. punit-1.7.3/src/punit/theories/__init__.py +0 -11
  41. punit-1.7.3/src/punit/traits/Trait.py +0 -32
  42. punit-1.7.3/src/punit/traits/__init__.py +0 -11
  43. {punit-1.7.3 → punit-1.9.0}/LICENSE +0 -0
  44. {punit-1.7.3 → punit-1.9.0}/README.md +0 -0
  45. {punit-1.7.3 → punit-1.9.0}/setup.cfg +0 -0
  46. {punit-1.7.3 → punit-1.9.0}/src/pUnit.egg-info/dependency_links.txt +0 -0
  47. {punit-1.7.3 → punit-1.9.0}/src/pUnit.egg-info/entry_points.txt +0 -0
  48. {punit-1.7.3 → punit-1.9.0}/src/pUnit.egg-info/requires.txt +0 -0
  49. {punit-1.7.3 → punit-1.9.0}/src/pUnit.egg-info/top_level.txt +0 -0
  50. {punit-1.7.3 → punit-1.9.0}/src/punit/discovery/TestModuleDiscovery.py +0 -0
  51. {punit-1.7.3 → punit-1.9.0}/src/punit/discovery/__init__.py +0 -0
  52. {punit-1.7.3 → punit-1.9.0}/src/punit/filters/Filter.py +0 -0
  53. {punit-1.7.3 → punit-1.9.0}/src/punit/filters/__init__.py +0 -0
  54. {punit-1.7.3 → punit-1.9.0}/src/punit/metadata/CallableMetadata.py +0 -0
  55. {punit-1.7.3 → punit-1.9.0}/src/punit/metadata/__init__.py +0 -0
  56. {punit-1.7.3 → punit-1.9.0}/src/punit/py.typed +0 -0
  57. {punit-1.7.3 → punit-1.9.0}/src/punit/reports/__init__.py +0 -0
  58. {punit-1.7.3 → punit-1.9.0}/src/punit/traits/TraitManager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pUnit
3
- Version: 1.7.3
3
+ Version: 1.9.0
4
4
  Summary: A modernized unit-test framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pUnit"
3
- version = "1.7.3"
3
+ version = "1.9.0"
4
4
  description = "A modernized unit-test framework for Python."
5
5
  keywords = ["test", "unittest", "unit-test", "xUnit", "nUnit", "pytest"]
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pUnit
3
- Version: 1.7.3
3
+ Version: 1.9.0
4
4
  Summary: A modernized unit-test framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License: MIT License
@@ -32,6 +32,8 @@ src/punit/reports/HtmlReportGenerator.py
32
32
  src/punit/reports/JUnitReportGenerator.py
33
33
  src/punit/reports/JsonReportGenerator.py
34
34
  src/punit/reports/__init__.py
35
+ src/punit/results/__init__.py
36
+ src/punit/results/fails.py
35
37
  src/punit/setups/Setup.py
36
38
  src/punit/setups/SetupManager.py
37
39
  src/punit/setups/__init__.py
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
4
  import sys
5
- from typing import Any, TextIO, Optional, cast
5
+ from typing import Any, Optional, TextIO
6
6
 
7
7
 
8
8
  class TextIOCapture:
@@ -48,6 +48,7 @@ class TestResult:
48
48
  __exception: Exception | None
49
49
  __file_name: str | None
50
50
  __host_name: str | None
51
+ __expected_failure_reason: str | None
51
52
  __is_success: bool | None
52
53
  __module_name: str | None
53
54
  __package_name: str | None
@@ -63,6 +64,7 @@ class TestResult:
63
64
  self.__exception = None
64
65
  self.__file_name = None
65
66
  self.__host_name = None
67
+ self.__expected_failure_reason = None
66
68
  self.__is_success = None
67
69
  self.__module_name = None
68
70
  self.__package_name = None
@@ -85,6 +87,14 @@ class TestResult:
85
87
  def class_name(self, value: Optional[str]) -> None:
86
88
  self.__class_name = value
87
89
 
90
+ @property
91
+ def expected_failure_reason(self) -> str | None:
92
+ return self.__expected_failure_reason
93
+
94
+ @expected_failure_reason.setter
95
+ def expected_failure_reason(self, value: str | None) -> None:
96
+ self.__expected_failure_reason = value
97
+
88
98
  @property
89
99
  def exception(self) -> Exception | None:
90
100
  return self.__exception
@@ -109,6 +119,16 @@ class TestResult:
109
119
  def host_name(self, value: str) -> None:
110
120
  self.__host_name = value
111
121
 
122
+ @property
123
+ def is_expected_failure(self) -> bool:
124
+ """Deprecated: use ``expected_failure_reason is not None`` instead."""
125
+ return self.expected_failure_reason is not None
126
+
127
+ @is_expected_failure.setter
128
+ def is_expected_failure(self, value: bool) -> None:
129
+ # Deprecated no-op; consumers should set expected_failure_reason directly.
130
+ pass
131
+
112
132
  @property
113
133
  def is_success(self) -> bool:
114
134
  return False if self.__is_success is None else self.__is_success
@@ -118,8 +138,8 @@ class TestResult:
118
138
  self.__is_success = value
119
139
 
120
140
  @property
121
- def module_name(self) -> str:
122
- return cast(str, self.__module_name)
141
+ def module_name(self) -> str | None:
142
+ return self.__module_name
123
143
 
124
144
  @module_name.setter
125
145
  def module_name(self, value: str) -> None:
@@ -1,8 +1,11 @@
1
1
  # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
+ """pUnit -- a modernized unit-testing framework for Python."""
5
+
4
6
  from .assertions import collections, exceptions, numeric, strings
5
7
  from .facts import fact
8
+ from .results import fails
6
9
  from .setups import Setup, setup
7
10
  from .teardowns import Teardown, teardown
8
11
  from .theories import theory, inlinedata
@@ -12,8 +15,8 @@ from .assertions.exceptions import raises
12
15
  from .assertions.numeric import approx
13
16
 
14
17
 
15
- __version__ = '1.7.3'
16
- __commit__ = 'a403690'
18
+ __version__ = '1.9.0'
19
+ __commit__ = '69c5f5e'
17
20
  __all__ = [
18
21
  '__version__', '__commit__',
19
22
  'assertions',
@@ -22,9 +25,9 @@ __all__ = [
22
25
  'numeric', 'approx',
23
26
  'strings',
24
27
  'fact',
25
- 'metadata',
26
28
  'setup', 'Setup',
27
29
  'teardown', 'Teardown',
28
30
  'theory', 'inlinedata',
29
- 'trait'
31
+ 'trait',
32
+ 'fails',
30
33
  ]
@@ -9,10 +9,12 @@ from pathlib import Path
9
9
 
10
10
  from .cli import CommandLineInterface
11
11
  from .discovery import TestModuleDiscovery
12
+ from .facts.FactManager import FactManager
12
13
  from .reports import HtmlReportGenerator, JUnitReportGenerator, JsonReportGenerator
13
14
  from .runner import TestRunner
14
15
  from .setups.SetupManager import SetupManager
15
16
  from .teardowns.TeardownManager import TeardownManager
17
+ from .theories.TheoryManager import TheoryManager
16
18
 
17
19
 
18
20
  async def async_main() -> None:
@@ -32,13 +34,27 @@ async def async_main() -> None:
32
34
  sys.path.append(src_path)
33
35
  if cwd_path not in sys.path:
34
36
  sys.path.append(cwd_path)
35
- test_module_discovery = TestModuleDiscovery(
36
- os.path.join(cli.workdir, cli.test_package_name),
37
- cli.includePatterns,
38
- cli.excludePatterns,
39
- cli)
40
- test_module_discovery.discover()
41
- test_runner = TestRunner(cli.test_package_name, test_module_discovery.filenames, cli)
37
+ test_runner: TestRunner
38
+ if cli.files:
39
+ dotnames = []
40
+ for filepath in cli.files:
41
+ rel = os.path.relpath(os.path.abspath(filepath), cli.workdir)
42
+ dotnames.append(
43
+ rel[:-3].replace('\\', '/').replace('/', '.')
44
+ )
45
+ FactManager.instance().excludeTraits = cli.excludeTraits
46
+ FactManager.instance().includeTraits = cli.includeTraits
47
+ TheoryManager.instance().excludeTraits = cli.excludeTraits
48
+ TheoryManager.instance().includeTraits = cli.includeTraits
49
+ test_runner = TestRunner('', dotnames, cli)
50
+ else:
51
+ test_module_discovery = TestModuleDiscovery(
52
+ os.path.join(cli.workdir, cli.test_package_name),
53
+ cli.includePatterns,
54
+ cli.excludePatterns,
55
+ cli)
56
+ test_module_discovery.discover()
57
+ test_runner = TestRunner(cli.test_package_name, test_module_discovery.filenames, cli)
42
58
  results = await test_runner.run()
43
59
  total_time = time.time() - start_time
44
60
  failure_count = 0
@@ -0,0 +1,28 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Assertion helpers for pUnit tests.
5
+
6
+ With pUnit, assertions are typically performed using Python's ``assert`` statement.
7
+ This module provides several helper submodules that offer additional assertion
8
+ capabilities beyond the built-in ``assert`` keyword.
9
+
10
+ Sub-modules
11
+ -----------
12
+
13
+ * :py:mod:`punit.assertions.collections`; helpers for comparing sequences and collections.
14
+ * :py:mod:`punit.assertions.exceptions`; helpers for asserting raised exceptions.
15
+ * :py:mod:`punit.assertions.numeric`; helpers for approximate numeric comparisons.
16
+ * :py:mod:`punit.assertions.strings`; helpers for string comparisons and checks.
17
+
18
+ """
19
+
20
+ from . import collections, exceptions, numeric, strings
21
+
22
+
23
+ __all__ = [
24
+ 'collections',
25
+ 'exceptions',
26
+ 'numeric',
27
+ 'strings'
28
+ ]
@@ -1,6 +1,13 @@
1
1
  # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
+ """Collection assertion helpers for comparing sequences and containers.
5
+
6
+ Provides utilities to check sequence equality, length bounds, and emptiness
7
+ beyond Python's built-in ``assert`` statement.
8
+
9
+ """
10
+
4
11
  from typing import Any, Callable, Optional, Sequence, cast
5
12
 
6
13
 
@@ -10,11 +17,30 @@ def are_same(actual: Sequence[Any] | set[Any] | dict[Any, Any] | None, expected:
10
17
 
11
18
  Can optionally specify *sort* so that differences in order do not cause failure.
12
19
 
13
- :param actual: The sequence to check.
14
- :param expected: The sequence to compare against.
15
- :param sort: Sort sequences before performing comparisons.
16
- :param sort_function: Custom function to use when sorting.
17
- :returns bool: True if the sequences contain the same elements in the same order, False otherwise.
20
+ Args:
21
+ actual: The sequence to check.
22
+ expected: The sequence to compare against.
23
+ sort: Sort sequences before performing comparisons.
24
+ sort_function: Custom function to use when sorting.
25
+
26
+ Returns:
27
+ True if the sequences contain the same elements in the same order, False otherwise
28
+
29
+ Example
30
+ -------
31
+
32
+ .. code-block:: python
33
+
34
+ from punit import collections
35
+
36
+ a = [1, 2, 3]
37
+ b = [1, 2, 3]
38
+ c = [3, 2, 1]
39
+
40
+ assert collections.are_same(a, b)
41
+ assert not collections.are_same(b, c)
42
+ assert collections.are_same(None, None)
43
+ assert not collections.are_same(a, None)
18
44
  """
19
45
  if actual is expected:
20
46
  return True
@@ -52,12 +78,31 @@ def are_same(actual: Sequence[Any] | set[Any] | dict[Any, Any] | None, expected:
52
78
 
53
79
  def has_length(actual: Sequence[Any] | set[Any] | dict[Any, Any] | None, min: Optional[int] = None, max: Optional[int] = None) -> bool:
54
80
  """
55
- Check if actual value has the expected number of elements.
81
+ Check if actual value's length falls within the inclusive range [min, max].
82
+
83
+ Args:
84
+ actual: The sequence (or collection) to check.
85
+ min: Inclusive lower bound on length (``len(actual) >= min``).
86
+ max: Inclusive upper bound on length (``len(actual) <= max``).
56
87
 
57
- :param actual: The actual value to check.
58
- :param int|None expected: The expected number of elements
88
+ Returns:
89
+ True if the length satisfies the bounds, False otherwise.
90
+ When actual is None, returns True only when both bounds are effectively zero.
59
91
 
60
- :returns bool: True if the sequence has exactly the expected number of elements, False otherwise
92
+ Example
93
+ -------
94
+
95
+ .. code-block:: python
96
+
97
+ from punit import collections
98
+
99
+ a = [1]
100
+ b = [1, 2]
101
+
102
+ assert collections.has_length(a, min=1)
103
+ assert collections.has_length(b, min=2)
104
+ assert collections.has_length([1, 2, 3], max=3)
105
+ assert not collections.has_length(a, min=2)
61
106
  """
62
107
  if actual is None:
63
108
  if ((min is None or min == 0) and (max is None or max == 0)):
@@ -74,8 +119,22 @@ def is_none_or_empty(actual: Sequence[Any] | set[Any] | dict[Any, Any] | None) -
74
119
  """
75
120
  Check if actual value is None or empty.
76
121
 
77
- :param actual: The actual to check.
78
- :returns: True if the value is None or empty, False otherwise.
122
+ Args:
123
+ actual: The sequence to check.
124
+
125
+ Returns:
126
+ True if the value is None or empty, False otherwise
127
+
128
+ Example
129
+ -------
130
+
131
+ .. code-block:: python
132
+
133
+ from punit import collections
134
+
135
+ assert not collections.is_none_or_empty([1, 2, 3])
136
+ assert collections.is_none_or_empty([])
137
+ assert collections.is_none_or_empty(None)
79
138
  """
80
139
  if actual is None:
81
140
  return True
@@ -0,0 +1,87 @@
1
+ # SPDX-FileCopyrightText: © 2024 Shaun Wilson
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Exception assertion helpers for verifying raised exceptions.
5
+
6
+ Provides the ``raises`` class which can verify that a callable raises an
7
+ exception of the expected type using either Python 3.11+ generic syntax or
8
+ an explicit ``expect`` keyword argument.
9
+
10
+ Example
11
+ -------
12
+
13
+ .. code-block:: python
14
+
15
+ from punit import exceptions
16
+
17
+ class MyException(Exception):
18
+ pass
19
+
20
+ def raises_exception():
21
+ raise MyException()
22
+
23
+ assert exceptions.raises[MyException](raises_exception)
24
+ assert exceptions.raises[MyException](raises_exception, exact=True)
25
+
26
+ """
27
+
28
+ from typing import Any, Callable, Generic, Optional, TypeVar, cast, get_args
29
+
30
+
31
+ TError = TypeVar('TError', bound=Exception)
32
+
33
+
34
+ class raises(Generic[TError]):
35
+ """Assert that a callable raises an expected exception.
36
+
37
+ Use ``raises`` to verify that a function, method, or lambda expression
38
+ raises an exception of the expected type. Two syntaxes are supported:
39
+
40
+ * Python >=3.11 generic syntax: ``raises[MyException](action)``
41
+ * Explicit expect argument (for <=3.11): ``raises(action, expect=MyException)``
42
+
43
+ The ``exact`` parameter controls whether the raised exception must be an exact
44
+ type match (not a subclass) of the expected type.
45
+
46
+ Example
47
+ -------
48
+
49
+ .. code-block:: python
50
+
51
+ class MyException(Exception):
52
+ pass
53
+
54
+ def raises_exception():
55
+ raise MyException()
56
+
57
+ assert raises[MyException](raises_exception)
58
+ assert not raises[ValueError](raises_exception)
59
+ assert raises[MyException](raises_exception, exact=True)
60
+ """
61
+
62
+ __action: Callable[[], Any]
63
+ __exact: bool
64
+ __expect: Optional[TError | type]
65
+
66
+ def __init__(self, action: Callable[[], Any], *, exact: bool = False, expect: Optional[TError | type] = None) -> None:
67
+ self.__action = action
68
+ self.__exact = exact
69
+ self.__expect = expect
70
+
71
+ def __bool__(self) -> bool:
72
+ try:
73
+ self.__action()
74
+ except BaseException as ex:
75
+ expected = self.__expect
76
+ if expected is None and hasattr(self, '__orig_class__') is not None:
77
+ expected = get_args(getattr(self, '__orig_class__'))[0]
78
+ if self.__exact:
79
+ return type(ex) is expected
80
+ elif expected is not None:
81
+ return issubclass(type(ex), cast(Any, expected))
82
+ return False
83
+
84
+
85
+ __all__ = [
86
+ 'raises'
87
+ ]
@@ -1,6 +1,35 @@
1
1
  # SPDX-FileCopyrightText: © 2026 Shaun Wilson
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
+ """Numeric assertion helpers for approximate comparisons and checks.
5
+
6
+ Provides the ``approx`` class (inspired by pytest.approx) for directional
7
+ tolerance-based numeric comparisons, plus standalone helpers like
8
+ ``isclose``, ``isnan``, ``isinfinite``, and ``percentage``.
9
+
10
+ The ``approx`` class supports natural Python comparison syntax with
11
+ directional (one-sided) tolerance:
12
+
13
+ * ``x == approx(expected)``; approximately equal (bidirectional tolerance)
14
+ * ``x > approx(threshold)``; strictly greater than (tolerance extends below)
15
+ * ``x >= approx(threshold)``; at least the threshold (tolerance extends below)
16
+ * ``x < approx(threshold)``; strictly less than (tolerance extends above)
17
+ * ``x <= approx(threshold)``; at most the threshold (tolerance extends above)
18
+
19
+ Example
20
+ -------
21
+
22
+ .. code-block:: python
23
+
24
+ from punit.assertions.numeric import approx, isclose, isnan
25
+
26
+ assert 0.1 + 0.2 == approx(0.3)
27
+ assert isclose(1 + 2j, 1.0 + 2.0j)
28
+ assert not isclose(3, 3.000000001)
29
+ assert isnan(float('nan'))
30
+
31
+ """
32
+
4
33
  from __future__ import annotations
5
34
 
6
35
  from decimal import Decimal
@@ -38,7 +67,7 @@ def _real_expected(base: '_ApproxComparator') -> float:
38
67
  """
39
68
  Return *base._expected* as float for real-only operations.
40
69
 
41
- Raises ``TypeError`` when the expected value is complex — those
70
+ Raises ``TypeError`` when the expected value is complex; those
42
71
  comparisons belong to ``isclose`` / ``_approximately_equal``.
43
72
  """
44
73
  if isinstance(base._expected, complex):
@@ -61,21 +90,26 @@ def isclose(
61
90
  complex inputs the real and imaginary parts are compared independently using the
62
91
  same tolerances.
63
92
 
64
- :param a: First numeric value.
65
- :param b: Second numeric value.
66
- :param rel_tol: Relative tolerance (default 1e-9).
67
- :param abs_tol: Absolute tolerance (default 0.0).
68
- :returns: True if ``|a - b| <= max(rel_tol * |b|, abs_tol)`` for real values, or
93
+ Args:
94
+ a: First numeric value.
95
+ b: Second numeric value.
96
+ rel_tol: Relative tolerance (default 1e-9).
97
+ abs_tol: Absolute tolerance (default 0.0).
98
+
99
+ Returns:
100
+ True if ``|a - b| <= max(rel_tol * |b|, abs_tol)`` for real values, or
69
101
  equivalent per-component comparison for complex.
70
102
 
71
- **Example**::
103
+ Example
104
+ -------
105
+
106
+ .. code-block:: python
72
107
 
73
- >>> isclose(1 + 2j, 1.0 + 2.0j)
74
- True
75
- >>> isclose(3, 3.000000001)
76
- False
77
- >>> isclose(3, 3.000000001, rel_tol=1e-6)
78
- True
108
+ from punit.assertions.numeric import isclose
109
+
110
+ assert isclose(1 + 2j, 1.0 + 2.0j)
111
+ assert not isclose(3, 3.000000001)
112
+ assert isclose(3, 3.000000001, rel_tol=1e-6)
79
113
  """
80
114
  if isinstance(a, complex) or isinstance(b, complex):
81
115
  a_c = a if isinstance(a, complex) else complex(a, 0)
@@ -511,6 +545,22 @@ def isnan(value: Numeric) -> bool:
511
545
  Check if value is NaN (Not a Number).
512
546
 
513
547
  For complex inputs, returns True only if both the real and imaginary parts are NaN.
548
+
549
+ Args:
550
+ value: The numeric value to check.
551
+
552
+ Returns:
553
+ True if value is NaN, False otherwise.
554
+
555
+ Example
556
+ -------
557
+
558
+ .. code-block:: python
559
+
560
+ from punit.assertions.numeric import isnan
561
+
562
+ assert isnan(float('nan'))
563
+ assert not isnan(0.0)
514
564
  """
515
565
  if isinstance(value, complex):
516
566
  return math.isnan(value.real) and math.isnan(value.imag)
@@ -522,6 +572,22 @@ def isinfinite(value: Numeric) -> bool:
522
572
  Check if value is infinite (positive or negative).
523
573
 
524
574
  For complex inputs, returns True only if either the real or imaginary part is infinite.
575
+
576
+ Args:
577
+ value: The numeric value to check.
578
+
579
+ Returns:
580
+ True if value is infinite, False otherwise.
581
+
582
+ Example
583
+ -------
584
+
585
+ .. code-block:: python
586
+
587
+ from punit.assertions.numeric import isinfinite
588
+
589
+ assert isinfinite(float('inf'))
590
+ assert not isinfinite(0.0)
525
591
  """
526
592
  if isinstance(value, complex):
527
593
  return math.isinf(value.real) or math.isinf(value.imag)
@@ -536,16 +602,34 @@ def percentage(
536
602
  """
537
603
  Calculate the percentage difference between two numeric values.
538
604
 
539
- When relative_to_expected=True, uses value_b as reference: abs(a - b) / |b| * 100.
540
- When relative_to_expected=False, uses symmetric form: abs(a - b) / ((|a| + |b|) / 2) * 100.
605
+ When relative_to_expected=True, uses value_b as reference::
606
+
607
+ abs(a - b) / |b| * 100
608
+
609
+ When relative_to_expected=False, uses symmetric form::
610
+
611
+ abs(a - b) / ((|a| + |b|) / 2) * 100
541
612
 
542
613
  For complex inputs, magnitudes (``abs()``) are used consistently: the numerator is
543
614
  the magnitude of the difference, and the denominator is the magnitude of the reference.
544
615
 
545
- :param value_a: The first numeric value.
546
- :param value_b: The second numeric value (reference when relative_to_expected=True).
547
- :param relative_to_expected: If True, use value_b as reference (default).
548
- :returns float: The percentage difference as a float. Returns ``inf`` if both values are zero but differ, otherwise ``0.0``.
616
+ Args:
617
+ value_a: The first numeric value.
618
+ value_b: The second numeric value (reference when relative_to_expected=True).
619
+ relative_to_expected: If True, use value_b as reference (default).
620
+
621
+ Returns:
622
+ The percentage difference as a float. Returns ``inf`` if both values are zero but differ, otherwise ``0.0``.
623
+
624
+ Example
625
+ -------
626
+
627
+ .. code-block:: python
628
+
629
+ from punit.assertions.numeric import percentage
630
+
631
+ assert percentage(10, 100) == 90.0
632
+ assert percentage(0, 0) == 0.0
549
633
  """
550
634
  diff_mag = abs(value_a - value_b)
551
635
  if relative_to_expected:
@@ -1,6 +1,25 @@
1
1
  # SPDX-FileCopyrightText: © 2025 Shaun Wilson
2
2
  # SPDX-License-Identifier: MIT
3
3
 
4
+ """String assertion helpers for comparison and checks.
5
+
6
+ Provides utilities for comparing string equality, checking length bounds,
7
+ and testing for None/empty/whitespace values.
8
+
9
+ Example
10
+ -------
11
+
12
+ .. code-block:: python
13
+
14
+ from punit import strings
15
+
16
+ assert strings.are_same('hello', 'hello')
17
+ assert not strings.are_same('hello', 'world')
18
+ assert strings.is_none_or_empty(None)
19
+ assert strings.has_length('hello', min=3, max=10)
20
+
21
+ """
22
+
4
23
  from typing import Optional
5
24
 
6
25
 
@@ -30,12 +49,30 @@ def are_same(a: str | None, b: str | None) -> bool:
30
49
 
31
50
  def has_length(actual: str | None, min: Optional[int] = None, max: Optional[int] = None) -> bool:
32
51
  """
33
- Check if actual value has the expected number of elements.
52
+ Check if actual value's length falls within the inclusive range [min, max].
34
53
 
35
- :param actual: The actual value to check.
36
- :param int|None expected: The expected number of elements
54
+ Args:
55
+ actual: The string to check.
56
+ min: Inclusive lower bound on length (``len(actual) >= min``).
57
+ max: Inclusive upper bound on length (``len(actual) <= max``).
58
+
59
+ Returns:
60
+ True if the length satisfies the bounds, False otherwise.
61
+ When actual is None, returns True only when both bounds are effectively zero.
37
62
 
38
- :returns bool: True if the sequence has exactly the expected number of elements, False otherwise
63
+ Example
64
+ -------
65
+
66
+ .. code-block:: python
67
+
68
+ from punit import strings
69
+
70
+ a = 'hello'
71
+
72
+ assert strings.has_length(a, min=5)
73
+ assert strings.has_length(a, max=5)
74
+ assert strings.has_length(a, min=3, max=7)
75
+ assert not strings.has_length(a, min=6)
39
76
  """
40
77
  if actual is None:
41
78
  if ((min is None or min == 0) and (max is None or max == 0)):
@@ -68,10 +105,21 @@ def is_none_or_whitespace(string: str | None) -> bool:
68
105
  Check if a string is None or whitespace.
69
106
 
70
107
  Args:
71
- string: The string to check
108
+ string: The string to check.
72
109
 
73
110
  Returns:
74
- True if the string is None or whitespace, False otherwise
111
+ True if the string is None or whitespace, False otherwise.
112
+
113
+ Example
114
+ -------
115
+
116
+ .. code-block:: python
117
+
118
+ from punit import strings
119
+
120
+ assert not strings.is_none_or_whitespace('hello')
121
+ assert strings.is_none_or_whitespace(' \t')
122
+ assert strings.is_none_or_whitespace(None)
75
123
  """
76
124
  if string is None:
77
125
  return True