Hot reloading for Python

Overview

jurigged

Jurigged lets you update your code while it runs. Using it is trivial:

  1. jurigged your_script.py
  2. Change some function or method with your favorite editor and save the file
  3. Jurigged will hot patch the new function into the running script

Jurigged updates live code smartly: changing a function or method will fudge code pointers so that all existing instances are simultaneously modified to implement the new behavior. When modifying a module, only changed lines will be re-run.

demo

Install

pip install jurigged

Command line

The simplest way to use jurigged is to add -m jurigged to your script invocation, or to use jurigged instead of python. You can use -v to get feedback about what files are watched and what happens when you change a file.

python -m jurigged -v script.py

OR

jurigged -v script.py

With no arguments given, it will start a live REPL:

python -m jurigged

OR

jurigged

Full help:

usage: jurigged [-h] [--verbose] [--watch PATH] [-m MODULE] [PATH] ...

Run a Python script so that it is live-editable.

positional arguments:
  PATH                  Path to the script to run
  ...                   Script arguments

optional arguments:
  -h, --help            show this help message and exit
  --verbose, -v         Show watched files and changes as they happen
  --watch PATH, -w PATH
                        Wildcard path/directory for which files to watch
  -m MODULE             Module or module:function to run

Troubleshooting

First, if there's a problem, use the verbose flag (jurigged -v) to get more information. It will output a Watch statement for every file that it watches and Update/Add/Delete statements when you update, add or delete a function in the original file and then save it.

The file is not being watched.

By default, scripts are watched in the current working directory. Try jurigged -w to watch a specific file, or jurigged -w / to watch all files.

The file is watched, but nothing happens when I change the function.

It's possibly because you are using an editor that saves into a temporary swap file and moves it into place (vi does this). The watchdog library that Jurigged uses loses track of the file when that happens. Pending a better solution, you can try to configure your editor so that it writes to the file directly. For example, in vi, :set nowritebackup seems to do the trick (either put it in your .vimrc or execute it before you save for the first time).

Jurigged said it updated the function but it's still running the old code.

If you are editing the body of a for loop inside a function that's currently running, the changes will only be in effect the next time that function is called. A workaround is to extract the body of the for loop into its own helper function, which you can then edit. Alternatively, you can use reloading alongside Jurigged.

Similarly, updating a generator or async function will not change the behavior of generators or async functions that are already running.

I can update some functions but not others.

There may be issues updating some functions when they are decorated or stashed in some data structure that Jurigged does not understand. Jurigged does have to find them to update them, unfortunately.

API

You can call jurigged.watch() to programmatically start watching for changes. This should also work within IPython or Jupyter as an alternative to the %autoreload magic.

import jurigged
jurigged.watch()

By default all files in the current directory will be watched, but you can use jurigged.watch("script.py") to only watch a single file, or jurigged.watch("/") to watch all modules.

Recoders

Functions can be programmatically changed using a Recoder. Make one with jurigged.make_recoder. This can be used to implement hot patching or mocking. The changes can also be written back to the filesystem.

from jurigged import make_recoder

def f(x):
    return x * x

assert f(2) == 4

# Change the behavior of the function, but not in the original file
recoder = make_recoder(f)
recoder.patch("def f(x): return x * x * x")
assert f(2) == 8

# Revert changes
recoder.revert()
assert f(2) == 4

# OR: write the patch to the original file itself
recoder.commit()

revert will only revert up to the last commit, or to the original contents if there was no commit.

A recoder also allows you to add imports, helper functions and the like to a patch, but you have to use recoder.patch_module(...) in that case.

Caveats

Jurigged works in a surprisingly large number of situations, but there are several cases where it won't work, or where problems may arise:

  • Functions that are already running will keep running with the existing code. Only the next invocations will use the new code.
    • When debugging with a breakpoint, functions currently on the stack can't be changed.
    • A running generator or async function won't change.
    • You can use reloading in addition to Jurigged if you want to be able to modify a running for loop.
  • Changing initializers or attribute names may cause errors on existing instances.
    • Jurigged modifies all existing instances of a class, but it will not re-run __init__ or rename attributes on existing instances, so you can easily end up with broken objects (new methods, but old data).
  • Updating the code of a decorator or a closure may or may not work. Jurigged will do its best, but it is possible that some closures will be updated but not others.
  • Decorators that look at/tweak function code will probably not update properly.
    • Wrappers that try to compile/JIT Python code won't know about jurigged and won't be able to redo their work for the new code.
    • They can be made to work if they set the (jurigged-specific) __conform__ attribute on the old function. __conform__ takes a reference to the function that should replace it, or None if it is to be deleted.

