Skip to content

Instantly share code, notes, and snippets.

@brycepg
Last active August 12, 2016 06:24
Show Gist options
  • Save brycepg/b10de7804c77f547ba3b6a32aad94e24 to your computer and use it in GitHub Desktop.
Save brycepg/b10de7804c77f547ba3b6a32aad94e24 to your computer and use it in GitHub Desktop.
Return Report of Procedural Functions using Decorators in Python
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"\"\"\"Use decorators to capture exceptions from a procedural call.\n",
"\n",
"Return a structure containing information about the call.\"\"\"\n",
"\n",
"\n",
"class TestInfo:\n",
" \"\"\"Contains data from function call.\n",
" \n",
" Includes status and any exception that occured\"\"\"\n",
" def __init__(self):\n",
" self.exc = None\n",
" self.status = None\n",
" self.info = None\n",
" def __repr__(self):\n",
" \"\"\"for IPython\"\"\"\n",
" return str(self.__dict__)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"import sys\n",
"from functools import wraps\n",
"def exc_info(func):\n",
" \"\"\"Decorator to return status information\"\"\"\n",
" @wraps(func)\n",
" def pack_exception(*args, **kwargs):\n",
" \"\"\"Return function status\n",
" \n",
" Requires test_info kwarg.\n",
" Pack the decorated function with an extra argument with test_info obj.\n",
" Catch exception to determine test_info status.\n",
" Return test_info object\"\"\"\n",
" t = TestInfo()\n",
" kwargs['test_info'] = t\n",
" try:\n",
" func(*args, **kwargs)\n",
" except Error:\n",
" t.status = \"failure\"\n",
" t.exc = sys.exc_info()\n",
" else:\n",
" t.status = \"good\"\n",
" return t\n",
" return pack_exception\n"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"class Error(Exception):\n",
" pass\n",
"\n",
"@exc_info\n",
"def bad_parse(dir_, test_name, test_info):\n",
" \"\"\"In this test an exception occurs before the side-effect happens\"\"\"\n",
" test_info.info = \"Specific info2\"\n",
" raise Error(\"Something happened\")\n",
" print(\"Did stuff on %s for %s\" % (dir_, test_name))\n",
" \n",
"@exc_info \n",
"def good_parse(dir_, test_name, test_info):\n",
" \"\"\"This test completes successfully\"\"\"\n",
" test_info.info = \"Specific information\"\n",
" print(\"Did stuff on %s for %s\" % (dir_, test_name))"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Did stuff on mydir for test1\n"
]
},
{
"data": {
"text/plain": [
"{'info': 'Specific information', 'status': 'good', 'exc': None}"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"good_parse(\"mydir\", \"test1\")"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"{'info': 'Specific info2', 'status': 'failure', 'exc': (<class '__main__.Error'>, Error('Something happened',), <traceback object at 0x7f5768845648>)}"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"bad_parse(\"mydir\", \"test2\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"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.4.5"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
@sheridp
Copy link

sheridp commented Aug 11, 2016

Looks good. One thing I might recommend is changing repr to actually print the traceback. Also, will this work if good_parse returned something? If not, I wonder if it might not be better to simply attach the test_info as an attribute of the function, which could be checked. IE. within the wrapper, call good_parse (which no longer takes a test_info object) and get whatever it returns. Then the user can check good_parse.test_info to see how the last call to it ran.

@sheridp
Copy link

sheridp commented Aug 11, 2016

Also, you might be able to clean / compact things a bit by using a decorator class rather than a function and moving TestInfo stuff into the decorator class ( again this would be more applicable if TestInfo was a function attribute rather than a returned object ).

@sheridp
Copy link

sheridp commented Aug 11, 2016

Here is an example of what I mean for the first comment

"""Use decorators to capture exceptions from a procedural call.

Return a structure containing information about the call."""


class TestInfo:
    """Contains data from function call.

    Includes status and any exception that occured"""
    def __init__(self):
        self.exc = None
        self.status = None
        self.info = None
    def __repr__(self):
        """for IPython"""
        return str(self.__dict__)

import sys
from functools import wraps
def exc_info(func):
    """Decorator to return status information"""

    func.t = TestInfo()
    @wraps(func)
    def pack_exception(*args, **kwargs):
        """Return function status

        Requires test_info kwarg.
        Pack the decorated function with an extra argument with test_info obj.
        Catch exception to determine test_info status.
        Return test_info object"""
        try:
            return func(*args, **kwargs)
        except Error:
            func.t.status = "failure"
            func.t.exc = sys.exc_info()
        else:
            func.t.status = "good"

    return pack_exception


class Error(Exception):
    pass

@exc_info
def bad_parse(dir_, test_name):
    """In this test an exception occurs before the side-effect happens"""
    #test_info.info = "Specific info2"
    raise Error("Something happened")
    print("Did stuff on %s for %s" % (dir_, test_name))

@exc_info    
def good_parse(dir_, test_name):
    """This test completes successfully"""
    #test_info.info = "Specific information"
    print("Did stuff on %s for %s" % (dir_, test_name))
    return 5

@brycepg
Copy link
Author

brycepg commented Aug 12, 2016

In the final version I pack the decorated functions return value, along with TestInfo object on return, if the decorated functions return value isn't None.
In this simplified version, I use the repr for readability in iPython. I need the TestInfo object to be returned even if there is a success to affirm that all functions were called without an exception.

I'm not sure how making the decorator a class will help. I need TestInfo to be a value-object returned to the caller to do analysis(this example is a proof of concept). I can define TestInfo class inside the decorator function exc_info however, which I think makes logical sense since nowhere else does it need to be used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment