punit 1.7.2__tar.gz → 1.8.1__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.2/src/pUnit.egg-info → punit-1.8.1}/PKG-INFO +1 -1
  2. {punit-1.7.2 → punit-1.8.1}/pyproject.toml +1 -1
  3. {punit-1.7.2 → punit-1.8.1/src/pUnit.egg-info}/PKG-INFO +1 -1
  4. {punit-1.7.2 → punit-1.8.1}/src/pUnit.egg-info/SOURCES.txt +2 -0
  5. {punit-1.7.2 → punit-1.8.1}/src/punit/TestResult.py +40 -4
  6. {punit-1.7.2 → punit-1.8.1}/src/punit/__init__.py +7 -4
  7. {punit-1.7.2 → punit-1.8.1}/src/punit/__main__.py +14 -14
  8. punit-1.8.1/src/punit/assertions/__init__.py +28 -0
  9. {punit-1.7.2 → punit-1.8.1}/src/punit/assertions/collections.py +70 -11
  10. punit-1.8.1/src/punit/assertions/exceptions.py +87 -0
  11. {punit-1.7.2 → punit-1.8.1}/src/punit/assertions/numeric.py +103 -19
  12. {punit-1.7.2 → punit-1.8.1}/src/punit/assertions/strings.py +54 -6
  13. {punit-1.7.2 → punit-1.8.1}/src/punit/cli.py +26 -7
  14. {punit-1.7.2 → punit-1.8.1}/src/punit/facts/Fact.py +49 -0
  15. {punit-1.7.2 → punit-1.8.1}/src/punit/facts/FactManager.py +8 -8
  16. punit-1.8.1/src/punit/facts/__init__.py +35 -0
  17. {punit-1.7.2 → punit-1.8.1}/src/punit/filters/Filter.py +13 -2
  18. {punit-1.7.2 → punit-1.8.1}/src/punit/filters/FilterManager.py +6 -6
  19. {punit-1.7.2 → punit-1.8.1}/src/punit/reports/HtmlReportGenerator.py +8 -1
  20. {punit-1.7.2 → punit-1.8.1}/src/punit/reports/JUnitReportGenerator.py +9 -4
  21. {punit-1.7.2 → punit-1.8.1}/src/punit/reports/JsonReportGenerator.py +5 -1
  22. punit-1.8.1/src/punit/results/__init__.py +22 -0
  23. punit-1.8.1/src/punit/results/fails.py +87 -0
  24. {punit-1.7.2 → punit-1.8.1}/src/punit/runner.py +37 -8
  25. {punit-1.7.2 → punit-1.8.1}/src/punit/setups/Setup.py +66 -2
  26. {punit-1.7.2 → punit-1.8.1}/src/punit/setups/SetupManager.py +2 -2
  27. punit-1.8.1/src/punit/setups/__init__.py +43 -0
  28. {punit-1.7.2 → punit-1.8.1}/src/punit/teardowns/Teardown.py +58 -2
  29. {punit-1.7.2 → punit-1.8.1}/src/punit/teardowns/TeardownManager.py +2 -2
  30. punit-1.8.1/src/punit/teardowns/__init__.py +42 -0
  31. {punit-1.7.2 → punit-1.8.1}/src/punit/theories/Theory.py +79 -1
  32. {punit-1.7.2 → punit-1.8.1}/src/punit/theories/TheoryManager.py +3 -3
  33. punit-1.8.1/src/punit/theories/__init__.py +33 -0
  34. punit-1.8.1/src/punit/traits/Trait.py +88 -0
  35. punit-1.8.1/src/punit/traits/__init__.py +51 -0
  36. punit-1.7.2/src/punit/assertions/__init__.py +0 -12
  37. punit-1.7.2/src/punit/assertions/exceptions.py +0 -37
  38. punit-1.7.2/src/punit/facts/__init__.py +0 -11
  39. punit-1.7.2/src/punit/setups/__init__.py +0 -11
  40. punit-1.7.2/src/punit/teardowns/__init__.py +0 -11
  41. punit-1.7.2/src/punit/theories/__init__.py +0 -11
  42. punit-1.7.2/src/punit/traits/Trait.py +0 -32
  43. punit-1.7.2/src/punit/traits/__init__.py +0 -11
  44. {punit-1.7.2 → punit-1.8.1}/LICENSE +0 -0
  45. {punit-1.7.2 → punit-1.8.1}/README.md +0 -0
  46. {punit-1.7.2 → punit-1.8.1}/setup.cfg +0 -0
  47. {punit-1.7.2 → punit-1.8.1}/src/pUnit.egg-info/dependency_links.txt +0 -0
  48. {punit-1.7.2 → punit-1.8.1}/src/pUnit.egg-info/entry_points.txt +0 -0
  49. {punit-1.7.2 → punit-1.8.1}/src/pUnit.egg-info/requires.txt +0 -0
  50. {punit-1.7.2 → punit-1.8.1}/src/pUnit.egg-info/top_level.txt +0 -0
  51. {punit-1.7.2 → punit-1.8.1}/src/punit/discovery/TestModuleDiscovery.py +0 -0
  52. {punit-1.7.2 → punit-1.8.1}/src/punit/discovery/__init__.py +0 -0
  53. {punit-1.7.2 → punit-1.8.1}/src/punit/filters/__init__.py +0 -0
  54. {punit-1.7.2 → punit-1.8.1}/src/punit/metadata/CallableMetadata.py +0 -0
  55. {punit-1.7.2 → punit-1.8.1}/src/punit/metadata/__init__.py +0 -0
  56. {punit-1.7.2 → punit-1.8.1}/src/punit/py.typed +0 -0
  57. {punit-1.7.2 → punit-1.8.1}/src/punit/reports/__init__.py +0 -0
  58. {punit-1.7.2 → punit-1.8.1}/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.2
