{ "info": { "author": "Raphael Gaschignard", "author_email": "raphael@rtpg.co", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Utilities" ], "description": "==============\nMighty Patcher\n==============\n\nThis project provides an automatic, restart-less code reloader for Python. Concretely this means that you can update the code inside a running Python program and test the new behaviour without having to go through a restart cycle.\n\nThis project is an attempt to give Python the same sort of tooling that gives Clojure `Figwheel`_ or gives Ruby `Sonic Pi`_. \n\n.. _Figwheel: https://figwheel.org/\n.. _Sonic Pi: https://sonic-pi.net/\n\n------------------\nWhat does this do?\n------------------\n\nThe Mighty Patcher provides a file-watching auto-reloader, whose instances will watch your project directory for changes to Python files on a separate thread. When changes are seen, this thread will re-import the python file and patch in the new module\n\n============\nInstallation\n============\n\nThis is available for installation on PyPI. This also sets up the Pytest plugin, so any project using ``pytest`` will be able to use this project's autoreloader in test runs through one configuration flag::\n\n pip install mighty_patcher\n\n=====\nUsage\n=====\n\nThe base entry point is the ``AutoReloader`` class. Before starting up your main program loop, instantiating an instance of this class will start a separate thread that watches for Python changes::\n\n from some_module.print import announce_double_time\n import some_module.math\n from time import sleep\n\n from mighty_patcher.watch import AutoReloader\n\n reloader = AutoReloader(\n path=\"/path/to/my/project/src/\"\n )\n\n while True:\n print(double(4))\n sleep(10)\n\nBy default the reloader will print information related to errors on code reloading, but tries to be tolerant to errors so that your program doesn't crash because you saved halfway through typing a file.\n\nThe reloader path is considered a module root, and the relative path of modified python files are used to determine what module they represent:\n\nFor example, here ``\"/path/to/my/project/src/some_module/math.py\"`` would map to ``some_module.math``\n\nBecause of how modules work, this will mostly be what you want. But if your project has various lookup paths set up (for example if some code refers to the math module as ``src.some_module.math``), then the file might not map over properly.\n\n----------------------------\nChanging behaviour on reload\n----------------------------\n\nIf you are working on something that has global state that you don't want to have thrown away on every reload, you can modify what your code does depending on whether it's the initial load of the code or whether you are reloading this code::\n\n\n # some_file.py\n from mighty_patcher import currently_reloading\n import time\n\n # this dictionary will get reset to an empty state \n _my_cache = {}\n\n if not currently_reloading():\n # this will get run on the first import of this file, but not on subsequent reloads\n\t program_run_start = time.time()\n number_of_restarts = 0\n\n if currently_reloading():\n # this block will only run on reloads\n\t number_of_restarts += 1\n\t print(\"Reloaded!\")\n\n\n-------\nCaveats\n-------\n\nBecause of how the reloading works, some primitive data types (notably numeric datatypes) might not always reload in the expected way.\n\nIf your code (or a third party library) uses a registry pattern and has some validation to make sure that (for example) you don't declare two classes as having the same name, you can run into issues when reloading files.\n\nThis is notably a problem with Django and its ORM models. This isn't a problem for the entire Django project, but files containing model definitions likely won't reload properly.\n\nThe main thing to remember is that this is changing stuff out from under CPython, and it's not the expected execution model. Don't use this on a long-running production server! Expect crashes and embrace them.\n\nAlso the implementation currently has a memory leak proportional to the number of reloads going on in a single session (old versions of objects stick around forever). This problem is solvable but requires a bit of work on the internals\n\n=====================\nUsage (Pytest plugin)\n=====================\n\nOnce you install this package you can use it as a pytest plugin.\n\nThe following options are made available when running pytest:\n\n - ``--reload-loop``\n\n Running this flag starts an autoreloader when you start your tests. When you reach a test failure, you will be dropped into pdb to examine the error.\n\n While you are in ``pdb`` mode, you can edit your project files and the autoreloader will install the new code. After you are confident that you have fixed the issue, you can leave the debugger with the ``c`` (continue) command and the test will be run again.\n\n - ``--reload-dir``\n\n Choose what directory the autoreloader should look at. This directory is then considered the package root for puprposes of determining what package a file belongs to (and thus what directory to reload)\n\n This defaults to the ``pytest`` invocation directory, but you might need to point to another directory if you do things like splitting your code into ``src`` and ``tests`` (where ``src`` is the package root)\n\n\n-----------------------\nCaveats (Pytest plugin)\n-----------------------\n\n - Because the debugger needs to handle standard input, currently you always need to pass in `-s` when invoking pytest to avoid the default \"capture standard input and output\" behaviour of pytest.\n\n - I have hit some issues with editing the test code itself (that is to say the actively running test class/test function rather than the application code). This requires a bit more investigation\n\n - As always, when in doubt, tear down the entire program and restart\n\n--------------------------------------------------------------\nWhats the big deal? Don't I already have ``importlib.reload``?\n--------------------------------------------------------------\n\nBeyond setting up the file-watching infrastructure to trigger module reloads, this project offers much deeper code replacement abilities than other tools out there.\n\nThe core issue with ``importlib.reload`` is a problem of *references*.\n\nAssuming you had the following project::\n\n\n # some_module/math.py\n\n def double(n):\n return 2.1 * n\n\n::\n\n # some_module/print.py\n\n from some_module.math import double\n from time import time\n\n def announce_double_time():\n print(double(time()))\n\n::\n\n # main.py\n from some_module.print import announce_double_time\n import some_module.math\n from time import sleep\n from importlib import reload\n\n while True:\n print(double(4))\n sleep(10)\n\t# reload the math and try again\n\treload(some_module.math)\n\n\nHere you could be working out the kinks of your module's math and so write a reload loop specifically for it (already kinda annoying). Unfortunately if you write this, it *won't reload the actual math usage*\n\nWhen you reload the module you end up replacing the values within the module object. So in a sense you end up with ``some_module.math.double = newly_loaded_double`` running on each reload.\n\n*But* inside your dependent module (``some_module.print``), you have a qualified import statement that gets executed once here::\n\n from some_module.math import double\n\n # is roughly the same as\n\n double sys.modules['some_module.math']['double']\n\n\nSo until you reload ``some_module.print``, *its* refernce to ``double`` will always point to the original implementaion, no matter how many times you reload the source module.\n\nHere you can solve the problem by doing workarounds like:\n\n- using module-qualified imports (``from some_module import math`` then ``math.double``), since then you will point to the module, and classic module reloading actually just edits the module inplace\n\n- making sure to reload dependencies properly. So \"reload `some_module.math`, then reload `some_module.print`\" (making sure to do things in the right order if you want to avoid a \"stale reference\" problem)\n\nBut ultimately this leads you down the road of adapting how you write your code so you can be able to use a tool. It forces you to write things un-naturally\n\n\n------------------------------------------------\nHow the Mighty Patcher avoids reference problems\n------------------------------------------------\n\nEven if importing in a function creates another reference to it, ultimately the reference is pointing to *the same function*.\n\nSo when you first load the program you end up with the following memory layout::\n\n\n [some_module.math] --\"double\" --> \n # ^ \n # |\n # |\n [some_module.print] --\"double\" ------/\n\n\nClassic module reloading will try to edit the modules to provide new definitions. But the Mighty Patcher instead opts to *replace the function object directly*, so that references are pointing to the correct object.\n\nThis isn't actually possible in pure Python, so this project has a built-in CPython extension to let us directly modify the function object, making sure that any reference to that function object will get the most up-to-date version of the object.\n\nThere are a lot of details and gotchas around this technique, but for the most part this drastically reduces turnaround time for workflows that might otherwise require a lot of restarts\n\n\n\nChangelog\n=========\n\n0.1 (2019-06-02)\n------------------\n\n* First release on PyPI.\n\n\n", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/rtpg/mighty_patcher", "keywords": "debugging", "license": "BSD 3-Clause License", "maintainer": "", "maintainer_email": "", "name": "mighty-patcher", "package_url": "https://pypi.org/project/mighty-patcher/", "platform": "", "project_url": "https://pypi.org/project/mighty-patcher/", "project_urls": { "Homepage": "https://github.com/rtpg/mighty_patcher", "Issue Tracker": "https://github.com/rtpg/mighty_patcher/issues" }, "release_url": "https://pypi.org/project/mighty-patcher/0.1/", "requires_dist": [ "watchdog" ], "requires_python": "!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "summary": "The restart-less hot-reloading wonder, Mighty Patcher", "version": "0.1" }, "last_serial": 5348688, "releases": { "0.1": [ { "comment_text": "", "digests": { "md5": "9976d89c18d49272300c2116a9b4c7b8", "sha256": "9784356c3670bf6010753f6f2bef4ae4f18f0f0e41ce006d9f8abac7b175f180" }, "downloads": -1, "filename": "mighty_patcher-0.1-cp36-cp36m-macosx_10_14_x86_64.whl", "has_sig": false, "md5_digest": "9976d89c18d49272300c2116a9b4c7b8", "packagetype": "bdist_wheel", "python_version": "cp36", "requires_python": "!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 15769, "upload_time": "2019-06-02T12:36:25", "url": "https://files.pythonhosted.org/packages/aa/98/cd1bfe30157a032c0bb7abc1fc4787e96f6998bd1220885ad2e4c9db61d6/mighty_patcher-0.1-cp36-cp36m-macosx_10_14_x86_64.whl" }, { "comment_text": "", "digests": { "md5": "f64bcad6825f1ff1d57f07c4b3a2922c", "sha256": "3ab611b5014ec033bb5a66e7b2faf967f7a19ea2d9e72402ba3666e504abad77" }, "downloads": -1, "filename": "mighty_patcher-0.1.tar.gz", "has_sig": false, "md5_digest": "f64bcad6825f1ff1d57f07c4b3a2922c", "packagetype": "sdist", "python_version": "source", "requires_python": "!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 21530, "upload_time": "2019-06-02T12:36:28", "url": "https://files.pythonhosted.org/packages/bb/44/d65833af0ca99e9a55c52506f765f8196aad367854bac7e13c03c370b0f3/mighty_patcher-0.1.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "9976d89c18d49272300c2116a9b4c7b8", "sha256": "9784356c3670bf6010753f6f2bef4ae4f18f0f0e41ce006d9f8abac7b175f180" }, "downloads": -1, "filename": "mighty_patcher-0.1-cp36-cp36m-macosx_10_14_x86_64.whl", "has_sig": false, "md5_digest": "9976d89c18d49272300c2116a9b4c7b8", "packagetype": "bdist_wheel", "python_version": "cp36", "requires_python": "!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 15769, "upload_time": "2019-06-02T12:36:25", "url": "https://files.pythonhosted.org/packages/aa/98/cd1bfe30157a032c0bb7abc1fc4787e96f6998bd1220885ad2e4c9db61d6/mighty_patcher-0.1-cp36-cp36m-macosx_10_14_x86_64.whl" }, { "comment_text": "", "digests": { "md5": "f64bcad6825f1ff1d57f07c4b3a2922c", "sha256": "3ab611b5014ec033bb5a66e7b2faf967f7a19ea2d9e72402ba3666e504abad77" }, "downloads": -1, "filename": "mighty_patcher-0.1.tar.gz", "has_sig": false, "md5_digest": "f64bcad6825f1ff1d57f07c4b3a2922c", "packagetype": "sdist", "python_version": "source", "requires_python": "!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", "size": 21530, "upload_time": "2019-06-02T12:36:28", "url": "https://files.pythonhosted.org/packages/bb/44/d65833af0ca99e9a55c52506f765f8196aad367854bac7e13c03c370b0f3/mighty_patcher-0.1.tar.gz" } ] }