pytest-pyspec 0.5.2__py3-none-any.whl → 0.10.1__py3-none-any.whl
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.
- pytest_pyspec/item.py +156 -35
- pytest_pyspec/output.py +4 -4
- pytest_pyspec/plugin.py +131 -38
- pytest_pyspec-0.10.1.dist-info/METADATA +131 -0
- pytest_pyspec-0.10.1.dist-info/RECORD +9 -0
- {pytest_pyspec-0.5.2.dist-info → pytest_pyspec-0.10.1.dist-info}/WHEEL +1 -1
- pytest_pyspec-0.10.1.dist-info/entry_points.txt +2 -0
- pytest_pyspec-0.10.1.dist-info/licenses/LICENSE +9 -0
- pytest_pyspec-0.5.2.dist-info/LICENSE +0 -674
- pytest_pyspec-0.5.2.dist-info/METADATA +0 -77
- pytest_pyspec-0.5.2.dist-info/RECORD +0 -9
- pytest_pyspec-0.5.2.dist-info/entry_points.txt +0 -3
pytest_pyspec/item.py
CHANGED
|
@@ -1,51 +1,89 @@
|
|
|
1
1
|
from types import ModuleType
|
|
2
2
|
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
3
6
|
import pytest
|
|
4
7
|
|
|
5
8
|
class Item:
|
|
9
|
+
"""
|
|
10
|
+
Wrapper around a pytest.Item that provides a human-friendly description.
|
|
11
|
+
|
|
12
|
+
Responsibilities:
|
|
13
|
+
- Derive a readable description for a test or container from either the
|
|
14
|
+
docstring of the underlying object or its Python name.
|
|
15
|
+
- Provide helpers to normalize names (convert CamelCase/snake_case into
|
|
16
|
+
words), strip common pytest prefixes, and keep a consistent
|
|
17
|
+
representation.
|
|
18
|
+
"""
|
|
19
|
+
|
|
6
20
|
def __init__(self, item: pytest.Item) -> None:
|
|
7
21
|
self._item = item
|
|
8
22
|
|
|
9
23
|
@property
|
|
10
|
-
def description(self):
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if not description:
|
|
14
|
-
description = self._item.name
|
|
15
|
-
description = self._parse_description(description)
|
|
24
|
+
def description(self) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Return a human-readable description for the wrapped pytest item.
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
Preference order:
|
|
29
|
+
1) First non-empty line of the object's docstring, stripped.
|
|
30
|
+
2) A description derived from the Python name of the item.
|
|
31
|
+
"""
|
|
32
|
+
docstring = getattr(self._item.obj, "__doc__", None)
|
|
33
|
+
if docstring:
|
|
34
|
+
return self._parse_docstring(docstring)
|
|
35
|
+
return self._parse_itemname(self._item.name)
|
|
20
36
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
def _parse_docstring(self, docstring: str) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Return the first line of a docstring, trimmed of surrounding spaces.
|
|
40
|
+
"""
|
|
41
|
+
first_line = docstring.splitlines()[0]
|
|
42
|
+
return first_line.strip()
|
|
26
43
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
44
|
+
def _parse_name(self, name: str) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Normalize a Python identifier into words.
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
Steps:
|
|
49
|
+
- Insert underscores before internal capital letters (turn "MyClass"
|
|
50
|
+
-> "My_Class").
|
|
51
|
+
- Lowercase everything.
|
|
52
|
+
- Replace underscores with single spaces and normalize multiple
|
|
53
|
+
underscores.
|
|
54
|
+
"""
|
|
55
|
+
description = re.sub(
|
|
56
|
+
r'(?!^[A-Z])([A-Z])',
|
|
57
|
+
r'_\g<1>',
|
|
58
|
+
name,
|
|
59
|
+
)
|
|
60
|
+
description = description.lower()
|
|
61
|
+
description = ' '.join(description.split('_'))
|
|
62
|
+
return description
|
|
38
63
|
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
def _parse_itemname(self, name: str) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Create a description from a pytest test function name.
|
|
41
67
|
|
|
68
|
+
- Uses _parse_name to normalize the identifier.
|
|
69
|
+
- Removes common prefixes like "test"/"it".
|
|
70
|
+
"""
|
|
71
|
+
description = self._parse_name(name)
|
|
72
|
+
description = re.sub(
|
|
73
|
+
r'^(test|it) ',
|
|
74
|
+
'',
|
|
75
|
+
description,
|
|
76
|
+
)
|
|
42
77
|
return description
|
|
43
78
|
|
|
44
|
-
def __repr__(self):
|
|
79
|
+
def __repr__(self) -> str:
|
|
45
80
|
return self._item.__repr__()
|
|
46
81
|
|
|
47
82
|
|
|
48
83
|
class Test(Item):
|
|
84
|
+
"""
|
|
85
|
+
Represents a single test item and its relationship to a container.
|
|
86
|
+
"""
|
|
49
87
|
def __init__(self, item: pytest.Item) -> None:
|
|
50
88
|
super().__init__(item)
|
|
51
89
|
self.container: Container = None
|
|
@@ -53,27 +91,47 @@ class Test(Item):
|
|
|
53
91
|
|
|
54
92
|
@property
|
|
55
93
|
def level(self) -> int:
|
|
94
|
+
"""
|
|
95
|
+
Depth in the container hierarchy (root container is 0).
|
|
96
|
+
"""
|
|
56
97
|
if not self.container:
|
|
57
98
|
return 0
|
|
58
99
|
return self.container.level +1
|
|
59
100
|
|
|
60
101
|
|
|
61
102
|
class Container(Item):
|
|
62
|
-
|
|
103
|
+
"""
|
|
104
|
+
Represents a grouping of tests (e.g., class/function scopes) in pytest.
|
|
105
|
+
|
|
106
|
+
Tracks:
|
|
107
|
+
- Nested containers (parents/children) following pytest's item tree.
|
|
108
|
+
- Contained tests.
|
|
109
|
+
Provides utilities to compute display names and levels.
|
|
110
|
+
"""
|
|
111
|
+
def __init__(self, item: pytest.Item) -> None:
|
|
63
112
|
super().__init__(item)
|
|
64
113
|
self.tests: List[Test] = list()
|
|
65
114
|
self.containers: List[Container] = list()
|
|
66
115
|
self.parent = None
|
|
67
116
|
|
|
68
|
-
def add(self, test: Test):
|
|
117
|
+
def add(self, test: Test) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Attach a Test to this container and set its back-reference.
|
|
120
|
+
"""
|
|
69
121
|
self.tests.append(test)
|
|
70
122
|
test.container = self
|
|
71
123
|
|
|
72
|
-
def add_container(self, container: 'Container'):
|
|
124
|
+
def add_container(self, container: 'Container') -> None:
|
|
125
|
+
"""
|
|
126
|
+
Attach a child container and set parent back-reference.
|
|
127
|
+
"""
|
|
73
128
|
self.containers.append(container)
|
|
74
129
|
container.parent = self
|
|
75
130
|
|
|
76
|
-
def flat_list(self):
|
|
131
|
+
def flat_list(self) -> list:
|
|
132
|
+
"""
|
|
133
|
+
Return a list of containers from root to self.
|
|
134
|
+
"""
|
|
77
135
|
containers = []
|
|
78
136
|
container = self
|
|
79
137
|
while container:
|
|
@@ -83,21 +141,63 @@ class Container(Item):
|
|
|
83
141
|
|
|
84
142
|
@property
|
|
85
143
|
def level(self) -> int:
|
|
144
|
+
"""
|
|
145
|
+
Compute this container's depth relative to the module root.
|
|
146
|
+
|
|
147
|
+
Walks up pytest's internal node tree until reaching a ModuleType parent.
|
|
148
|
+
"""
|
|
86
149
|
level = 0
|
|
87
150
|
|
|
88
151
|
item = self._item
|
|
152
|
+
# Climb the pytest node hierarchy until we reach the module node
|
|
89
153
|
while item.parent and not isinstance(item.parent.obj, ModuleType):
|
|
90
154
|
level += 1
|
|
91
155
|
item = item.parent
|
|
92
156
|
|
|
93
157
|
return level
|
|
158
|
+
|
|
159
|
+
def _parse_docstring(self, docstring: str) -> str:
|
|
160
|
+
"""
|
|
161
|
+
Parse container docstring and prepend an article when appropriate.
|
|
162
|
+
|
|
163
|
+
If the first word doesn't start with "with"/"without", prefix with
|
|
164
|
+
"a ". This mirrors RSpec-style descriptions like "a function" vs
|
|
165
|
+
"with options".
|
|
166
|
+
"""
|
|
167
|
+
description = super()._parse_docstring(docstring)
|
|
168
|
+
if not description.startswith(('with ', 'without ')):
|
|
169
|
+
description = f'a {description}'
|
|
170
|
+
return description
|
|
171
|
+
|
|
172
|
+
def _parse_itemname(self, name: str) -> str:
|
|
173
|
+
"""
|
|
174
|
+
Create a container description from a pytest node name.
|
|
175
|
+
|
|
176
|
+
- Normalize the identifier into words.
|
|
177
|
+
- Replace leading "test"/"describe" with the article "a ".
|
|
178
|
+
"""
|
|
179
|
+
description = self._parse_name(name)
|
|
180
|
+
description = re.sub(
|
|
181
|
+
r'^([Tt]est|[Dd]escribe) ',
|
|
182
|
+
'a ',
|
|
183
|
+
description)
|
|
184
|
+
return description
|
|
94
185
|
|
|
95
186
|
|
|
96
187
|
class ItemFactory:
|
|
188
|
+
"""
|
|
189
|
+
Factory responsible for creating Test items and wiring them into containers.
|
|
190
|
+
"""
|
|
97
191
|
def __init__(self) -> None:
|
|
98
192
|
self.container_factory = ContainerFactory()
|
|
99
193
|
|
|
100
194
|
def create(self, item: pytest.Item) -> Test:
|
|
195
|
+
"""
|
|
196
|
+
Wrap a pytest item as a Test and attach it to its Container.
|
|
197
|
+
|
|
198
|
+
Also mutates pytest's item name to the human-readable description for
|
|
199
|
+
reporting.
|
|
200
|
+
"""
|
|
101
201
|
test_item = Test(item)
|
|
102
202
|
|
|
103
203
|
container_item = item.parent
|
|
@@ -105,33 +205,54 @@ class ItemFactory:
|
|
|
105
205
|
if container:
|
|
106
206
|
container.add(test_item)
|
|
107
207
|
|
|
208
|
+
item.name = test_item.description
|
|
108
209
|
return test_item
|
|
109
210
|
|
|
110
211
|
|
|
111
212
|
class ContainerFactory:
|
|
213
|
+
"""
|
|
214
|
+
Factory that ensures containers are unique per pytest node and linked
|
|
215
|
+
properly.
|
|
216
|
+
"""
|
|
112
217
|
def __init__(self) -> None:
|
|
113
218
|
self.containers: Dict[str, Container] = dict()
|
|
114
219
|
|
|
115
|
-
def create(self, item) -> Container:
|
|
220
|
+
def create(self, item: pytest.Item) -> 'Container':
|
|
221
|
+
"""
|
|
222
|
+
Create (or fetch) the deepest Container for a given pytest node.
|
|
223
|
+
"""
|
|
116
224
|
containers = self._create_containers(item)
|
|
117
225
|
if not containers:
|
|
118
226
|
return None
|
|
119
|
-
|
|
227
|
+
|
|
120
228
|
return containers[-1]
|
|
121
229
|
|
|
122
|
-
def _create_unique_container(self, item):
|
|
230
|
+
def _create_unique_container(self, item: pytest.Item) -> 'Container':
|
|
231
|
+
"""
|
|
232
|
+
Return a unique Container for a pytest node, creating if absent.
|
|
233
|
+
|
|
234
|
+
Also sets the pytest node's display name to the container description.
|
|
235
|
+
"""
|
|
236
|
+
# Cache containers per pytest node to keep uniqueness
|
|
123
237
|
if item not in self.containers:
|
|
124
238
|
container = Container(item)
|
|
125
239
|
self.containers[item] = container
|
|
126
240
|
|
|
127
241
|
container = self.containers.get(item)
|
|
242
|
+
# Mutate node's visible name to our human-friendly description
|
|
243
|
+
item.name = container.description
|
|
128
244
|
return container
|
|
129
245
|
|
|
130
|
-
def _create_containers(self, item):
|
|
246
|
+
def _create_containers(self, item: pytest.Item) -> list:
|
|
247
|
+
"""
|
|
248
|
+
Build the chain of containers from a pytest node up to the module root.
|
|
249
|
+
"""
|
|
131
250
|
containers = []
|
|
132
251
|
child_container = None
|
|
252
|
+
# Walk upwards creating/collecting containers until the module level
|
|
133
253
|
while item and not isinstance(item.obj, ModuleType):
|
|
134
254
|
container = self._create_unique_container(item)
|
|
255
|
+
# Link the previous (deeper) container as a child of the current one
|
|
135
256
|
if child_container:
|
|
136
257
|
container.add_container(child_container)
|
|
137
258
|
|
pytest_pyspec/output.py
CHANGED
|
@@ -2,7 +2,7 @@ import pytest
|
|
|
2
2
|
from .item import Container, Test
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def print_container(container: Container):
|
|
5
|
+
def print_container(container: Container) -> str:
|
|
6
6
|
output = "\n"
|
|
7
7
|
if container:
|
|
8
8
|
for container in container.flat_list():
|
|
@@ -10,18 +10,18 @@ def print_container(container: Container):
|
|
|
10
10
|
container = container.parent
|
|
11
11
|
return output
|
|
12
12
|
|
|
13
|
-
def print_parent_container(container: Container):
|
|
13
|
+
def print_parent_container(container: Container) -> str:
|
|
14
14
|
ident = " " * container.level
|
|
15
15
|
output = f"\n{ident}{container.description}"
|
|
16
16
|
return output
|
|
17
17
|
|
|
18
|
-
def print_test(test: Test):
|
|
18
|
+
def print_test(test: Test) -> str:
|
|
19
19
|
ident = " " * test.level
|
|
20
20
|
status = print_test_status(test)
|
|
21
21
|
output = f"\n{ident}{status} {test.description}"
|
|
22
22
|
return output
|
|
23
23
|
|
|
24
|
-
def print_test_status(test: Test):
|
|
24
|
+
def print_test_status(test: Test) -> str:
|
|
25
25
|
if test.outcome == 'passed':
|
|
26
26
|
return '✓'
|
|
27
27
|
elif test.outcome == 'failed':
|
pytest_pyspec/plugin.py
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Pytest plugin that prints test output in an RSpec-like, readable format.
|
|
4
|
+
|
|
5
|
+
When enabled with the ``--pyspec`` flag, this plugin:
|
|
6
|
+
- Extends test discovery to also match ``it_`` functions and classes whose
|
|
7
|
+
names start with ``Describe`` or ``With``.
|
|
8
|
+
- Builds a tree of containers/tests and renders them with friendly
|
|
9
|
+
descriptions (favoring docstrings when present).
|
|
10
|
+
- Uses pytest's stash to pass the current/previous Test objects between
|
|
11
|
+
hooks, so we can decide when to print a container header.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any, Tuple, Generator
|
|
15
|
+
|
|
1
16
|
import pytest
|
|
2
17
|
from pytest_pyspec.item import ItemFactory, Test
|
|
3
18
|
from pytest_pyspec.output import print_container, print_test
|
|
4
19
|
|
|
5
20
|
|
|
6
|
-
def pytest_addoption(
|
|
7
|
-
|
|
21
|
+
def pytest_addoption(
|
|
22
|
+
parser: pytest.Parser,
|
|
23
|
+
pluginmanager: pytest.PytestPluginManager
|
|
24
|
+
) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Register the ``--pyspec`` command-line flag.
|
|
27
|
+
|
|
28
|
+
The flag toggles all formatting and discovery behavior provided by this
|
|
29
|
+
plugin. We keep discovery changes behind the flag to avoid surprises in
|
|
30
|
+
normal pytest runs.
|
|
31
|
+
"""
|
|
8
32
|
group = parser.getgroup('general')
|
|
9
33
|
group.addoption(
|
|
10
34
|
'--pyspec',
|
|
@@ -14,60 +38,129 @@ def pytest_addoption(parser: pytest.Parser,
|
|
|
14
38
|
)
|
|
15
39
|
|
|
16
40
|
|
|
17
|
-
|
|
18
|
-
def pytest_configure(config: pytest.Config):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
41
|
+
ENABLED_KEY = pytest.StashKey[bool]()
|
|
42
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Initialize plugin state and, when enabled, extend test discovery rules.
|
|
45
|
+
|
|
46
|
+
- ``enabled`` is set only when ``--pyspec`` is present and ``-v`` is not
|
|
47
|
+
used (verbose mode already prints enough, so we defer to pytest's
|
|
48
|
+
default output in that case).
|
|
49
|
+
- Appends discovery patterns for ``it_`` functions and ``Describe``/``With``
|
|
50
|
+
classes when pyspec is on.
|
|
51
|
+
"""
|
|
52
|
+
# Store enabled state in config.stash for access in all hooks.
|
|
53
|
+
enabled = config.getoption('pyspec') and not config.getoption('verbose')
|
|
54
|
+
config.stash[ENABLED_KEY] = enabled
|
|
22
55
|
|
|
56
|
+
if config.getoption('pyspec'):
|
|
57
|
+
# Extend discovery to match RSpec-like naming.
|
|
23
58
|
python_functions = config.getini("python_functions")
|
|
24
59
|
python_functions.append('it_')
|
|
25
60
|
config.option.python_functions = python_functions
|
|
26
61
|
|
|
27
62
|
python_classes = config.getini("python_classes")
|
|
28
|
-
python_classes.append('
|
|
29
|
-
python_classes.append('
|
|
63
|
+
python_classes.append('Describe')
|
|
64
|
+
python_classes.append('With')
|
|
30
65
|
config.option.python_classes = python_classes
|
|
31
66
|
|
|
32
67
|
|
|
68
|
+
# Stash keys used to share Test objects between hooks.
|
|
69
|
+
# These allow the report hook to know the current and previous test, so we can
|
|
70
|
+
# decide when to print a container header (on container change).
|
|
71
|
+
TEST_KEY = pytest.StashKey[Test]()
|
|
72
|
+
PREV_TEST_KEY = pytest.StashKey[Test]()
|
|
73
|
+
def pytest_collection_modifyitems(
|
|
74
|
+
session: pytest.Session,
|
|
75
|
+
config: pytest.Config,
|
|
76
|
+
items: list[pytest.Item],
|
|
77
|
+
) -> None:
|
|
78
|
+
"""
|
|
79
|
+
After collection, wrap each pytest item with our Test model and stash it.
|
|
33
80
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
81
|
+
The previous Test is also stashed so later hooks can determine container
|
|
82
|
+
boundaries and print headers only when the container changes.
|
|
83
|
+
"""
|
|
84
|
+
enabled = config.stash.get(ENABLED_KEY, False)
|
|
37
85
|
if enabled:
|
|
38
86
|
factory = ItemFactory()
|
|
39
87
|
prev_test = None
|
|
40
88
|
for i, item in enumerate(items):
|
|
41
89
|
test = factory.create(item)
|
|
42
|
-
item.stash[
|
|
43
|
-
item.stash[
|
|
90
|
+
item.stash[TEST_KEY] = test
|
|
91
|
+
item.stash[PREV_TEST_KEY] = prev_test
|
|
44
92
|
prev_test = test
|
|
45
93
|
|
|
46
94
|
|
|
47
95
|
@pytest.hookimpl(hookwrapper=True)
|
|
48
|
-
def pytest_runtest_makereport(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
report.test = item.stash[test_key]
|
|
55
|
-
report.prev_test = item.stash[prev_test_key]
|
|
96
|
+
def pytest_runtest_makereport(
|
|
97
|
+
item: pytest.Item,
|
|
98
|
+
call: pytest.CallInfo
|
|
99
|
+
) -> Generator[Any, Any, None]:
|
|
100
|
+
"""
|
|
101
|
+
Inject Test metadata into the report object for later formatting.
|
|
56
102
|
|
|
103
|
+
As a hookwrapper we yield to let pytest create the report, then attach our
|
|
104
|
+
Test and prev_test (from stash) so ``pytest_report_teststatus`` can render
|
|
105
|
+
the right lines.
|
|
106
|
+
"""
|
|
107
|
+
outcome = yield
|
|
108
|
+
enabled = item.config.stash.get(ENABLED_KEY, False)
|
|
109
|
+
if enabled:
|
|
110
|
+
report: pytest.Report = outcome.get_result()
|
|
111
|
+
# TODO Check whether the report has a stash
|
|
112
|
+
# TODO move previous test to test class
|
|
113
|
+
if TEST_KEY in item.stash:
|
|
114
|
+
report.test = item.stash[TEST_KEY]
|
|
115
|
+
report.prev_test = item.stash[PREV_TEST_KEY]
|
|
57
116
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
117
|
+
|
|
118
|
+
from typing import Any, Tuple
|
|
119
|
+
|
|
120
|
+
def pytest_report_teststatus(
|
|
121
|
+
report: pytest.TestReport,
|
|
122
|
+
config: pytest.Config
|
|
123
|
+
) -> Any:
|
|
124
|
+
"""
|
|
125
|
+
Produce short status and a human-friendly text line for each test event.
|
|
126
|
+
|
|
127
|
+
Behavior (only when pyspec is enabled and a Test is attached):
|
|
128
|
+
- On setup: if the container changed, print the container header.
|
|
129
|
+
- On call (or skipped during setup): print the test line with its status
|
|
130
|
+
mark. We reuse pytest's status tuple format.
|
|
131
|
+
"""
|
|
132
|
+
enabled = config.stash.get(ENABLED_KEY, False)
|
|
133
|
+
if enabled and hasattr(report, 'test'):
|
|
134
|
+
test = report.test
|
|
135
|
+
prev_test = report.prev_test
|
|
136
|
+
|
|
137
|
+
if report.when == 'setup':
|
|
138
|
+
if not prev_test or test.container != prev_test.container:
|
|
139
|
+
# Show container
|
|
140
|
+
output = print_container(test.container)
|
|
141
|
+
return '', output, ('', {'white': True})
|
|
142
|
+
|
|
143
|
+
# Determine if this is the last test in the container
|
|
144
|
+
is_last_in_container = False
|
|
145
|
+
if hasattr(test.container, 'tests'):
|
|
146
|
+
# Find the index of this test in the container's test list
|
|
147
|
+
try:
|
|
148
|
+
idx = test.container.tests.index(test)
|
|
149
|
+
is_last_in_container = idx == len(test.container.tests) - 1
|
|
150
|
+
except (ValueError, AttributeError):
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
if report.when == 'call':
|
|
154
|
+
test.outcome = report.outcome
|
|
155
|
+
output = print_test(test)
|
|
156
|
+
if is_last_in_container:
|
|
157
|
+
output += '\n'
|
|
158
|
+
return report.outcome, output, ''
|
|
159
|
+
|
|
160
|
+
if report.when == 'setup' and report.skipped:
|
|
161
|
+
test.outcome = report.outcome
|
|
162
|
+
output = print_test(test)
|
|
163
|
+
if is_last_in_container:
|
|
164
|
+
output += '\n'
|
|
165
|
+
return report.outcome, output, ''
|
|
166
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-pyspec
|
|
3
|
+
Version: 0.10.1
|
|
4
|
+
Summary: A plugin that transforms the pytest output into a result similar to the RSpec. It enables the use of docstrings to display results and also enables the use of the prefixes "describe", "with" and "it".
|
|
5
|
+
Project-URL: Repository, https://github.com/felipecrp/pytest-pyspec
|
|
6
|
+
Author-email: Felipe Curty <felipecrp@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: <4,>=3.10
|
|
10
|
+
Requires-Dist: pytest<10,>=9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
[](https://github.com/felipecrp/pytest-pyspec/actions/workflows/pytest.yml)
|
|
14
|
+
|
|
15
|
+
The **pytest-pyspec** plugin provides semantics to the pytest output. It
|
|
16
|
+
transforms the pytest's output into a result similar to the RSpec.
|
|
17
|
+
|
|
18
|
+
The default pytest output is like the following:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
test/test_pytest.py::TestFunction::test_use_test_name PASSED
|
|
22
|
+
test/test_pytest.py::TestFunction::test_use_the_prefix_test PASSED
|
|
23
|
+
test/test_pytest.py::TestFunction::test_use_the_prefix_it PASSED
|
|
24
|
+
test/test_pytest.py::TestFunction::WithDocstring::test_use_docstring PASSED
|
|
25
|
+
```
|
|
26
|
+
The **pytest-pyspec** transforms the output into the following:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
A function
|
|
30
|
+
✓ use test name
|
|
31
|
+
✓ use the prefix test
|
|
32
|
+
✓ use the prefix it
|
|
33
|
+
|
|
34
|
+
A function
|
|
35
|
+
with docstring
|
|
36
|
+
✓ use docstring
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
You just need to prefix your test case classes with:
|
|
40
|
+
|
|
41
|
+
- _describe / test_ to represent objects
|
|
42
|
+
- _with / without_ to represent context
|
|
43
|
+
|
|
44
|
+
And prefix your tests with:
|
|
45
|
+
|
|
46
|
+
- _it / test_ to represent objects
|
|
47
|
+
|
|
48
|
+
The following is a sample test that generates the previous tests` output.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
class TestFunction:
|
|
52
|
+
def test_use_test_name(self):
|
|
53
|
+
assert 1 == 1
|
|
54
|
+
|
|
55
|
+
def test_use_the_prefix_test(self):
|
|
56
|
+
assert 1 == 1
|
|
57
|
+
|
|
58
|
+
def test_use_the_prefix_it(self):
|
|
59
|
+
assert 1 == 1
|
|
60
|
+
|
|
61
|
+
class WithDocstring:
|
|
62
|
+
def test_use_docstring(self):
|
|
63
|
+
assert 1 == 1
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Moreover, you can use a docstring to overwrite the test description. The
|
|
67
|
+
following tests have the same output as the previous tests:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
class TestA:
|
|
71
|
+
""" Function """
|
|
72
|
+
def test_1(self):
|
|
73
|
+
""" use test name """
|
|
74
|
+
assert 1 == 1
|
|
75
|
+
|
|
76
|
+
def test_2(self):
|
|
77
|
+
""" use the prefix test """
|
|
78
|
+
assert 1 == 1
|
|
79
|
+
|
|
80
|
+
def test_3(self):
|
|
81
|
+
""" use the prefix it """
|
|
82
|
+
assert 1 == 1
|
|
83
|
+
|
|
84
|
+
class TestB:
|
|
85
|
+
""" with docstring """
|
|
86
|
+
def test_4(self):
|
|
87
|
+
""" use docstring """
|
|
88
|
+
assert 1 == 1
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The following test sample:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import pytest
|
|
95
|
+
|
|
96
|
+
class DescribeHouse:
|
|
97
|
+
def it_has_door(self):
|
|
98
|
+
assert 1 == 1
|
|
99
|
+
|
|
100
|
+
class WithTwoFloors:
|
|
101
|
+
def it_has_stairs(self):
|
|
102
|
+
assert 1 == 1
|
|
103
|
+
|
|
104
|
+
def it_has_second_floor(self):
|
|
105
|
+
assert 1 == 1
|
|
106
|
+
|
|
107
|
+
def it_has_third_floor(self):
|
|
108
|
+
assert 1 == 2
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Generates the following output:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
test/test_sample.py
|
|
115
|
+
|
|
116
|
+
A house
|
|
117
|
+
✓ has door
|
|
118
|
+
|
|
119
|
+
A house
|
|
120
|
+
with two floors
|
|
121
|
+
✓ has stairs
|
|
122
|
+
✓ has second floor
|
|
123
|
+
✗ has third floor
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Installing and running **pySpec**
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install pytest pytest-pyspec
|
|
130
|
+
pytest --pyspec
|
|
131
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pytest_pyspec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pytest_pyspec/item.py,sha256=FCVWRd1O8EQ01eDYd7bh2v0jO-O3BERyZKIxs3WFSPo,8173
|
|
3
|
+
pytest_pyspec/output.py,sha256=VJAqNhd5695Dn5G6Jow3cfQkYXbfbcSquAf1IAivUVs,816
|
|
4
|
+
pytest_pyspec/plugin.py,sha256=_vx7g_2wq1AFV4mnC5tkYLbaK8xYAvlh1vstJRx35EQ,5664
|
|
5
|
+
pytest_pyspec-0.10.1.dist-info/METADATA,sha256=cWocxgEZURwKPat6Yn6Qrb5gS7iZp5wcd9Kf7RTcBL0,3148
|
|
6
|
+
pytest_pyspec-0.10.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
pytest_pyspec-0.10.1.dist-info/entry_points.txt,sha256=6YScWbxnkpduYs2Ef3gKOe6rV8Cjbj-OxCGMDNdV4Kc,48
|
|
8
|
+
pytest_pyspec-0.10.1.dist-info/licenses/LICENSE,sha256=PXKqh3rDWRsfhWsjBQ27qqYzE5E05n-eFRH95LsKRT4,1094
|
|
9
|
+
pytest_pyspec-0.10.1.dist-info/RECORD,,
|