Skip to content

Instantly share code, notes, and snippets.

@hamelin
Created August 14, 2023 18:54
Show Gist options
  • Save hamelin/8248912c06493f668a281dc76cdea782 to your computer and use it in GitHub Desktop.
Save hamelin/8248912c06493f668a281dc76cdea782 to your computer and use it in GitHub Desktop.
Pytest emulation directly in a notebook
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 8,
"id": "740bd213-ff52-4e48-8ffb-cdbc0df1d43d",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import ast\n",
"import inspect as ins\n",
"import itertools as it\n",
"from pathlib import Path"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "3bc959de-88da-48ae-8d40-ede18ad7e09d",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"TESTS = {}"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "7840b922-1189-4b8d-a578-b874c54eae2b",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"class pytest:\n",
" @staticmethod\n",
" def fixture(fn):\n",
" return fn(**{p: globals()[p] for p in ins.signature(fn).parameters.keys()})\n",
"\n",
" class mark:\n",
" @staticmethod\n",
" def parametrize(names_, seqs_params):\n",
" def parametrize_test(fn_test):\n",
" assert fn_test.__name__ in TESTS\n",
" names = names_.split(\",\")\n",
" paramz_new = []\n",
" for paramz in TESTS[fn_test.__name__]:\n",
" for i, values in enumerate(seqs_params):\n",
" if len(values) != len(names):\n",
" raise ValueError(\n",
" f\"Parameter sequence {i} carries {len(values)} \"\n",
" f\"values, but {len(values)} names must be bound.\"\n",
" )\n",
" paramz_new.append({**paramz, **dict(zip(names, values))})\n",
" TESTS[fn_test.__name__] = paramz_new\n",
" return fn_test\n",
" return parametrize_test"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "abde42d5-9c30-408b-bd5a-cc1bde3b985a",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"class request:\n",
" @staticmethod\n",
" def getfixturevalue(name):\n",
" G = globals()\n",
" if name not in G:\n",
" raise NameError(f\"Fixture {name} is not defined.\")\n",
" return G[name]"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "84eb5f19-faa3-40f7-99f7-902f8b264018",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"class TestDiscoverer(ast.NodeTransformer):\n",
"\n",
" def visit_FunctionDef(self, node):\n",
" if node.name.startswith(\"test_\"):\n",
" TESTS[node.name] = [{}]\n",
" return node\n",
"\n",
"\n",
"ipy = get_ipython()\n",
"ipy.ast_transformers.clear()\n",
"ipy.ast_transformers.append(TestDiscoverer())"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "ada94aeb-6818-4433-bfb0-a581fae4fec2",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"def printer_result(name, seq_paramz, width):\n",
" if len(seq_paramz) < 2:\n",
" return lambda _, label: print(f\"{name:{width}} ... {label}\")\n",
"\n",
" print(f\"{name}:\")\n",
" return lambda i, label: print(f\"{f' {i:4d}':{width}} ... {label}\")"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "9f92ea5e-8c0d-4633-94af-4a5151c5094c",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"if \"run_tests\" in globals():\n",
" try:\n",
" get_ipython().events.unregister(\"post_run_cell\", run_tests)\n",
" except ValueError as err:\n",
" if \"is not registered\" not in str(err):\n",
" raise\n",
"\n",
"\n",
"def run_tests(_):\n",
" G = globals()\n",
" if not TESTS:\n",
" return\n",
" width = max(len(n) for n in it.chain(TESTS.keys(), [f\" xxxx\"]))\n",
" print(\"=\" * 88)\n",
" for name, seq_paramz in TESTS.items():\n",
" if name not in G:\n",
" print(f\"{name:{width}}: missing\")\n",
" continue\n",
" assert seq_paramz\n",
" print_result = printer_result(name, seq_paramz, width)\n",
" fn_test = G[name]\n",
" sig = ins.signature(fn_test)\n",
" for i, paramz in enumerate(seq_paramz):\n",
" params = {**paramz}\n",
" for param in sig.parameters.keys():\n",
" if param not in params:\n",
" if param in G:\n",
" params[param] = G[param]\n",
" else:\n",
" raise ValueError(\n",
" f\"Missing fixture {param} necessary for test {name}\"\n",
" )\n",
"\n",
" try:\n",
" fn_test(**params)\n",
" print_result(i, \"OK\")\n",
" except AssertionError as err:\n",
" print_result(i, f\"fail at line {err.__traceback__.tb_next.tb_lineno}\")\n",
" except Exception as err:\n",
" print_result(i, f\"{str(err)} [{type(err).__name__}] at line {err.__traceback__.tb_next.tb_lineno}\")\n",
"\n",
"\n",
"get_ipython().events.register(\"post_run_cell\", run_tests)"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "c46bd043-be7d-4d2f-8204-b46727a1204e",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"test/conftest.py\n"
]
}
],
"source": [
"for path_conftest in Path(\".\").glob(\"**/conftest.py\"):\n",
" if not str(path_conftest).startswith(\".\"):\n",
" print(path_conftest)\n",
" with open(\"test/conftest.py\", \"r\", encoding=\"utf-8\") as file:\n",
" code = \"\\n\".join([line.rstrip() for line in file if \"import pytest\" not in line])\n",
" exec(code)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment