Last active
October 31, 2023 18:30
-
-
Save bmorris3/afbf24bdcd9aa0458f017f2b6af9a66b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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