How it works

In a nutshell, jurigged works as follows:

  1. Insert an import hook that collects and watches source files.
  2. Parse a source file into a set of definitions.
  3. Crawl through a module to find function objects and match them to definitions.
    • It will go through class members, follow functions' __wrapped__ and __closure__ pointers, and so on.
  4. When a file is modified, re-parse it into a set of definitions and match them against the original, yielding a set of changes, additions and deletions.
  5. For a change, exec the new code (with the decorators stripped out, if they haven't changed), then take the resulting function's internal __code__ pointer and shove it into the old one. If the change fails, it will be reinterpreted as a deletion of the old code followed by the addition of the new code.
  6. New additions are run in the module namespace.

Comparison

The two most comparable implementations of Jurigged's feature set that I could find (but it can be a bit difficult to find everything comparable) are %autoreload in IPython and limeade. Here are the key differences:

  • They both re-execute the entire module when its code is changed. Jurigged, by contrast, surgically extracts changed functions from the parse tree and only replaces these. It only executes new or changed statements in a module.

    Which is better is somewhat situation-dependent: on one hand, re-executing the module will pick up more changes. On the other hand, it will reinitialize module variables and state, so certain things might break. Jurigged's approach is more conservative and will only pick up on modified functions, but it will not touch anything else, so I believe it is less likely to have unintended side effects. It also tells you what it is doing :)

  • They will re-execute decorators, whereas Jurigged will instead dig into them and update the functions it finds inside.

    Again, there's no objectively superior approach. %autoreload will properly re-execute changed decorators, but these decorators will return new objects, so if a module imports an already decorated function, it won't update to the new version. If you only modify the function's code and not the decorators, however, Jurigged will usually be able to change it inside the decorator, so all the old instances will use the new behavior.

  • They rely on synchronization points, whereas Jurigged can be run in its own thread.

    This is a double-edged sword, because even though Jurigged can add live updates to existing scripts with zero lines of additional code, it is not thread safe at all (code could be executed in the middle of an update, which is possibly an inconsistent state).

Other similar efforts:

  • reloading can wrap an iterator to make modifiable for loops. Jurigged cannot do that, but you can use both packages at the same time.
Comments
  • Idea for mapping between codes and functions

    Idea for mapping between codes and functions

    Here's an idea for how you might be able to find the correct code and function objects. I'm not 100% sure if this would actually fit in the way you're doing things but I thought it might help. I also don't actually know how reliable gc is for this, it might be, but I haven't really investigated.

    import gc
    import types
    
    
    def find_subcodes(code):
        """
        Yields all code objects descended from this code object.
        e.g. given a module code object, will yield all codes defined in that module.
        """
        for const in code.co_consts:
            if isinstance(const, types.CodeType):
                yield const
                yield from find_subcodes(const)
    
    
    def get_functions(code):
        """
        Returns functions that use the given code.
        """
        return [
            ref
            for ref in gc.get_referrers(code)
            if isinstance(ref, types.FunctionType)
               and ref.__code__ == code
        ]
    
    
    module_code = compile("""
    def foo():
        def bar():
            pass
        return bar
    """, "<string>", "exec")
    
    codes = list(find_subcodes(module_code))
    
    exec(module_code)
    
    bars = [foo(), foo()]
    
    code_to_functions = {code: set(get_functions(code)) for code in codes}
    
    print(code_to_functions)
    
    assert code_to_functions == {foo.__code__: {foo}, bars[0].__code__: set(bars)}
    
    opened by alexmojaki 9
  • Working great on Ubuntu, but not on Windows

    Working great on Ubuntu, but not on Windows

    First of all - I love this code. Thank you so much for creating it. I'm using it to create a live-coding environment to make music with SuperCollider.

    I had no problems running jurigged on Ubuntu, but I can't get it to work on Windows. I tried running it the same way: python3 -m jurigged -v livecoding.py but on Windows it never registers any change in the file when I save. I'm using Sublime on Windows to save. My python version is 3.9.7 and Windows version is 10. Is there a known issue with Windows?

    opened by schollz 5
  • UnicodeDecodeError: 'gbk' codec can't decode byte 0xaf in position 594: illegal multibyte sequence

    UnicodeDecodeError: 'gbk' codec can't decode byte 0xaf in position 594: illegal multibyte sequence

    Traceback (most recent call last):
      File "C:\Users\InPRTx\AppData\Local\Programs\Python\Python38\lib\runpy.py", line 194, in _run_module_as_main
        return _run_code(code, main_globals, None,
      File "C:\Users\InPRTx\AppData\Local\Programs\Python\Python38\lib\runpy.py", line 87, in _run_code
        exec(code, run_globals)
      File "C:\Users\InPRTx\AppData\Local\Programs\Python\Python38\lib\site-packages\jurigged\__main__.py", line 4, in <module>
        cli()
      File "C:\Users\InPRTx\AppData\Local\Programs\Python\Python38\lib\site-packages\jurigged\live.py", line 258, in cli
        mod, run = find_runner(opts, pattern)
      File "C:\Users\InPRTx\AppData\Local\Programs\Python\Python38\lib\site-packages\jurigged\live.py", line 183, in find_runner
        registry.prepare("__main__", path)
      File "C:\Users\InPRTx\AppData\Local\Programs\Python\Python38\lib\site-packages\jurigged\register.py", line 60, in prepare
        f.read(),
    UnicodeDecodeError: 'gbk' codec can't decode byte 0xaf in position 594: illegal multibyte sequence
    

    jurigged 0.3.3

    It seems that it is not set to open the file with utf8 encoding by default

    https://github.com/breuleux/jurigged/blob/75dc1a599a8c6c2807cb114e66f9b59f986b8913/jurigged/register.py#L57

    opened by InPRTx 3
  • Support for pre- and post-update hooks

    Support for pre- and post-update hooks

    Essentially, I was hoping that it were possible to add support for custom hooks to run before and after updating the code in memory so that you can run pre- and post-update tasks.

    This is a fairly niche use case, but a Discord bot framework I'm working on (https://github.com/NAFTeam/NAFF) is looking to implement jurigged for fast-paced development of Discord bots, and re-registering command with Discord whenever files are changed. However, it's not really feasible, as doing so requires tracking everything both before and after the change, which currently isn't possible (that I'm aware of)

    opened by zevaryx 2
  • Files not reloaded with PyCharm with Windows

    Files not reloaded with PyCharm with Windows

    Description

    On Windows, jurigged does not seem to reload a file it's running after I edit it with PyCharm. I have read this may be an issue with how WatchDog monitors for file updates, but I didn't see JetBrains IDEs mentioned specifically so I'm not sure if it's expected that this would work on them.

    It may be nice to include a table of editors which are known to be compatible with jurigged, and any notes for making them work if necessary.

    Steps to reproduce

    1. Create a new project in PyCharm
    2. Write the script
    from time import sleep
    
    def do_something():
        print("Hello")
    
    def main():
        for _ in range(100):
            do_something()
            sleep(1)
    
    if __name__ == '__main__':
        main()
    
    1. Run the script with jurigged -v main.py in your terminal
    2. As the script is printing "Hello" repeatedly, change print("Hello") to print("Hi") and click out of the file so it saves.

    Expected behavior

    The script would go from printing "Hello" to "Hi"

    Actual behavior

    The script continues to print "Hello"

    Platform

    PyCharm version

    PyCharm 2021.2.2 (Professional Edition) Build #PY-212.5284.44, built on September 14, 2021

    Python version

    Python 3.10.0

    Dependency versions

    ansicon==1.89.0
    blessed==1.19.0
    codefind==0.1.2
    jinxed==1.1.0
    jurigged==0.3.4
    ovld==0.3.2
    six==1.16.0
    watchdog==1.0.2
    wcwidth==0.2.5
    

    Device specifications

    Processor	Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz   3.60 GHz
    Installed RAM	32.0 GB (31.9 GB usable)
    System type	64-bit operating system, x64-based processor
    

    Windows specifications

    Edition	Windows 11 Education Insider Preview
    Version	Dev
    Installed on	‎10/‎19/‎2021
    OS build	22478.1012
    Experience	Windows Feature Experience Pack 1000.22478.1012.0
    
    opened by nottheswimmer 2
  • No matching distribution found for jurigged

    No matching distribution found for jurigged

    ERROR: Could not find a version that satisfies the requirement jurigged (from versions: none)
    ERROR: No matching distribution found for jurigged
    

    Pip 21.2.2 Python 3.7.11

    Any idea on how to fix that, please?

    opened by desprit 2
  • File change not detected

    File change not detected

    I have the following snippet:

    import time
    
    import jurigged
    
    
    def experiment(*args, **kwargs):
        def decorator(func):
    
            while True:
                time.sleep(0.1)
                res = func(*args, **kwargs)
                print(repr(res), end="\r")
    
        return decorator
    
    
    if __name__ == "__main__":
    
        @experiment()
        def main():
            a = 2
            return 1
    

    If I run:

    jurigged -v essai.py 
    Watch /tmp/essai.py
    

    I do get "1", but if I modify the code, jurigged doesn't "Update main.main" as usual. I suppose the decorator does something to it.

    opened by ksamuel 2
  • Add pre- and post-run callback support

    Add pre- and post-run callback support

    As per #18 , this adds (basic) callback support for pre- and post-refresh. It doesn't pass in anything into the callbacks, but is meant to provide a basic means of adding this support.

    If you have any suggestions for edits, please let me know and I'll get them made.

    opened by zevaryx 1
  • Can't pass arguments that start with -- to module

    Can't pass arguments that start with -- to module

    If you attempt to do jurigged -m module --argument 123, argparse inside jurigged will get angry and throw unrecognized arguments.

    As a workaround you can pass "" after -m module.

    opened by OctoNezd 1
  • watchdog version is incompatible

    watchdog version is incompatible

    When I install jurigged, by default it requires watchdog version <2.0.0, >=1.0.2 and it automatically install watch 1.0.2. But mkdocs 1.2.2 is also a dependency of jurigged which requires watchdog >=2.0. The wathcdog 1.0.2 which install by default is incompatible with it. How to solve this dependency issue?

    image image

    opened by yesdeepakmittal 1
  • Errorr using jurigged on docker

    Errorr using jurigged on docker

    I am getting an error when i am using with docker:

    Traceback (most recent call last):
      File "/usr/local/bin/jurigged", line 8, in <module>
        sys.exit(cli())
      File "/usr/local/lib/python3.9/site-packages/jurigged/live.py", line 268, in cli
        run()
      File "/usr/local/lib/python3.9/site-packages/jurigged/live.py", line 188, in run
        runpy.run_path(path, module_object=mod)
      File "/usr/local/lib/python3.9/site-packages/jurigged/runpy.py", line 279, in run_path
        code, fname = _get_code_from_file(run_name, path_name)
      File "/usr/local/lib/python3.9/site-packages/jurigged/runpy.py", line 252, in _get_code_from_file
        with io.open_code(decoded_path) as f:
    FileNotFoundError: [Errno 2] No such file or directory: '/srv/www/s'
    

    Dockerfile

    
    FROM python:3.9-slim
    
    RUN mkdir /srv/www/
    
    ADD ./ /srv/www/
    
    WORKDIR /srv/www/
    
    ENV GRPC_PYTHON_VERSION 1.39.0
    RUN python -m pip install --upgrade pip
    RUN pip install grpcio==${GRPC_PYTHON_VERSION} grpcio-tools==${GRPC_PYTHON_VERSION} grpcio-reflection==${GRPC_PYTHON_VERSION} grpcio-health-checking==${GRPC_PYTHON_VERSION} grpcio-testing==${GRPC_PYTHON_VERSION}
    
    RUN pip install -r requirements.txt
    
    RUN pip install jurigged
    ENTRYPOINT ["jurigged"]
    CMD ["main.py"]
    
    

    Python file main.py

    import time
    import datetime
    
    def main():
        while True:
            print("Executed %s" % datetime.datetime.now())
            time.sleep(5)
    
    
    main()
    
    
    opened by luhfilho 1
  • Workaround for Geany IDE: Detect other types of file modifications: file move, file close, file overwritten

    Workaround for Geany IDE: Detect other types of file modifications: file move, file close, file overwritten

    This is related to how Geany handles files by default: It creates a temporary file copy, writes the modifications to it and them copies the modifications to the current file. These steps are made to prevent file corruption when there is no space left in the device. Unfortunately this is not detected as a modification by jurigged and the file is not hot-reloaded

    these are the events I logged by using on_any_event inside the class JuriggedHandler(FileSystemEventHandler):

    event type: created path src : app_path/test_flask_hotreloading/.goutputstream-6PJ0X1 event type: modified path src : app_path/test_flask_hotreloading event type: modified path src : app_path/test_flask_hotreloading/.goutputstream-6PJ0X1 event type: closed path src : app_path/test_flask_hotreloading/test_flask_hello.py event type: modified path src : app_path/test_flask_hotreloading event type: modified path src : app_path/test_flask_hotreloading/.goutputstream-6PJ0X1 event type: moved path src : app_path/test_flask_hotreloading/.goutputstream-6PJ0X1 event type: modified path src : app_path/test_flask_hotreloading event type: closed path src : app_path/test_flask_hotreloading/test_flask_hello.py event type: modified path src : app_path/test_flask_hotreloading

    I propose the following temporary workaround:

        on_closed = on_modified
    

    however, the best way can be to use only an on_any_event event and them, inside it handle all possible situations:

    def on_any_event(self, event):
        print(f'event type: {event.event_type}  path src : {event.src_path}', flush = True)
    

    a more aggressive approach:

    def on_any_event(self, event):
        print(f'event type: {event.event_type}  path src : {event.src_path}', flush = True)
        if ".py" in event.src_path:
            #do reload procedure
    

    I've not made a pull request because it is open to discussion, but I'm using the temporary workaround.

    opened by animaone 0
  • jurigged -v <script.py> executes and exits

    jurigged -v executes and exits

    hi.py:

     print('hi')                                              
    
    jurigged -v hi.py      
    Watch /path/hi.py
    hi
    

    The process simply exits after executing the script once. How do I get the reloading to work on a M1 mac?

    opened by mereck 0
  • jurigged -w starts python interactive mode

    jurigged -w starts python interactive mode

    jurigged -w should start jurigged watching only the specified file.

    But: jurigged -w script.py shows this: image

    If I try: jurigged -w script.py script.py the script starts but It watch for the entire current directory instead of just the specified file.

    I'd just want to start jurigged and watch for only one file, am I doing it wrongly?

    opened by mopitithomne 5
  • Maybe a file name not supported by win was used

    Maybe a file name not supported by win was used

    PS C:\Users\InPRTx\Desktop\git> git clone https://github.com/breuleux/jurigged.git Cloning into 'jurigged'... remote: Enumerating objects: 878, done. remote: Counting objects: 100% (878/878), done. remote: Compressing objects: 100% (457/457), done. Receiving objects: 94% (826/878)used 618 (delta 393), pack-reused 0 Receiving objects: 100% (878/878), 203.28 KiB | 1.83 MiB/s, done. Resolving deltas: 100% (612/612), done. error: invalid path 'tests/snippets/ballon:main.py' fatal: unable to checkout working tree warning: Clone succeeded, but checkout failed. You can inspect what was checked out with 'git status' and retry with 'git restore --source=HEAD :/'

    opened by InPRTx 0
Owner
Olivier Breuleux
Olivier Breuleux
Simple python module to get the information regarding battery in python.

Battery Stats A python3 module created for easily reading the current parameters of Battery in realtime. It reads battery stats from /sys/class/power_

Shreyas Ashtamkar 5 Oct 21, 2022
ticktock is a minimalist library to view Python time performance of Python code.

ticktock is a minimalist library to view Python time performance of Python code.

Victor Benichoux 30 Sep 28, 2022
Python @deprecat decorator to deprecate old python classes, functions or methods.

deprecat Decorator Python @deprecat decorator to deprecate old python classes, functions or methods. Installation pip install deprecat Usage To use th

null 12 Dec 12, 2022
A python package containing all the basic functions and classes for python. From simple addition to advanced file encryption.

A python package containing all the basic functions and classes for python. From simple addition to advanced file encryption.

PyBash 11 May 22, 2022
Find dependent python scripts of a python script in a project directory.

Find dependent python scripts of a python script in a project directory.

null 2 Dec 5, 2021
A functional standard library for Python.

Toolz A set of utility functions for iterators, functions, and dictionaries. See the PyToolz documentation at https://toolz.readthedocs.io LICENSE New

null 4.1k Dec 30, 2022
Python Classes Without Boilerplate

attrs is the Python package that will bring back the joy of writing classes by relieving you from the drudgery of implementing object protocols (aka d

The attrs Cabal 4.6k Jan 6, 2023
🔩 Like builtins, but boltons. 250+ constructs, recipes, and snippets which extend (and rely on nothing but) the Python standard library. Nothing like Michael Bolton.

Boltons boltons should be builtins. Boltons is a set of over 230 BSD-licensed, pure-Python utilities in the same spirit as — and yet conspicuously mis

Mahmoud Hashemi 6k Jan 4, 2023
Retrying library for Python

Tenacity Tenacity is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just

Julien Danjou 4.3k Jan 5, 2023
Simple yet flexible natural sorting in Python.

natsort Simple yet flexible natural sorting in Python. Source Code: https://github.com/SethMMorton/natsort Downloads: https://pypi.org/project/natsort

Seth Morton 712 Dec 23, 2022
A Python utility belt containing simple tools, a stdlib like feel, and extra batteries. Hashing, Caching, Timing, Progress, and more made easy!

Ubelt is a small library of robust, tested, documented, and simple functions that extend the Python standard library. It has a flat API that all behav

Jon Crall 638 Dec 13, 2022
Retrying is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything.

Retrying Retrying is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just

Ray Holder 1.9k Dec 29, 2022
Pampy: The Pattern Matching for Python you always dreamed of.

Pampy: Pattern Matching for Python Pampy is pretty small (150 lines), reasonably fast, and often makes your code more readable and hence easier to rea

Claudio Santini 3.5k Jan 6, 2023
isort is a Python utility / library to sort imports alphabetically, and automatically separated into sections and by type.

isort is a Python utility / library to sort imports alphabetically, and automatically separated into sections and by type. It provides a command line utility, Python library and plugins for various editors to quickly sort all your imports.

Python Code Quality Authority 5.5k Jan 8, 2023
Functional UUIDs for Python.

??️FUUID stands for Functional Universally Unique IDentifier. FUUIDs are compatible with regular UUIDs but are naturally ordered by generation time, collision-free and support succinct representations such as raw binary and base58-encoded strings.

Phil Demetriou 147 Oct 27, 2022
✨ Une calculatrice totalement faite en Python par moi, et en français.

Calculatrice ❗ Une calculatrice totalement faite en Python par moi, et en français. ?? Voici une calculatrice qui vous permet de faire vos additions,

MrGabin 3 Jun 6, 2021
✨ Un juste prix totalement fait en Python par moi, et en français.

Juste Prix ❗ Un juste prix totalement fait en Python par moi, et en français. ?? Avec l'utilisation du module "random", j'ai pu faire un choix aléatoi

MrGabin 3 Jun 6, 2021
✨ Un chois aléatoire d'un article sur Wikipedia totalement fait en Python par moi, et en français.

Wikipedia Random Article ❗ Un chois aléatoire d'un article sur Wikipedia totalement fait en Python par moi, et en français. ?? Grâce a une requète a w

MrGabin 4 Jul 18, 2021
✨ Un DNS Resolver totalement fait en Python par moi, et en français

DNS Resolver ❗ Un DNS Resolver totalement fait en Python par moi, et en français. ?? Grâce a une adresse (url) vous pourrez avoir l'ip ainsi que le DN

MrGabin 3 Jun 6, 2021