jurigged

repository·master·Indexed 23 days ago

https://github.com/breuleux/jurigged

A live-coding tool for Python (version >= 3.8) that allows real-time hot-patching of functions and methods while a program is running. It preserves program state to avoid expensive reloads and includes a 'develoop' feature for terminal-based interactive development loops. Key features include programmatic file watching via jurigged.watch(), the Recoder API for in-memory patching and mocking, and a CLI for running scripts with live-editing enabled.

Tokens
3.9K
Snippets
6
Records
27
Agent score
82%

What's inside jurigged

  1. Use the develoop feature to loop over functions

    master

    The develoop feature allows you to create a live development environment for specific functions. When a function is entered, its output is captured, and the program waits for input. If the source code is modified, the function re-runs automatically without restarting the whole program.

    Loop over a function

    Use --loop <function_name> or --loop <module_name>:<function_name>.

    Loop only on exceptions

    Use --xloop <function_name> to only trigger the loop if the function raises an exception.

    Develoop Interface Commands

    • r: Manually re-run the loop (can be done during a run).
    • a: Abort the current run (useful for infinite loops).
    • c: Exit the loop and continue the program normally.
    • q: Quit the program.

    Handling stdin and breakpoints

    The default interface does not support stdin or breakpoint(). To use them, decorate your function with @__.loop(interface="basic").

    # Loop over a function
    jurigged --loop function_name script.py
    jurigged --loop module_name:function_name script.py
    
    # Only stop on exceptions
    jurigged --xloop function_name script.py
  2. Implement __conform__ to support custom function transforms

    master

    If you use a decorator or a transform that generates a new code object based on the original source (e.g., JIT or custom compilation), Jurigged might not automatically update the transformed version.

    To fix this, ensure the object holding the transformed function has a __conform__ method and that the original code object is stored in a __slot__ named code.

    When Jurigged detects a change, it calls __conform__(new_code) on your object, allowing you to update your internal state with the new code object.

    import types
    
    class Custom:
        __slots__ = ("code",)
    
        def __init__(self, transformed_fn, code):
            self.code = code
            self.transformed_fn = transformed_fn
    
        def __conform__(self, new_code):
            if new_code is None:
                # Function is being deleted
                ...
    
            if isinstance(new_code, types.FunctionType):
                new_code = new_code.__code__
    
            do_something(new_code)
            self.code = new_code
    
    # Usage example:
    transformed_fn.somefield = Custom(transformed_fn, orig_fn.__code__)
  3. Run a script with live-editing enabled

    master

    You can run a script with Jurigged in two ways:

    1. Use the jurigged command directly.
    2. Use python -m jurigged before your script.

    Use the -v or --verbose flag to see which files are being watched and to receive feedback when functions are updated, added, or deleted.

    jurigged -v script.py
    
    # OR
    
    python -m jurigged -v script.py
  4. Handle out-of-sync status in Recoder

    master

    A Recoder tracks whether its in-memory patch is still valid relative to the source file.

    • Status live: The patch is synchronized with the current state of the code.
    • Status out-of-sync: The underlying file has changed in a way that affects the watched definitions.

    If a Recoder is out-of-sync, you can call repatch() to attempt to re-apply the _current_patch to the module to restore the desired behavior.

  5. Loop over functions with --loop or --xloop

    master

    If you have the jurigged[develoop] extra installed, you can use the --loop or --xloop flags to repeatedly execute specific functions. This is useful for testing code changes in real-time.

    • --loop <FUNC>: Automatically re-runs the specified function whenever its dependencies change.
    • --xloop <FUNC>: Automatically re-runs the specified function specifically when it raises an exception.

    Example using the CLI:

    jurigged -m my_module:my_function --loop my_function
  6. Troubleshoot Jurigged issues

    master

    General Debugging

    Use the --verbose or -v flag to see Watch <file> and Update/Add/Delete <function> statements.

    Common Issues

    • File not being watched: By default, Jurigged watches the current working directory. Use -w <PATH> to specify a specific directory or file (e.g., jurigged -w /).
    • Changes not applying: If using polling doesn't work, try the --poll <INTERVAL> flag. Some editors (like vi) use temporary swap files which might interfere with file watching.
    • Function updates but old code still runs: If you are editing the body of a for loop inside a function that is currently running, the changes only apply on the next call. To fix this, extract the loop body into a helper function. Note that generators and async functions currently cannot be updated while they are already running.
    • Some functions won't update: Functions that are heavily decorated or stashed in complex data structures might be difficult for Jurigged to locate and update.
  7. Programmatically watch files with jurigged.watch()

    master

    You can start watching for changes programmatically within your code. This is useful for environments like IPython or Jupyter as an alternative to %autoreload.

    By default, jurigged.watch() watches all files in the current directory. You can specify a single file or a root directory.

  8. Use Recoders to patch or mock functions

    master

    A Recoder allows you to programmatically change function behavior. This can be used for hot patching or mocking without modifying the original source file. You can also choose to write these changes back to the filesystem.

    • make_recoder(func): Creates a recoder for a specific function.
    • recoder.patch(code_string): Applies a new implementation.
    • recoder.revert(): Reverts to the state before the last patch (or the original state if no commits exist).
    • recoder.commit(): Writes the current patched state to the original source file.
    • recoder.patch_module(...): Used to add imports or helper functions to a patch.
    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()
  9. Troubleshooting: Watcher behavior and file deletion

    master

    When using jurigged to watch files, be aware of how the underlying watcher handles file deletions:

    • Watch Directories, not Files: If you watch a specific file, the watcher may stop working if that file is deleted (e.g., when some editors perform an atomic save by deleting and recreating the file).
    • Recommendation: To ensure continuous monitoring even when editors recreate files, watch the directory containing the files instead of the files themselves.
  10. Create a Recoder with make_recoder()

    master

    Use make_recoder(obj, deletable=False) to create a Recoder instance for a specific Python object (like a function or class). The Recoder allows you to apply code patches to that specific object without modifying the original source file. If the object is found in the registry, it returns a Recoder instance; otherwise, it returns None.

    • obj: The Python object you want to recode.
    • deletable: A boolean indicating if deletions are allowed during patching.