pytest-pyspec 0.5.3__py3-none-any.whl → 1.0.0__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.
Potentially problematic release.
This version of pytest-pyspec might be problematic. Click here for more details.
- pytest_pyspec/plugin.py +138 -30
- pytest_pyspec/tree.py +386 -0
- pytest_pyspec-1.0.0.dist-info/METADATA +202 -0
- pytest_pyspec-1.0.0.dist-info/RECORD +8 -0
- {pytest_pyspec-0.5.3.dist-info → pytest_pyspec-1.0.0.dist-info}/WHEEL +1 -1
- pytest_pyspec-1.0.0.dist-info/entry_points.txt +2 -0
- pytest_pyspec-1.0.0.dist-info/licenses/LICENSE +9 -0
- pytest_pyspec/item.py +0 -141
- pytest_pyspec/output.py +0 -30
- pytest_pyspec-0.5.3.dist-info/LICENSE +0 -674
- pytest_pyspec-0.5.3.dist-info/METADATA +0 -80
- pytest_pyspec-0.5.3.dist-info/RECORD +0 -9
- pytest_pyspec-0.5.3.dist-info/entry_points.txt +0 -3
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-pyspec
|
|
3
|
+
Version: 1.0.0
|
|
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
|
+
Project-URL: Issues, https://github.com/felipecrp/pytest-pyspec/issues
|
|
7
|
+
Author-email: Felipe Curty <felipecrp@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Operating System :: POSIX
|
|
15
|
+
Classifier: Operating System :: Unix
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: <4,>=3.10
|
|
26
|
+
Requires-Dist: pytest<10,>=9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
[](https://github.com/felipecrp/pytest-pyspec/actions/workflows/pytest.yml)
|
|
30
|
+
|
|
31
|
+
# pytest-pyspec
|
|
32
|
+
|
|
33
|
+
The **pytest-pyspec** plugin transforms pytest output into a beautiful, readable format similar to RSpec. It provides semantic meaning to your tests by organizing them into descriptive hierarchies.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Semantic Output**: Transform pytest's default output into readable, hierarchical descriptions
|
|
38
|
+
- **Multiple Prefixes**: Support for `describe/test` (objects), `with/without/when` (contexts), and `it/test` (tests)
|
|
39
|
+
- **Docstring Support**: Override test descriptions using docstrings
|
|
40
|
+
- **Consolidated Output**: Smart grouping that avoids repeating parent headers
|
|
41
|
+
- **Natural Language**: Automatic lowercase formatting of common words (the, is, are, etc.)
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install pytest pytest-pyspec
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Running
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pytest --pyspec
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
### Car Scenario
|
|
60
|
+
|
|
61
|
+
A minimal car example with properties and behaviors:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
class DescribeCar:
|
|
65
|
+
def test_has_engine(self):
|
|
66
|
+
assert True
|
|
67
|
+
|
|
68
|
+
class WithFullTank:
|
|
69
|
+
def test_drive_long_distance(self):
|
|
70
|
+
assert True
|
|
71
|
+
|
|
72
|
+
class WithoutFuel:
|
|
73
|
+
def test_cannot_start_engine(self):
|
|
74
|
+
assert True
|
|
75
|
+
|
|
76
|
+
class WhenTheEngineIsRunning:
|
|
77
|
+
def test_consumes_fuel(self):
|
|
78
|
+
assert True
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
With **pytest-pyspec**, this produces:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
a Car
|
|
85
|
+
✓ has engine
|
|
86
|
+
|
|
87
|
+
with Full Tank
|
|
88
|
+
✓ drive long distance
|
|
89
|
+
|
|
90
|
+
without Fuel
|
|
91
|
+
✓ cannot start engine
|
|
92
|
+
|
|
93
|
+
when the Engine is Running
|
|
94
|
+
✓ consumes fuel
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Available Prefixes
|
|
98
|
+
|
|
99
|
+
**pytest-pyspec** supports three types of prefixes to create semantic test hierarchies:
|
|
100
|
+
|
|
101
|
+
#### 1. Object Classes (use `describe` or `test`)
|
|
102
|
+
|
|
103
|
+
Define what you're testing:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
class DescribeCar: # or class TestCar:
|
|
107
|
+
def test_has_four_wheels(self):
|
|
108
|
+
assert True
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Output:
|
|
112
|
+
```
|
|
113
|
+
a Car
|
|
114
|
+
✓ has four wheels
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### 2. Context Classes (use `with`, `without`, or `when`)
|
|
118
|
+
|
|
119
|
+
Define the context or state:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
class DescribeCar:
|
|
123
|
+
class WithFullTank:
|
|
124
|
+
def test_can_drive_long_distances(self):
|
|
125
|
+
assert True
|
|
126
|
+
|
|
127
|
+
class WithoutFuel:
|
|
128
|
+
def test_cannot_start_engine(self):
|
|
129
|
+
assert True
|
|
130
|
+
|
|
131
|
+
class WhenTheEngineIsRunning:
|
|
132
|
+
def test_consumes_fuel(self):
|
|
133
|
+
assert True
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Output:
|
|
137
|
+
```
|
|
138
|
+
a Car
|
|
139
|
+
with Full Tank
|
|
140
|
+
✓ can drive long distances
|
|
141
|
+
|
|
142
|
+
without Fuel
|
|
143
|
+
✓ cannot start engine
|
|
144
|
+
|
|
145
|
+
when the Engine is Running
|
|
146
|
+
✓ consumes fuel
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### 3. Test Functions (use `it_` or `test_`)
|
|
150
|
+
|
|
151
|
+
Define the expected behavior:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
class DescribeCar:
|
|
155
|
+
def it_has_four_wheels(self):
|
|
156
|
+
assert True
|
|
157
|
+
|
|
158
|
+
def test_has_engine(self):
|
|
159
|
+
assert True
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Output:
|
|
163
|
+
```
|
|
164
|
+
a Car
|
|
165
|
+
✓ has four wheels
|
|
166
|
+
✓ has engine
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Using Docstrings
|
|
170
|
+
|
|
171
|
+
Override automatic naming with custom descriptions:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
class TestCar:
|
|
175
|
+
"""sports car"""
|
|
176
|
+
|
|
177
|
+
def test_top_speed(self):
|
|
178
|
+
"""reaches 200 mph"""
|
|
179
|
+
assert True
|
|
180
|
+
|
|
181
|
+
class WhenTheNitroIsActivated:
|
|
182
|
+
"""when nitro boost is activated"""
|
|
183
|
+
|
|
184
|
+
def test_acceleration(self):
|
|
185
|
+
"""accelerates rapidly"""
|
|
186
|
+
assert True
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Output:
|
|
190
|
+
```
|
|
191
|
+
a sports car
|
|
192
|
+
✓ reaches 200 mph
|
|
193
|
+
|
|
194
|
+
when nitro boost is activated
|
|
195
|
+
✓ accelerates rapidly
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Configuration
|
|
199
|
+
|
|
200
|
+
The plugin is automatically enabled when you use the `--pyspec` flag. No additional configuration is required.
|
|
201
|
+
|
|
202
|
+
For more information, see the [documentation](https://github.com/felipecrp/pytest-pyspec/blob/main/doc/README.md).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pytest_pyspec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pytest_pyspec/plugin.py,sha256=HS3iA2Xiz9LzdiB_k-2K0YpLtz5vF72Q7yS6l7LmhRk,6869
|
|
3
|
+
pytest_pyspec/tree.py,sha256=1Qg0oh7T-__vQEC9w828GI79lLAPrfzZx8wcPDY8JsE,14812
|
|
4
|
+
pytest_pyspec-1.0.0.dist-info/METADATA,sha256=O8ZEekcDGnnxQm0rOjxmET5GMmgWoK_tQY6_uyB1uz4,4916
|
|
5
|
+
pytest_pyspec-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
pytest_pyspec-1.0.0.dist-info/entry_points.txt,sha256=6YScWbxnkpduYs2Ef3gKOe6rV8Cjbj-OxCGMDNdV4Kc,48
|
|
7
|
+
pytest_pyspec-1.0.0.dist-info/licenses/LICENSE,sha256=PXKqh3rDWRsfhWsjBQ27qqYzE5E05n-eFRH95LsKRT4,1094
|
|
8
|
+
pytest_pyspec-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright 2024 Felipe Curty and others
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
pytest_pyspec/item.py
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
from types import ModuleType
|
|
2
|
-
from typing import Dict, List
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
|
-
class Item:
|
|
6
|
-
def __init__(self, item: pytest.Item) -> None:
|
|
7
|
-
self._item = item
|
|
8
|
-
|
|
9
|
-
@property
|
|
10
|
-
def description(self):
|
|
11
|
-
description = self._item.obj.__doc__
|
|
12
|
-
|
|
13
|
-
if not description:
|
|
14
|
-
description = self._item.name
|
|
15
|
-
description = self._parse_description(description)
|
|
16
|
-
|
|
17
|
-
description = description.splitlines()[0]
|
|
18
|
-
description = description.capitalize()
|
|
19
|
-
return description
|
|
20
|
-
|
|
21
|
-
def _parse_description(self, description: str):
|
|
22
|
-
split = False
|
|
23
|
-
if description.lower().startswith("it_"):
|
|
24
|
-
description = description.replace('it_', '')
|
|
25
|
-
split = True
|
|
26
|
-
|
|
27
|
-
if description.lower().startswith("test_it_"):
|
|
28
|
-
description = description.replace('test_it_', '')
|
|
29
|
-
split = True
|
|
30
|
-
|
|
31
|
-
if description.lower().startswith("test_describe_"):
|
|
32
|
-
description = description.replace('test_describe_', '')
|
|
33
|
-
split = True
|
|
34
|
-
|
|
35
|
-
if description.lower().startswith("describe_"):
|
|
36
|
-
description = description.replace('describe_', '')
|
|
37
|
-
split = True
|
|
38
|
-
|
|
39
|
-
if split:
|
|
40
|
-
description = description.replace('_', ' ')
|
|
41
|
-
|
|
42
|
-
return description
|
|
43
|
-
|
|
44
|
-
def __repr__(self):
|
|
45
|
-
return self._item.__repr__()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class Test(Item):
|
|
49
|
-
def __init__(self, item: pytest.Item) -> None:
|
|
50
|
-
super().__init__(item)
|
|
51
|
-
self.container: Container = None
|
|
52
|
-
self.outcome: str = None
|
|
53
|
-
|
|
54
|
-
@property
|
|
55
|
-
def level(self) -> int:
|
|
56
|
-
if not self.container:
|
|
57
|
-
return 0
|
|
58
|
-
return self.container.level +1
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class Container(Item):
|
|
62
|
-
def __init__(self, item: pytest.Item):
|
|
63
|
-
super().__init__(item)
|
|
64
|
-
self.tests: List[Test] = list()
|
|
65
|
-
self.containers: List[Container] = list()
|
|
66
|
-
self.parent = None
|
|
67
|
-
|
|
68
|
-
def add(self, test: Test):
|
|
69
|
-
self.tests.append(test)
|
|
70
|
-
test.container = self
|
|
71
|
-
|
|
72
|
-
def add_container(self, container: 'Container'):
|
|
73
|
-
self.containers.append(container)
|
|
74
|
-
container.parent = self
|
|
75
|
-
|
|
76
|
-
def flat_list(self):
|
|
77
|
-
containers = []
|
|
78
|
-
container = self
|
|
79
|
-
while container:
|
|
80
|
-
containers.insert(0, container)
|
|
81
|
-
container = container.parent
|
|
82
|
-
return containers
|
|
83
|
-
|
|
84
|
-
@property
|
|
85
|
-
def level(self) -> int:
|
|
86
|
-
level = 0
|
|
87
|
-
|
|
88
|
-
item = self._item
|
|
89
|
-
while item.parent and not isinstance(item.parent.obj, ModuleType):
|
|
90
|
-
level += 1
|
|
91
|
-
item = item.parent
|
|
92
|
-
|
|
93
|
-
return level
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class ItemFactory:
|
|
97
|
-
def __init__(self) -> None:
|
|
98
|
-
self.container_factory = ContainerFactory()
|
|
99
|
-
|
|
100
|
-
def create(self, item: pytest.Item) -> Test:
|
|
101
|
-
test_item = Test(item)
|
|
102
|
-
|
|
103
|
-
container_item = item.parent
|
|
104
|
-
container = self.container_factory.create(container_item)
|
|
105
|
-
if container:
|
|
106
|
-
container.add(test_item)
|
|
107
|
-
|
|
108
|
-
return test_item
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
class ContainerFactory:
|
|
112
|
-
def __init__(self) -> None:
|
|
113
|
-
self.containers: Dict[str, Container] = dict()
|
|
114
|
-
|
|
115
|
-
def create(self, item) -> Container:
|
|
116
|
-
containers = self._create_containers(item)
|
|
117
|
-
if not containers:
|
|
118
|
-
return None
|
|
119
|
-
|
|
120
|
-
return containers[-1]
|
|
121
|
-
|
|
122
|
-
def _create_unique_container(self, item):
|
|
123
|
-
if item not in self.containers:
|
|
124
|
-
container = Container(item)
|
|
125
|
-
self.containers[item] = container
|
|
126
|
-
|
|
127
|
-
container = self.containers.get(item)
|
|
128
|
-
return container
|
|
129
|
-
|
|
130
|
-
def _create_containers(self, item):
|
|
131
|
-
containers = []
|
|
132
|
-
child_container = None
|
|
133
|
-
while item and not isinstance(item.obj, ModuleType):
|
|
134
|
-
container = self._create_unique_container(item)
|
|
135
|
-
if child_container:
|
|
136
|
-
container.add_container(child_container)
|
|
137
|
-
|
|
138
|
-
containers.insert(0, container)
|
|
139
|
-
child_container = container
|
|
140
|
-
item = item.parent
|
|
141
|
-
return containers
|
pytest_pyspec/output.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from .item import Container, Test
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def print_container(container: Container):
|
|
6
|
-
output = "\n"
|
|
7
|
-
if container:
|
|
8
|
-
for container in container.flat_list():
|
|
9
|
-
output += print_parent_container(container)
|
|
10
|
-
container = container.parent
|
|
11
|
-
return output
|
|
12
|
-
|
|
13
|
-
def print_parent_container(container: Container):
|
|
14
|
-
ident = " " * container.level
|
|
15
|
-
output = f"\n{ident}{container.description}"
|
|
16
|
-
return output
|
|
17
|
-
|
|
18
|
-
def print_test(test: Test):
|
|
19
|
-
ident = " " * test.level
|
|
20
|
-
status = print_test_status(test)
|
|
21
|
-
output = f"\n{ident}{status} {test.description}"
|
|
22
|
-
return output
|
|
23
|
-
|
|
24
|
-
def print_test_status(test: Test):
|
|
25
|
-
if test.outcome == 'passed':
|
|
26
|
-
return '✓'
|
|
27
|
-
elif test.outcome == 'failed':
|
|
28
|
-
return '✗'
|
|
29
|
-
else:
|
|
30
|
-
return '»'
|