requiresthat 2025.6.15.6__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.
- requiresthat-2025.6.15.6/PKG-INFO +45 -0
- requiresthat-2025.6.15.6/README.rst +37 -0
- requiresthat-2025.6.15.6/pyproject.toml +19 -0
- requiresthat-2025.6.15.6/setup.cfg +4 -0
- requiresthat-2025.6.15.6/src/requiresthat/__init__.py +1 -0
- requiresthat-2025.6.15.6/src/requiresthat/_exceptions.py +15 -0
- requiresthat-2025.6.15.6/src/requiresthat/_requires.py +40 -0
- requiresthat-2025.6.15.6/src/requiresthat/_when.py +13 -0
- requiresthat-2025.6.15.6/src/requiresthat.egg-info/PKG-INFO +45 -0
- requiresthat-2025.6.15.6/src/requiresthat.egg-info/SOURCES.txt +11 -0
- requiresthat-2025.6.15.6/src/requiresthat.egg-info/dependency_links.txt +1 -0
- requiresthat-2025.6.15.6/src/requiresthat.egg-info/top_level.txt +1 -0
- requiresthat-2025.6.15.6/tests/test_requiresthat.py +108 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: requiresthat
|
3
|
+
Version: 2025.6.15.6
|
4
|
+
Summary: Decorate an instance method with pre- and/or postconditions that must be fulfilled
|
5
|
+
Author-email: Ann T Ropea <bedhanger@gmx.de>
|
6
|
+
Project-URL: Homepage, https://gitlab.com/bedhanger/mwe/-/tree/master/python/requiresthat
|
7
|
+
Description-Content-Type: text/x-rst
|
8
|
+
|
9
|
+
requiresthat
|
10
|
+
============
|
11
|
+
|
12
|
+
Decorate an instance method with pre- and/or postconditions that must be fulfilled
|
13
|
+
|
14
|
+
Example usage
|
15
|
+
-------------
|
16
|
+
|
17
|
+
.. code-block:: python
|
18
|
+
|
19
|
+
from requiresthat import requires, RequirementNotFulfilledError, APRIORI, POSTMORTEM, BEFOREANDAFTER
|
20
|
+
|
21
|
+
class C:
|
22
|
+
|
23
|
+
def __init__(self, data=None):
|
24
|
+
self.data = data
|
25
|
+
|
26
|
+
@requires(that='self.data is not None')
|
27
|
+
@requires(that='self.data == "spam"', when=APRIORI)
|
28
|
+
@requires(that='True is not False')
|
29
|
+
@requires(that='self.data != "spam"', when=POSTMORTEM)
|
30
|
+
@requires(that='len(self.data) >= 3', when=BEFOREANDAFTER)
|
31
|
+
def method(self):
|
32
|
+
self.data = 'ham'
|
33
|
+
|
34
|
+
X = C(data='spam')
|
35
|
+
X.method()
|
36
|
+
|
37
|
+
The ``that`` can be almost any valid Python statement which can be evaluated for its veracity, and
|
38
|
+
whose result will decide whether or not the method fires/will be considered a success.
|
39
|
+
|
40
|
+
The parameter ``when`` decides if the condition is a-priori, post-mortem, or before-and-after.
|
41
|
+
The default is a-priori, meaning a precondition. Note that before-and-after does *not* mean during;
|
42
|
+
you cannot mandate an invariant this way!
|
43
|
+
|
44
|
+
``RequirementNotFulfilledError`` is the exception you have to deal with in case a condition is not
|
45
|
+
met.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
requiresthat
|
2
|
+
============
|
3
|
+
|
4
|
+
Decorate an instance method with pre- and/or postconditions that must be fulfilled
|
5
|
+
|
6
|
+
Example usage
|
7
|
+
-------------
|
8
|
+
|
9
|
+
.. code-block:: python
|
10
|
+
|
11
|
+
from requiresthat import requires, RequirementNotFulfilledError, APRIORI, POSTMORTEM, BEFOREANDAFTER
|
12
|
+
|
13
|
+
class C:
|
14
|
+
|
15
|
+
def __init__(self, data=None):
|
16
|
+
self.data = data
|
17
|
+
|
18
|
+
@requires(that='self.data is not None')
|
19
|
+
@requires(that='self.data == "spam"', when=APRIORI)
|
20
|
+
@requires(that='True is not False')
|
21
|
+
@requires(that='self.data != "spam"', when=POSTMORTEM)
|
22
|
+
@requires(that='len(self.data) >= 3', when=BEFOREANDAFTER)
|
23
|
+
def method(self):
|
24
|
+
self.data = 'ham'
|
25
|
+
|
26
|
+
X = C(data='spam')
|
27
|
+
X.method()
|
28
|
+
|
29
|
+
The ``that`` can be almost any valid Python statement which can be evaluated for its veracity, and
|
30
|
+
whose result will decide whether or not the method fires/will be considered a success.
|
31
|
+
|
32
|
+
The parameter ``when`` decides if the condition is a-priori, post-mortem, or before-and-after.
|
33
|
+
The default is a-priori, meaning a precondition. Note that before-and-after does *not* mean during;
|
34
|
+
you cannot mandate an invariant this way!
|
35
|
+
|
36
|
+
``RequirementNotFulfilledError`` is the exception you have to deal with in case a condition is not
|
37
|
+
met.
|
@@ -0,0 +1,19 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "requiresthat"
|
7
|
+
version = "2025.6.15.6"
|
8
|
+
authors = [
|
9
|
+
{name = "Ann T Ropea", email = "bedhanger@gmx.de"},
|
10
|
+
]
|
11
|
+
description = "Decorate an instance method with pre- and/or postconditions that must be fulfilled"
|
12
|
+
readme = "README.rst"
|
13
|
+
dependencies = []
|
14
|
+
|
15
|
+
[project.urls]
|
16
|
+
Homepage = "https://gitlab.com/bedhanger/mwe/-/tree/master/python/requiresthat"
|
17
|
+
|
18
|
+
[tool.pytest.ini_options]
|
19
|
+
pythonpath = "src"
|
@@ -0,0 +1 @@
|
|
1
|
+
from ._requires import requires, RequirementNotFulfilledError, APRIORI, POSTMORTEM, BEFOREANDAFTER
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"""Raise this when a requirement is found wanting"""
|
2
|
+
|
3
|
+
import textwrap
|
4
|
+
|
5
|
+
class RequirementNotFulfilledError(Exception):
|
6
|
+
|
7
|
+
def __init__(self, that, when, msg=None):
|
8
|
+
"""Show a default or a user-provided message indicating that some condition is unmet"""
|
9
|
+
|
10
|
+
self.default_msg = textwrap.dedent(f"""
|
11
|
+
{that!r} ({when.name!r}) does not hold
|
12
|
+
""").strip()
|
13
|
+
|
14
|
+
# Call the base class' constructor to init the exception class
|
15
|
+
super().__init__(msg or self.default_msg)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
"""See the README file"""
|
2
|
+
|
3
|
+
from typing import Optional, Callable
|
4
|
+
from functools import wraps
|
5
|
+
|
6
|
+
from ._when import When, APRIORI, POSTMORTEM, BEFOREANDAFTER
|
7
|
+
from ._exceptions import RequirementNotFulfilledError
|
8
|
+
|
9
|
+
def requires(that, when: When = APRIORI) -> Optional[Callable]:
|
10
|
+
"""Require <that> of the decoratee, and require it <when>"""
|
11
|
+
|
12
|
+
def func_wrapper(func: Callable) -> Optional[Callable]:
|
13
|
+
"""First-level wrap the decoratee"""
|
14
|
+
|
15
|
+
@wraps(func)
|
16
|
+
def inner_wrapper(self, *pargs, **kwargs) -> Optional[Callable]:
|
17
|
+
"""Wrap the first-level wrapper
|
18
|
+
|
19
|
+
The wrapping stops here...
|
20
|
+
"""
|
21
|
+
try:
|
22
|
+
if when == APRIORI:
|
23
|
+
assert eval(that)
|
24
|
+
# We can use a return here :-)
|
25
|
+
return func(self, *pargs, **kwargs)
|
26
|
+
elif when == POSTMORTEM:
|
27
|
+
func(self, *pargs, **kwargs)
|
28
|
+
assert eval(that)
|
29
|
+
elif when == BEFOREANDAFTER:
|
30
|
+
assert eval(that)
|
31
|
+
func(self, *pargs, **kwargs)
|
32
|
+
assert eval(that)
|
33
|
+
# We don't need an else clause; trying to enlist something that's not in the enum
|
34
|
+
# will be penalised with an AttributeError, and small typos will be healed with a
|
35
|
+
# suggestion as to what you might have meant.
|
36
|
+
except AssertionError as exc:
|
37
|
+
raise RequirementNotFulfilledError(that, when) from exc
|
38
|
+
return inner_wrapper
|
39
|
+
|
40
|
+
return func_wrapper
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""When should a condition hold"""
|
2
|
+
|
3
|
+
from enum import Enum, auto
|
4
|
+
|
5
|
+
class When(Enum):
|
6
|
+
APRIORI = auto()
|
7
|
+
POSTMORTEM = auto()
|
8
|
+
BEFOREANDAFTER = auto()
|
9
|
+
# There is no DURING or INBETWEEN!
|
10
|
+
|
11
|
+
APRIORI = When.APRIORI
|
12
|
+
POSTMORTEM = When.POSTMORTEM
|
13
|
+
BEFOREANDAFTER = When.BEFOREANDAFTER
|
@@ -0,0 +1,45 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: requiresthat
|
3
|
+
Version: 2025.6.15.6
|
4
|
+
Summary: Decorate an instance method with pre- and/or postconditions that must be fulfilled
|
5
|
+
Author-email: Ann T Ropea <bedhanger@gmx.de>
|
6
|
+
Project-URL: Homepage, https://gitlab.com/bedhanger/mwe/-/tree/master/python/requiresthat
|
7
|
+
Description-Content-Type: text/x-rst
|
8
|
+
|
9
|
+
requiresthat
|
10
|
+
============
|
11
|
+
|
12
|
+
Decorate an instance method with pre- and/or postconditions that must be fulfilled
|
13
|
+
|
14
|
+
Example usage
|
15
|
+
-------------
|
16
|
+
|
17
|
+
.. code-block:: python
|
18
|
+
|
19
|
+
from requiresthat import requires, RequirementNotFulfilledError, APRIORI, POSTMORTEM, BEFOREANDAFTER
|
20
|
+
|
21
|
+
class C:
|
22
|
+
|
23
|
+
def __init__(self, data=None):
|
24
|
+
self.data = data
|
25
|
+
|
26
|
+
@requires(that='self.data is not None')
|
27
|
+
@requires(that='self.data == "spam"', when=APRIORI)
|
28
|
+
@requires(that='True is not False')
|
29
|
+
@requires(that='self.data != "spam"', when=POSTMORTEM)
|
30
|
+
@requires(that='len(self.data) >= 3', when=BEFOREANDAFTER)
|
31
|
+
def method(self):
|
32
|
+
self.data = 'ham'
|
33
|
+
|
34
|
+
X = C(data='spam')
|
35
|
+
X.method()
|
36
|
+
|
37
|
+
The ``that`` can be almost any valid Python statement which can be evaluated for its veracity, and
|
38
|
+
whose result will decide whether or not the method fires/will be considered a success.
|
39
|
+
|
40
|
+
The parameter ``when`` decides if the condition is a-priori, post-mortem, or before-and-after.
|
41
|
+
The default is a-priori, meaning a precondition. Note that before-and-after does *not* mean during;
|
42
|
+
you cannot mandate an invariant this way!
|
43
|
+
|
44
|
+
``RequirementNotFulfilledError`` is the exception you have to deal with in case a condition is not
|
45
|
+
met.
|
@@ -0,0 +1,11 @@
|
|
1
|
+
README.rst
|
2
|
+
pyproject.toml
|
3
|
+
src/requiresthat/__init__.py
|
4
|
+
src/requiresthat/_exceptions.py
|
5
|
+
src/requiresthat/_requires.py
|
6
|
+
src/requiresthat/_when.py
|
7
|
+
src/requiresthat.egg-info/PKG-INFO
|
8
|
+
src/requiresthat.egg-info/SOURCES.txt
|
9
|
+
src/requiresthat.egg-info/dependency_links.txt
|
10
|
+
src/requiresthat.egg-info/top_level.txt
|
11
|
+
tests/test_requiresthat.py
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
requiresthat
|
@@ -0,0 +1,108 @@
|
|
1
|
+
#!/usr/bin/env --split-string=python -m pytest --verbose
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
from requiresthat import requires, RequirementNotFulfilledError, APRIORI, POSTMORTEM, BEFOREANDAFTER
|
6
|
+
|
7
|
+
class TestCase_requiresthat_01:
|
8
|
+
|
9
|
+
def test_trivial(self):
|
10
|
+
|
11
|
+
class Spam:
|
12
|
+
|
13
|
+
@requires(that='True is not False')
|
14
|
+
@requires(that='self is not None')
|
15
|
+
def run(self): ...
|
16
|
+
|
17
|
+
S = Spam()
|
18
|
+
S.run()
|
19
|
+
|
20
|
+
def test_tbd_is_more_than_none(self):
|
21
|
+
|
22
|
+
class Spam:
|
23
|
+
|
24
|
+
# Ok, we suggest that "not" is "more than", but the idea should be clear...
|
25
|
+
@requires(that='... is not None')
|
26
|
+
def run(self): ...
|
27
|
+
|
28
|
+
S = Spam()
|
29
|
+
S.run()
|
30
|
+
|
31
|
+
def test_good(self):
|
32
|
+
|
33
|
+
class Spam:
|
34
|
+
|
35
|
+
def __init__(self):
|
36
|
+
self.foo = 66
|
37
|
+
self.bar = None
|
38
|
+
|
39
|
+
@requires(that='self.foo == 66')
|
40
|
+
@requires(that='self.bar is None')
|
41
|
+
def run(self): ...
|
42
|
+
|
43
|
+
S = Spam()
|
44
|
+
S.run()
|
45
|
+
|
46
|
+
def test_bad(self):
|
47
|
+
|
48
|
+
class Spam:
|
49
|
+
def __init__(self):
|
50
|
+
self.foo = 66
|
51
|
+
self.bar = ... # To be continued, not None
|
52
|
+
|
53
|
+
@requires(that='self.foo == 66')
|
54
|
+
@requires(that='self.bar is None')
|
55
|
+
def run(self): ...
|
56
|
+
|
57
|
+
S = Spam()
|
58
|
+
with pytest.raises(RequirementNotFulfilledError):
|
59
|
+
S.run()
|
60
|
+
|
61
|
+
@pytest.mark.filterwarnings("ignore::SyntaxWarning")
|
62
|
+
def test_ugly(self):
|
63
|
+
|
64
|
+
class Spam:
|
65
|
+
def __init__(self):
|
66
|
+
pass
|
67
|
+
|
68
|
+
@requires(that=b"'24' is not 'the answer'")
|
69
|
+
def run(self): ...
|
70
|
+
|
71
|
+
S = Spam()
|
72
|
+
S.run()
|
73
|
+
|
74
|
+
def test_too_soon_is_bad(self):
|
75
|
+
|
76
|
+
class Spam:
|
77
|
+
|
78
|
+
@requires(that='self.spam == "eggs"')
|
79
|
+
def __init__(self):
|
80
|
+
self.spam = 'ham'
|
81
|
+
|
82
|
+
def run(self): ...
|
83
|
+
|
84
|
+
# We break the constructor (the fact that we confuse spam and eggs is incidental)
|
85
|
+
with pytest.raises(AttributeError):
|
86
|
+
S = Spam()
|
87
|
+
|
88
|
+
# Remember, the constructor just failed...
|
89
|
+
with pytest.raises(UnboundLocalError):
|
90
|
+
S.run()
|
91
|
+
|
92
|
+
def test_docu(self):
|
93
|
+
|
94
|
+
class C:
|
95
|
+
|
96
|
+
def __init__(self, data=None):
|
97
|
+
self.data = data
|
98
|
+
|
99
|
+
@requires(that='self.data is not None')
|
100
|
+
@requires(that='self.data == "spam"', when=APRIORI)
|
101
|
+
@requires(that='True is not False')
|
102
|
+
@requires(that='self.data != "spam"', when=POSTMORTEM)
|
103
|
+
@requires(that='len(self.data) >= 3', when=BEFOREANDAFTER)
|
104
|
+
def method(self):
|
105
|
+
self.data = 'ham'
|
106
|
+
|
107
|
+
X = C(data='spam')
|
108
|
+
X.method()
|