pytest-testbook 1.1.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.
@@ -0,0 +1 @@
1
+ __version__ = '0.0.20'
@@ -0,0 +1,9 @@
1
+ """Pytest-Testbook pytest hooks."""
2
+
3
+
4
+ def pytest_testbook_kernel_setup(scenario):
5
+ """Will be called after the Jupyter kernel is started."""
6
+
7
+
8
+ def pytest_testbook_kernel_teardown(scenario):
9
+ """Will be called before the Jupyter kernel is shut down."""
@@ -0,0 +1,205 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import sys
4
+ import pytest
5
+ import nbformat
6
+ import re
7
+ import pprint
8
+ from pathlib import Path
9
+ from queue import Empty
10
+ from jupyter_client import KernelManager
11
+
12
+ # Modern global state management (replacing sys.modules hack)
13
+ _km = None
14
+ _kc = None
15
+ _setup_done = False
16
+
17
+
18
+ def pytest_addhooks(pluginmanager):
19
+ """Register plugin hooks."""
20
+ try:
21
+ from pytest_testbook import hooks
22
+ pluginmanager.add_hookspecs(hooks)
23
+ except ImportError:
24
+ pass # Handle gracefully if the hooks module is unavailable
25
+
26
+
27
+ def pytest_collect_file(file_path, parent):
28
+ """Modern pytest hook using pathlib.Path (file_path)."""
29
+ if file_path.suffix == ".ipynb":
30
+ # Use from_parent instead of direct instantiation
31
+ return Testbook.from_parent(parent, path=file_path)
32
+
33
+
34
+ def pytest_sessionstart(session):
35
+ """ before session.main() is called. """
36
+ global _km, _kc
37
+ _km = KernelManager()
38
+ _km.start_kernel()
39
+ _kc = _km.client()
40
+ _kc.start_channels()
41
+ _kc.wait_for_ready()
42
+
43
+
44
+ def pytest_sessionfinish(session, exitstatus):
45
+ """ whole test run finishes. """
46
+ global _km, _kc
47
+ if _kc:
48
+ # Quits the browser if it still exists
49
+ _kc.execute("try:\n browser.quit()\nexcept:\n pass\n", allow_stdin=False)
50
+ _kc.stop_channels()
51
+ if _km:
52
+ _km.shutdown_kernel(now=True)
53
+
54
+
55
+ class Testbook(pytest.File):
56
+ def collect(self):
57
+ # Modern pytest uses self.path (pathlib.Path)
58
+ nb = nbformat.read(self.path.open(encoding="utf-8"), 4)
59
+
60
+ self.km = _km
61
+ self.kc = _kc
62
+ self.case = ""
63
+ setup = False
64
+ self.test_setup = []
65
+
66
+ name = "Default_Name"
67
+
68
+ for cell in nb.cells:
69
+ if cell.cell_type == 'markdown':
70
+ if "## Test Results" in cell.source:
71
+ return
72
+ if '## Test Configurations' in cell.source or '## Environmental Needs' in cell.source:
73
+ setup = True
74
+ continue
75
+ if cell.source.startswith("## TC"):
76
+ case, _, _ = cell.source.partition("](https")
77
+ self.case = case.replace("## ", "").replace("[", "")
78
+
79
+ for step in ["### Given", "### And", "### When", "### Then", "### But"]:
80
+ if cell.source.startswith(step):
81
+ setup = False
82
+ self.header = cell.source.split("\n")[0].replace("### ", "")
83
+ header_clean = re.sub(r'### |\(|\)|\"|\'', '', self.header)
84
+ # Modern path name resolution
85
+ self.header = f"{self.path.stem}.{self.header}"
86
+ name = header_clean.strip().replace(" ", "_").lower()
87
+
88
+ elif cell.cell_type == 'code' and nb.metadata.get('kernelspec', {}).get('language') == 'python':
89
+ if setup:
90
+ self.test_setup.append(cell.source)
91
+ continue
92
+ if name == "Default_Name":
93
+ continue
94
+
95
+ # Modern node yielding via from_parent
96
+ yield Teststep.from_parent(
97
+ self,
98
+ name=name,
99
+ case=self.case,
100
+ header=self.header,
101
+ cell=cell
102
+ )
103
+
104
+ def setup(self):
105
+ # Safely trigger hooks if they exist
106
+ if hasattr(self.config.hook, 'pytest_testbook_kernel_setup'):
107
+ self.config.hook.pytest_testbook_kernel_setup(scenario=self)
108
+
109
+ global _setup_done
110
+ if not _setup_done:
111
+ for setup_code in self.test_setup:
112
+ send_and_execute(self, setup_code)
113
+ _setup_done = True
114
+
115
+ def teardown(self):
116
+ if hasattr(self.config.hook, 'pytest_testbook_kernel_teardown'):
117
+ self.config.hook.pytest_testbook_kernel_teardown(scenario=self)
118
+
119
+
120
+ class TestbookException(Exception):
121
+ """ custom exception for error reporting. """
122
+
123
+
124
+ def send_and_execute(item, source, allow_stdin=False):
125
+ if isinstance(item, Testbook):
126
+ kernel = item.kc
127
+ elif isinstance(item, Teststep):
128
+ kernel = item.parent.kc
129
+ else:
130
+ raise TestbookException("Unknown Item")
131
+
132
+ run_id = kernel.execute(source, allow_stdin=allow_stdin)
133
+ timeout = 1800 # 1800 seconds == 30 minutes
134
+
135
+ while True:
136
+ try:
137
+ reply = kernel.get_shell_msg(timeout=timeout)
138
+ if reply.get("parent_header", {}) and reply["parent_header"].get("msg_id") == run_id:
139
+ break
140
+ except Empty:
141
+ # Fixed old string formatting bug here
142
+ raise TestbookException(f"Timeout of {timeout} seconds exceeded executing cell: {source}")
143
+
144
+ status = reply['content'].get('status')
145
+
146
+ if status == 'ok':
147
+ return "Test successfully completed."
148
+
149
+ elif status == 'error':
150
+ _traceback = reply['content'].get('traceback', [])
151
+ colored_traceback = "\n".join(_traceback)
152
+ uncolored_traceback = re.sub(r'\x1b[^m]*m', '', "\n".join(_traceback))
153
+
154
+ if isinstance(item, Teststep):
155
+ item._location = (item.location[0], item.location[1], item.header)
156
+ if item.config.getvalue("color") == 'yes':
157
+ item.traceback = colored_traceback
158
+ else:
159
+ item.traceback = uncolored_traceback
160
+ elif isinstance(item, Testbook):
161
+ if item.config.getvalue("color") == 'yes':
162
+ print(colored_traceback)
163
+ else:
164
+ print(uncolored_traceback)
165
+ raise TestbookException(source, _traceback)
166
+
167
+ elif status == 'aborted':
168
+ raise TestbookException(source, "Test was aborted")
169
+ else:
170
+ pprint.pprint(reply)
171
+ pprint.pprint(reply['content'])
172
+ raise TestbookException(source, "Unknown Status Code")
173
+
174
+
175
+ class Teststep(pytest.Item):
176
+
177
+ @classmethod
178
+ def from_parent(cls, parent, *, name, case, header, cell):
179
+ # Modern initialization overriding from_parent
180
+ obj = super().from_parent(parent, name=name)
181
+ obj.case = case
182
+ obj.header = header
183
+ obj.cell = cell
184
+ return obj
185
+
186
+ def runtest(self):
187
+ send_and_execute(self, self.cell.source)
188
+
189
+ def repr_failure(self, excinfo):
190
+ if isinstance(excinfo.value, TestbookException):
191
+ # Modern Python attribute checking
192
+ tb = getattr(self, 'traceback', "\n".join(str(a) for a in excinfo.value.args))
193
+ return f"{self.cell.source}\n\n{tb}"
194
+ return super().repr_failure(excinfo)
195
+
196
+
197
+ def pytest_addoption(parser):
198
+ group = parser.getgroup('testbook')
199
+ group.addoption(
200
+ '--kernel_reuse',
201
+ action='store',
202
+ dest='dest_foo', # Note: ensure this destination variable is intentional
203
+ default='no',
204
+ help='Do you want to allow multiple testbooks to be run on the same kernel?'
205
+ )
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-testbook
3
+ Version: 1.1.0
4
+ Summary: A pytest plugin for executing Jupyter Notebooks as tests
5
+ Author: ldiary
6
+ Classifier: Framework :: Pytest
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pytest>=5.4.0
13
+ Requires-Dist: nbformat
14
+ Requires-Dist: jupyter_client
15
+ Dynamic: license-file
@@ -0,0 +1,9 @@
1
+ pytest_testbook/__init__.py,sha256=m3MwU52xKwEwFoINAThbVIthInA2BGrGh6ptKPsP4pM,22
2
+ pytest_testbook/hooks.py,sha256=nabJhTXQtEa0lGNU0VdQWuP9FvEVZgrwzN8pSlgub8k,267
3
+ pytest_testbook/plugin.py,sha256=yBx8D6eAKW6RPI9DeFJ1caTzD-7mS2NmJWej0FRYSx8,7093
4
+ pytest_testbook-1.1.0.dist-info/licenses/LICENSE,sha256=AYrfjfj6541GHP-6LqG6UofAdUGvzhLw7EXrndAnkXM,1106
5
+ pytest_testbook-1.1.0.dist-info/METADATA,sha256=l_SV8g7zh631fYkushdGS7qhyVe51F2AX8-JtAZbWh8,472
6
+ pytest_testbook-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ pytest_testbook-1.1.0.dist-info/entry_points.txt,sha256=S1g8asOHTeEfUkSEO2gMRa8WUVvEoBVmPCmSccesNQc,45
8
+ pytest_testbook-1.1.0.dist-info/top_level.txt,sha256=PlJoz-wzP8y8ZjxJUiiB6Nga51_hqVv2h-upkn_IDkc,16
9
+ pytest_testbook-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ testbook = pytest_testbook.plugin
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Ernesto D. Luzon Jr.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ pytest_testbook