{ "metadata": { "name": "", "signature": "sha256:95c087ab96d342fa198cf2959944b1f15389c72b418a337b1250c4ae1d95a4bb" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Here lives documentation for ``patched``\n", "
\n", "Back to [GitHub repository](https://github.com/abetkin/patched)\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Sections\n", "- [Introductory examples](#intro)\n", "\n", "- [\"Global\" objects: config, storage, logger](#global_objects)\n", "\n", "- [Smart logging](#logger)\n", "\n", "- [Comparison with other frameworks. Roadmap](#roadmap)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Introductory examples" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The most straightforward way to use the package is namely for patching. ``patched`` allows you to replace one callable with another.\n", "\n", "Let's see the" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###simplest example\n", "Suppose we have a `Calculator` class:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class Calculator:\n", " def eval(self, string):\n", " return eval(string)\n", "\n", "calc = Calculator()\n", "calc.eval('3+4')" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 4, "text": [ "7" ] } ], "prompt_number": 4 }, { "cell_type": "markdown", "metadata": {}, "source": [ "`PatchSuite` is a suite of patches. The suite below consists of 1 patch:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import random\n", "from patched import PatchSuite, patch\n", "\n", "class BrokenCalc(PatchSuite):\n", " \n", " @patch(parent=Calculator)\n", " def eval(self, string):\n", " value = eval(string)\n", " random_inacuracy = 0.01 * value * random.random()\n", " return value + 0.01 + random_inacuracy\n", "\n", "with BrokenCalc():\n", " print( calc.eval('3+4'))" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "7.010385867746241\n" ] } ], "prompt_number": 5 }, { "cell_type": "markdown", "metadata": {}, "source": [ "`parent` is the object being patched (a module, for example). The above patch suite definition is equivalent to this:\n", "\n", " class BrokenCalc(PatchSuite):\n", " class Meta:\n", " parent = Calculator\n", " \n", " @patch(wrapper_type=wrappers.Replacement)\n", " def eval(self, string):\n", " # ...\n", " \n", "(Keyword) arguments declared in `Meta` class are the default ones for each patch.\n", "\n", "`Replacement` wrapper type is the default one, so passing it has no effect. It means that the decorated function replaces the original function. The other available types are\n", "\n", "- `Hook`: the decorated function is executed after the original one, the return value of the latter being known\n", "\n", "- `Insertion`: inserted attribute is missing from the original parent object" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "Let's see a more interesting\n", "###example" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You know the `Counter` class from the `collections` module:\n" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import collections\n", "\n", "collections.Counter('animal')" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 5, "text": [ "Counter({'a': 2, 'm': 1, 'i': 1, 'n': 1, 'l': 1})" ] } ], "prompt_number": 5 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's make it count only to 0:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from patched import wrappers\n", "\n", "@patch(parent=collections, attribute='Counter')\n", "class MyCounter(collections.Counter):\n", " def __init__(self, *args, **kw):\n", " c = collections.Counter(*args, **kw)\n", " zeros_dict = dict.fromkeys(c.keys(), 0)\n", " super().__init__(zeros_dict)\n", "\n", "with PatchSuite([MyCounter.make_patch()]):\n", " print( collections.Counter('animal'))" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "MyCounter({'m': 0, 'i': 0, 'n': 0, 'a': 0, 'l': 0})\n" ] } ], "prompt_number": 22 }, { "cell_type": "markdown", "metadata": {}, "source": [ "The code seems to be totally different from the previous example. First: the callables being swapped are not functions, but classes (we replace `Counter` with `MyCounter`). It's not the common and recommended way, but for the purposes of this introductory section..\n", "\n", "Second: patches are not collected from the testsuite declaration but passed to it explicitly. And somehow `MyCounter` has `make_patch` attribute..\n", "\n", "Well, `patch` is a class, and it's instances are meant to show \"I want to make a patch out of this\". It defines `__call__()` method that accepts callable as a parameter which means `patch` instance can be used as a decorator. Only `patch` replaces the callable it decorates, so\n" ] }, { "cell_type": "code", "collapsed": false, "input": [ "type(MyCounter) == patch" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 7, "text": [ "True" ] } ], "prompt_number": 7 }, { "cell_type": "code", "collapsed": false, "input": [ "MyCounter" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 8, "text": [ "{'wrapper_func': __main__.MyCounter,\n", " 'wrapper_type': patched.patching.wrappers.Replacement,\n", " 'attribute': 'Counter',\n", " 'parent': }" ] } ], "prompt_number": 8 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Yeah, `patch` inherits `dict`.\n", "\n", "As I said, `patch` instance only marks an attribute as a future patch and can provide parameters to it. The real patch is constucted here: " ] }, { "cell_type": "code", "collapsed": false, "input": [ "MyCounter.make_patch()" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 9, "text": [ "Patch collections.Counter" ] } ], "prompt_number": 9 }, { "cell_type": "code", "collapsed": false, "input": [ "type(_)" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 10, "text": [ "patched.patching.base.Patch" ] } ], "prompt_number": 10 }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Note:** inside the `MyCounter` callable we have used `collections.Counter(...)` and it really points to original callable there. That's because the patch is undone for the time of execution of our replacement callable. Respective lines from the source code:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "load -r 31-35 ../patched/patching/wrappers.py" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 29 }, { "cell_type": "code", "collapsed": false, "input": [ " patch.off()\n", " try:\n", " return self.run(*args, **kwargs)\n", " finally:\n", " patch.on()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Previous example didn't actually stop `Counter` from counting: it patched only it's `__init__` method, but you can do all sorts of operations with counters.\n", "\n", "Let's make a more\n", "\n", "### bulletproof example" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class StopCounting(PatchSuite):\n", " class Meta:\n", " parent = collections.Counter\n", " \n", " @patch(wrapper_type=wrappers.Hook)\n", " def _subtract_self(self, *args, _subtract=collections.Counter.subtract, **kw):\n", " _subtract(self, **self)\n", " \n", " @patch()\n", " def _return_self(self, *args, **kw):\n", " return self\n", " \n", " __init__ = update = subtract = _subtract_self\n", " \n", " __add__ = __sub__ = __or__ = __and__ = _return_self\n", " \n", " del _subtract_self, _return_self\n", "\n", "with StopCounting():\n", " c = collections.Counter('animal')\n", "c" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "union gives Counter({'a': 2, 'm': 1, 'i': 1, 'n': 1, 'l': 1})\n" ] } ], "prompt_number": 21 }, { "cell_type": "code", "collapsed": false, "input": [ "c2 = collections.Counter('elephant')\n", "c2" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 35, "text": [ "Counter({'e': 2, 'l': 1, 'h': 1, 't': 1, 'p': 1, 'a': 1, 'n': 1})" ] } ], "prompt_number": 35 }, { "cell_type": "code", "collapsed": false, "input": [ "with StopCounting():\n", " print('union gives %s' % (c | c2))" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "union gives Counter({'i': 0, 'm': 0, 'l': 0, 'a': 0, 'n': 0})\n" ] } ], "prompt_number": 36 }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you see, we are back with the initial style of declaring patches. All methods that update our instance subtract itself in the end, and all methods that return new instance return self. Patches `_subtract_self` and `_return_self` are deleted from class namespace and won't be collected.\n", "\n", "*Note*: Also inside the replacement function we can get access to a lot of attributes from the respective event. For that we do write\n", "\n", " @patch(pass_event=True, **other_kw)\n", " def replacement_func(*args, event, **kw):\n", " ...\n", "\n", "but that won't be covered now. Here is how it may look in practice:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "class StopCounting(PatchSuite):\n", " class Meta:\n", " parent = collections.Counter\n", " \n", " @patch(wrapper_type=wrappers.Hook)\n", " def _subtract_self(self, *args, _subtract=collections.Counter.subtract, **kw):\n", " _subtract(self, **self)\n", " \n", " @patch(pass_event=True)\n", " def _return_counter(self, *args, event, _subtract=collections.Counter.subtract, **kw):\n", " ret = event.wrapped_func(self, *args, **kw)\n", " _subtract(ret, **ret)\n", " return ret\n", " \n", " __init__ = update = subtract = _subtract_self\n", " \n", " __add__ = __sub__ = __or__ = __and__ = _return_counter\n", " \n", " del _subtract_self, _return_counter\n", "\n", "c2 = collections.Counter('elephant')\n", "c = collections.Counter('animal')\n", "with StopCounting():\n", " print('union gives %s' % (c | c2))" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "union gives Counter({'m': 0, 'l': 0, 'n': 0, 'i': 0, 'h': 0, 'e': 0, 't': 0, 'a': 0, 'p': 0})\n" ] } ], "prompt_number": 20 }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## \"Global\" objects: config, storage, logger" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These are actually not global, but threadlocal objects: they are global for this thread. To get the current instance you use `.instance()`, to construct new instance you instantiate the object:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from patched.core.threadlocal import ThreadLocalMixin\n", "\n", "class MyGlobal(ThreadLocalMixin):\n", " global_name = \"yet_another_global\"\n", "\n", "instance = MyGlobal.instance()\n", "instance.var = 5\n", "print( MyGlobal.instance().var)\n", "MyGlobal.instance() == MyGlobal()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "5\n" ] }, { "metadata": {}, "output_type": "pyout", "prompt_number": 35, "text": [ "False" ] } ], "prompt_number": 35 }, { "cell_type": "markdown", "metadata": {}, "source": [ "So, we have 3 global objects (`patched.core.objects`):\n", "\n", "- Config\n", "\n", " Yes, it's also a threadlocal object, not an absolute global. It can contain settings that are better to be reset: for example, events-breakpoints to stop at. In general, the `Config` object doesn't play any significant role yet.\n", "\n", "\n", "- Storage\n", " \n", " It's a hierarchical tree that stores data (in memory, just keeps links to objects, preventing them from deletion). It's straightforward to use: you put an object in it" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from patched import get_storage\n", "\n", "get_storage()['some.parameter'] = {'default': True}" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 41 }, { "cell_type": "markdown", "metadata": {}, "source": [ "and later fetch it:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "get_storage()['some']" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 40, "text": [ "{'parameter': {'default': True}}" ] } ], "prompt_number": 40 }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The most interesting one is\n", "\n", "- ##Logger\n", "\n", " It stores `LoggableEvent`s. For now the common case of it is the patched function that has been executed, but it can be absolutely different. This event is formatted to represent a corresponding line in the log, but it can also be accessed as a Python object, and introspected. But enough words, let's see some examples." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Firstly instantiate fresh new logger (reset all the previous records):" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from patched.core.objects import Logger\n", "new = Logger()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 15 }, { "cell_type": "markdown", "metadata": {}, "source": [ "For the next set of examples we will use a web application, the official tutorial of [Django REST framework](https://github.com/tomchristie/rest-framework-tutorial)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And one more thing: I have crafted an IPython extension to view the log, that is so short that I will paste it here:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "load -r 5-18 ../patched/tools/ipython.py" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 43 }, { "cell_type": "code", "collapsed": false, "input": [ "@magics_class\n", "class BlackMagics(Magics):\n", "\n", " @line_magic\n", " def events(self, line):\n", " logger = Logger.instance()\n", " if not line:\n", " return logger\n", "\n", " event = logger[int(line)]\n", " return event\n", "\n", "def load_ipython_extension(ip):\n", " ip.register_magics(BlackMagics)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can load it with the following command, or you can add it to IPython config to be loaded automatically." ] }, { "cell_type": "code", "collapsed": false, "input": [ "load_ext patched.tools.ipython" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 11 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ok, let's start: you need ``django`` and `djangorestframework` to be installed" ] }, { "cell_type": "code", "collapsed": false, "input": [ "import os\n", "os.chdir('../examples/rest-tutorial')\n", "os.environ['DJANGO_SETTINGS_MODULE'] = 'tutorial.settings'\n", "import django # if it's django >= 1.7, you need the next 2 lines\n", "django.setup()" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 1 }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you didn't know, ``django`` has a test client that allows you to request your urls, and `rest_framework` extends it:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from rest_framework.test import APIClient\n", "client = APIClient()\n", "client.login(username='vitalii', password='123')" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 2, "text": [ "True" ] } ], "prompt_number": 2 }, { "cell_type": "markdown", "metadata": {}, "source": [ "The web application allows you to query existing code snippets and to create new ones. Following POST request creates a new snippet: " ] }, { "cell_type": "code", "collapsed": false, "input": [ "resp = client.post('/snippets/', {'title': 'my title', 'code': 'True = False'})\n", "resp.data" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 5, "text": [ "{'url': 'http://testserver/snippets/5/', 'highlight': 'http://testserver/snippets/5/highlight/', 'owner': 'vitalii', 'title': 'my title', 'code': 'True = False', 'linenos': False, 'language': 'python', 'style': 'friendly'}" ] } ], "prompt_number": 5 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's see what's is it doing under the hood. let's add some logging. I've read from the documentation that it does the (de)serialization and that serializers have `from_native` method:" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from rest_framework import serializers as rest_serializers, fields as rest_fields\n", "\n", "class SerializerPatch(PatchSuite):\n", " \n", " from_native = patch(parent=rest_serializers.Serializer)\n", "\n", "with SerializerPatch():\n", " resp = client.post('/snippets/', {'title': 'my title', 'code': 'True = False'})\n", " print(resp.data)\n" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "{'url': 'http://testserver/snippets/15/', 'highlight': 'http://testserver/snippets/15/highlight/', 'owner': 'vitalii', 'title': 'my title', 'code': 'True = False', 'linenos': False, 'language': 'python', 'style': 'friendly'}\n" ] } ], "prompt_number": 6 }, { "cell_type": "code", "collapsed": false, "input": [ "events" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 17, "text": [ "Logged events:\n", "0| from_native returned " ] } ], "prompt_number": 17 }, { "cell_type": "code", "collapsed": false, "input": [ "events 0" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 18, "text": [ "from_native(self = ,\n", " data = ,\n", " files = ,\n", " rv = Snippet object)" ] } ], "prompt_number": 18 }, { "cell_type": "code", "collapsed": false, "input": [ "_.rv.code" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 19, "text": [ "'True = False'" ] } ], "prompt_number": 19 }, { "cell_type": "markdown", "metadata": {}, "source": [ "As you see, it's not just a text line in the log. But let's get some more records, I've heard field values are obtained with `field_from_native` method: " ] }, { "cell_type": "code", "collapsed": false, "input": [ "class WritableFieldPatch(PatchSuite):\n", "\n", " field_from_native = patch(parent=rest_fields.WritableField, log_prefix='-> ')\n", "\n", "Logger()\n", "\n", "with SerializerPatch() + WritableFieldPatch():\n", " resp = client.post('/snippets/', {'title': 'my title', 'code': 'True = False'})\n", " print(resp.data)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "{'url': 'http://testserver/snippets/12/', 'highlight': 'http://testserver/snippets/12/highlight/', 'owner': 'vitalii', 'title': 'my title', 'code': 'True = False', 'linenos': False, 'language': 'python', 'style': 'friendly'}\n" ] } ], "prompt_number": 26 }, { "cell_type": "code", "collapsed": false, "input": [ "events" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 27, "text": [ "Logged events:\n", "0| -> field_from_native returned None\n", "1| -> field_from_native returned None\n", "2| -> field_from_native returned None\n", "3| -> field_from_native returned None\n", "4| -> field_from_native returned None\n", "5| from_native returned " ] } ], "prompt_number": 27 }, { "cell_type": "code", "collapsed": false, "input": [ "events 3" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 29, "text": [ "field_from_native(self = ,\n", " data = ,\n", " files = ,\n", " field_name = language,\n", " into = {'style': 'friendly', 'title': 'my title', 'linenos': False, 'language': 'python', 'code': 'True = False'},\n", " rv = None)" ] } ], "prompt_number": 29 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Oh yeah, `field_from_native` simply puts the result into the `into` dict, and returns None.\n", "\n", "Let's make logging messages more informative (the code below is not meant to be understood right away):" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from patched.patching.events import HookFunctionExecuted\n", "from patched.patching import wrappers\n", "from IPython.lib.pretty import pretty\n", "\n", "class FieldFromNativeEvent(HookFunctionExecuted):\n", "\n", " def _log_pretty_(self, p, cycle):\n", " if cycle:\n", " p.text('HookFunction(..)')\n", " return\n", " with p.group(len(self.log_prefix), self.log_prefix):\n", " p.text( pretty(self.field_value))\n", " p.text(' was set into ')\n", " p.breakable()\n", " p.text(self.field_name)\n", "\n", "\n", "class WritableFieldPatch(PatchSuite):\n", "\n", " @patch(wrapper_type=wrappers.Hook, event_class=FieldFromNativeEvent,\n", " parent=rest_fields.WritableField, pass_event=True, log_prefix='-> ',\n", " )\n", " def field_from_native(self, data, files, field_name, into,\n", " return_value, event):\n", " event.field_name = field_name\n", " event.field_value = into.get(field_name)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 16 }, { "cell_type": "code", "collapsed": false, "input": [ "from patched.core.objects import Logger\n", "Logger()\n", "\n", "with SerializerPatch() + WritableFieldPatch():\n", " resp = client.post('/snippets/', {'title': 'my title', 'code': 'True = False'})\n", " print(resp.data)" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "{'url': 'http://testserver/snippets/18/', 'highlight': 'http://testserver/snippets/18/highlight/', 'owner': 'vitalii', 'title': 'my title', 'code': 'True = False', 'linenos': False, 'language': 'python', 'style': 'friendly'}\n" ] } ], "prompt_number": 17 }, { "cell_type": "code", "collapsed": false, "input": [ "events" ], "language": "python", "metadata": {}, "outputs": [ { "metadata": {}, "output_type": "pyout", "prompt_number": 18, "text": [ "Logged events:\n", "0| -> 'my title' was set into title\n", "1| -> 'True = False' was set into code\n", "2| -> False was set into linenos\n", "3| -> 'python' was set into language\n", "4| -> 'friendly' was set into style\n", "5| from_native returned " ] } ], "prompt_number": 18 }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now it's all better.)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Comparison with other frameworks. Roadmap" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "I can only compare ``patches`` to ``mock`` because I don't know any others. For now it is enough to say that they are very different: roughly speaking, ``mock`` allows you to patch anything with anything. The functionality provided py ``patched`` probably occupies there ten lines of code (I suspect I've seen that function). So, these two packages don't really intersect.\n", "\n", "What it does intersect with.. esspecially the logging part of it - is the debugging utilities provided by python itself.\n", "For \"smart logging\" it probably would be better to make use of those.\n", "\n", "So you should probably expect the package to become smaller, containing only the patching part, and some other packages to appear." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "
\n", "[[go the contents](#sections)] " ] } ], "metadata": {} } ] }