3
+ Version: 1.8.1
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.2"
3
+ version = "1.8.1"
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.2
3
+ Version: 1.8.1
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:
@@ -27,11 +27,28 @@ class TextIOCapture:
27
27
 
28
28
 
29
29
  class TestResult:
30
+ """
31
+ Accumulates the outcome of a single test execution.
32
+
33
+ Example
34
+ -------
35
+
36
+ Create and populate a result manually:
37
+
38
+ .. code-block:: python
39
+
40
+ result = TestResult()
41
+ result.module_name = 'tests.example'
42
+ result.test_name = 'test_something'
43
+ result.is_success = True
44
+
45
+ """
30
46
 
31
47
  __class_name: Optional[str]
32
48
  __exception: Exception | None
33
49
  __file_name: str | None
34
50
  __host_name: str | None
51
+ __expected_failure_reason: str | None
35
52
  __is_success: bool | None
36
53
  __module_name: str | None
37
54
  __package_name: str | None
@@ -47,6 +64,7 @@ class TestResult:
47
64
  self.__exception = None
48
65
  self.__file_name = None
49
66
  self.__host_name = None
67
+ self.__expected_failure_reason = None
50
68
  self.__is_success = None
51
69
  self.__module_name = None
52
70
  self.__package_name = None
@@ -69,6 +87,14 @@ class TestResult:
69
87
  def class_name(self, value: Optional[str]) -> None:
70
88
  self.__class_name = value
71
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
+
72
98
  @property
73
99
  def exception(self) -> Exception | None:
74
100
  return self.__exception
@@ -93,6 +119,16 @@ class TestResult:
93
119
  def host_name(self, value: str) -> None:
94
120
  self.__host_name = value
95
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
+
96
132
  @property
97
133
  def is_success(self) -> bool:
98
134
  return False if self.__is_success is None else self.__is_success
@@ -102,8 +138,8 @@ class TestResult:
102
138
  self.__is_success = value
103
139
 
104
140
  @property
105
- def module_name(self) -> str:
106
- return cast(str, self.__module_name)
141
+ def module_name(self) -> str | None:
142
+ return self.__module_name
107
143
 
108
144
  @module_name.setter
109
145
  def module_name(self, value: str) -> None:
@@ -175,7 +211,7 @@ class TestResult:
175
211
  elif took >= 0.000000001:
176
212
  return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'μs'
177
213
  else:
178
- return f'{(took*1000):.3f}'.rstrip('0').rstrip('.') + 'ms'
214
+ return f'{(took*1000000):.3f}'.rstrip('0').rstrip('.') + 'ns'
179
215
 
180
216
  def capture_output(self, quiet: bool = False) -> None:
181
217
  self.__stdout_capture = TextIOCapture(sys.stdout, quiet)
@@ -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.2'
16
- __commit__ = 'b60afab'
18
+ __version__ = '1.8.1'
19
+ __commit__ = 'faca395'
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
  ]
@@ -16,7 +16,7 @@ from .teardowns.TeardownManager import TeardownManager
16
16
 
17
17
 
18
18
  async def async_main() -> None:
19
- ts = time.time()
19
+ start_time = time.time()
20
20
  cli = CommandLineInterface.parse()
21
21
  if cli.help: # pragma: no cover
22
22
  cli.print_help()
@@ -26,27 +26,27 @@ async def async_main() -> None:
26
26
  cli.print_version()
27
27
  os.chdir(cli.workdir)
28
28
  if cli.no_pathfix is not True:
29
- pathbase = str(Path.cwd())
30
- srcbase = os.path.join(pathbase, 'src')
31
- if os.path.exists(srcbase) and srcbase not in sys.path:
32
- sys.path.append(srcbase)
33
- if pathbase not in sys.path:
34
- sys.path.append(pathbase)
29
+ cwd_path = str(Path.cwd())
30
+ src_path = os.path.join(cwd_path, 'src')
31
+ if os.path.exists(src_path) and src_path not in sys.path:
32
+ sys.path.append(src_path)
33
+ if cwd_path not in sys.path:
34
+ sys.path.append(cwd_path)
35
35
  test_module_discovery = TestModuleDiscovery(
36
36
  os.path.join(cli.workdir, cli.test_package_name),
37
37
  cli.includePatterns,
38
38
  cli.excludePatterns,
39
39
  cli)
40
40
  test_module_discovery.discover()
41
- testRunner = TestRunner(cli.test_package_name, test_module_discovery.filenames, cli)
42
- results = await testRunner.run()
43
- totalTime = time.time() - ts
44
- failureCount = 0
41
+ test_runner = TestRunner(cli.test_package_name, test_module_discovery.filenames, cli)
42
+ results = await test_runner.run()
43
+ total_time = time.time() - start_time
44
+ failure_count = 0
45
45
  for result in results:
46
46
  if not result.is_success:
47
- failureCount += 1 # pragma: no cover
47
+ failure_count += 1 # pragma: no cover
48
48
  if not cli.quiet:
49
- print(f'Total: {len(results)}, Failures: {failureCount}, Took: {totalTime:.3f}s')
49
+ print(f'Total: {len(results)}, Failures: {failure_count}, Took: {total_time:.3f}s')
50
50
  if cli.reportFormat is not None: # pragma: no cover
51
51
  report: str = ''
52
52
  match cli.reportFormat:
@@ -74,7 +74,7 @@ async def async_main() -> None:
74
74
  pass
75
75
 
76
76
  if cli.no_exitcode is not True:
77
- if failureCount:
77
+ if failure_count:
78
78
  # test failures trigger exit code 119 (for automation gating)
79
79
  sys.exit(119)
80
80
  if TeardownManager.instance().teardown_error_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: