Skip to content

Instantly share code, notes, and snippets.

@bmorris3
Last active October 31, 2023 18:30
Show Gist options
  • Save bmorris3/afbf24bdcd9aa0458f017f2b6af9a66b to your computer and use it in GitHub Desktop.
Save bmorris3/afbf24bdcd9aa0458f017f2b6af9a66b to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"id": "599b4a29-add9-4853-9f98-1cccf31ca75c",
"metadata": {},
"source": [
"## Reproducible history logging via decorators and docstrings"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "9bb1d22b-9204-4b8e-8fab-d278731cf9b4",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import os\n",
"import re\n",
"import sys\n",
"import logging\n",
"import inspect\n",
"\n",
"log = logging.getLogger(\"history\")\n",
"log.setLevel(logging.INFO)\n",
"\n",
"filename = 'history.py'\n",
"if os.path.exists(filename):\n",
" os.remove(filename)\n",
"\n",
"file_handler = logging.FileHandler(filename=filename, mode='a')\n",
"file_handler.setFormatter(logging.Formatter())\n",
"log.addHandler(file_handler)\n",
"\n",
"stream_handler = logging.StreamHandler(sys.stdout)\n",
"log.addHandler(stream_handler)\n",
"\n",
"\n",
"def log_in_history(func):\n",
" def wrapper(self, *args, **kwargs):\n",
" signature = inspect.signature(func)\n",
" keywords = {}\n",
" for i, (name, param) in enumerate(signature.parameters.items()):\n",
" if name == 'self':\n",
" continue\n",
" if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:\n",
" keywords[name] = args[i-1]\n",
" elif param.kind == inspect.Parameter.KEYWORD_ONLY:\n",
" if name in kwargs:\n",
" keywords[name] = kwargs[name]\n",
" else:\n",
" keywords[name] = param.default\n",
"\n",
" # get docstring for this function:\n",
" docstring = inspect.getdoc(func)\n",
" \n",
" # extract \"API Calls\" section from docstring:\n",
" python_calls = api_docstring_to_python(\n",
" docstring, keywords\n",
" )\n",
" \n",
" # add trailing newline:\n",
" log.error(python_calls + '\\n')\n",
" \n",
" # don't forget to run the function, too!\n",
" func(self, *args, **kwargs)\n",
"\n",
" # ensure func's docstring etc. is still attached to \n",
" # the wrapped method:\n",
" sig = inspect.signature(func)\n",
" wrapper.__signature__ = sig\n",
" wrapper.__doc__ = func.__doc__\n",
" wrapper.__annotations__ = func.__annotations__\n",
" wrapper.__name__ = func.__name__\n",
" \n",
" return wrapper\n",
"\n",
"\n",
"def api_docstring_to_python(docstring, keywords):\n",
" api_calls = docstring.split(\n",
" 'API Calls\\n---------\\n'\n",
" )[1]\n",
" \n",
" vars_to_format = re.findall(r'\\{(.*?)\\}', api_calls)\n",
" \n",
" define_vars = \"\"\n",
" for var in keywords:\n",
" if var not in vars_to_format:\n",
" define_vars += f\"{var} = {keywords[var]}\\n\"\n",
"\n",
" return define_vars + api_calls.format(**keywords)\n",
"\n",
"class Demo:\n",
" \n",
" # learn how to use the `/` and `*` in call signatures\n",
" # to specify position only, positional or keyword, or \n",
" # keyword only arguments:\n",
" # https://stackoverflow.com/a/61719220/1340208\n",
"\n",
" @log_in_history\n",
" def go(self, multiple, *, pi=3.1415926):\n",
" \"\"\"\n",
" Do this simple `go` function.\n",
" \n",
" API Calls\n",
" ---------\n",
" result = multiple * pi\n",
" \"\"\"\n",
" # the function's contents need not be\n",
" # strictly identical to the public API \n",
" # version that we offer in the log\n",
"\n",
" return multiple * pi\n",
" \n",
" @log_in_history\n",
" def go_again(self, multiple, *, pi=3.1415926):\n",
" \"\"\"\n",
" Do this simple `go_again` function.\n",
" \n",
" API Calls\n",
" ---------\n",
" # some notes\n",
" pi = {pi}\n",
" \n",
" # some more notes\n",
" multiple = {multiple}\n",
" \n",
" result = multiple * pi\n",
" \"\"\"\n",
" # the function's contents need not be\n",
" # strictly identical to the public API \n",
" # version that we offer in the API Calls\n",
" # section of the docstring\n",
"\n",
" return multiple * pi\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "46f7bd73-e180-4ab7-aa49-803d1b871d82",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"multiple = 2\n",
"pi = 3.1415926\n",
"result = multiple * pi\n",
"\n"
]
}
],
"source": [
"demo = Demo()\n",
"\n",
"demo.go(2)"
]
},
{
"cell_type": "markdown",
"id": "3ea08865-b2d8-48f7-9122-070b04dc20af",
"metadata": {},
"source": [
"This docstring doesn't pre-format the values for `multiple` or `pi`, but the decorator automatically adds their definitions at the top.\n"
]
},
{
"cell_type": "markdown",
"id": "d7b71b06-d96a-4e42-a687-86ddffc627ac",
"metadata": {},
"source": [
"***"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "94353058-d8ba-4dd1-8ae1-fad1038ab722",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"# some notes\n",
"pi = 3.1415926\n",
"\n",
"# some more notes\n",
"multiple = 2\n",
"\n",
"result = multiple * pi\n",
"\n"
]
}
],
"source": [
"demo = Demo()\n",
"\n",
"demo.go_again(2)"
]
},
{
"cell_type": "markdown",
"id": "a26318b3-a8bf-42f2-ae5d-0bf544c278b3",
"metadata": {},
"source": [
"This docstring *does* pre-format the values for `multiple` and `pi`, so they're not added to the top."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b256924b-1272-48c8-8657-12e8280bc504",
"metadata": {},
"outputs": [],
"source": []
}
],
"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