Skip to main content

Method behavior modification at runtime, made easy.

Project description

Harmonify

Harmonify is a Python library that allows users to change the behavior of classes at runtime, with nothing more than a few function calls. Inspired by Harmony (the very popular C# library that also allows runtime behavior modification), Harmonify is flexible and uses a simple system based on monkey-patching. Like its C# equivalent, it can be used for:

  • Debugging: Inject logging or checks into methods without changing them permanently.
  • Testing: Isolate parts of your code by temporarily changing how certain methods behave.
  • Extending Libraries: Add new features or modify behavior of classes from libraries you can't edit directly.
  • Experimentation: Play around with how code runs in a non-destructive way.

Features

  • Prefix Patching: Run your custom code before the original method executes.
  • Postfix Patching: Run your custom code after the original method executes, even allowing you to modify its return value.
  • Replace Patching: Completely swap out the original method's logic with your own.
  • Create & Delete methods: Add or remove methods from a class or a module, without changing the other ones.
  • Easy Unpatching: Restore methods to their original state with a simple call.
  • Function Patching: Patch functions as easily as methods!
  • Function and Method Hooking: Use a very simple API to hook into any method (that is hookable)!
  • Code Injection & Injection undo-ing: Add you own code inside any Python function or method and revert at any time.
    • Note: Be careful with code injection, and do not inject code coming from a source you don't trust! If you're a library developer and want to prevent your code from being injected into, decorate your code with the harmonify.no_inject decorator. If you want your code to be injected into, you can also use the harmonify.allow_inject decorator fol clarity.

Installation

Installing Harmonify is as easy as using pip:

pip install harmonify-patcher

After that, Harmonify will be available globally!

Example Programs

Function Patching

my_library.py

def sqrt(x: float) -> float:
	return x ** (1 / 2)

def pow(x: float, n: int) -> float:
	return x ** n

def get_version():
	return "1.0"

main.py

import harmonify
import my_library

def new_get_ver():
	return "Latest release"

print(my_library.get_version())   # 1.0
harmonify.patch_function(
	target_module = my_library,
	function_name = "get_version",
	replace = new_get_ver
)
print(my_library.get_version())   # Latest release

Code Injection

api_lib.py

import harmonify

def open_api1():
	print("Doing API stuff...")

@harmonify.allow_inject
def open_api2():
	print("More API stuff...")

@harmonify.no_inject
def restricted_api(uname, passwd):
	print(f"Doing restricted API access with:\n\tUsername: {uname}\n\tPassword: {passwd}")

main.py

import harmonify
import api_lib

if harmonify.inject_function(
    target_module = api_lib,
    function_name = "open_api1",
    insert_line = 1,
    insert_type = harmonify.InsertType.AFTER_TARGET,
    code_to_inject = "print('Injected!')"
):
    print("Successfully injected open_api1()")

if harmonify.inject_function(
    target_module = api_lib,
    function_name = "open_api2",
    insert_line = 1,
    insert_type = harmonify.InsertType.AFTER_TARGET,
    code_to_inject = "print('Injected!')"
):
    print("Successfully injected open_api2()")

if harmonify.inject_function(
    target_module = api_lib,
    function_name = "restricted_api",
    insert_line = 1,
    insert_type = harmonify.InsertType.AFTER_TARGET,
    code_to_inject = "print('Stealing info!')"
):
    print("Successfully injected restricted_api()")

api_lib.open_api1()
api_lib.open_api2()
api_lib.restricted_api(uname="Super Secret Agent #42", passwd="SuperSecretPassword123")

Method Hooking

game.py

import harmonify   # Required for the hook API

class Game:
    def __init__(self, player):
        self.player = player
        self.setup()
        harmonify.call_hook('setup', [self])   # Call the setup hook in case anyone needs to do something with the game
    
    # ...

main.py

import hamronify
import game

def my_setup_hook(game_self: game.Game):
    game_self.player.health += 10   # Give the player a health boost

harmonify.register_method_hook(
    target_class = game.Game,
    method_name = "__init__",
    hook_callback = my_setup_hook,
    hook_id = "setup"
)

g = Game()
assert g.player.health == 110   # Assume the player starts with 100HP

Changelog

3.2.0

Added an API reference/documentation, stored in DOCS.md.
Fix typos throughout the library and remove the Harmonify handler class.

3.1.2

Modify the Patch class to support the default Patch.inject() value (A.K.A. no injection).

3.1.0

Rewrote the patch class system to look more like Harmony's.
Added a general handler class, named Harmonify.

3.0.0

Reverted to the old injector, while also cleaning it up. This is because the new injector was too messy and had too many bugs.
Changed some names in the injector module to make more sense.
Allow hooks to use IDs instead of numbers when calling hooks.

2.0.0

Final fix made regarding import syntax.

2.0.0 (Release Candidate 2)

Fixed import syntax. It seems like Python hates me.

2.0.0 (Release Candidate 1)

Remade the injector module, separating it as a package and remaking the API for injecting cleaner. Added the TreePath class, meant for retrieving statements based on an exact path. These changes make the injector more robust and simpler to use. Updated the injector example from above.

1.4.2

Added functions for easy use of the context managers from 1.4.1, named apply_patch, apply_inject and add_hook.

1.4.1

Added the generic call_hook function, along with context managers for temporary patches/injections/hooks. Also added the remove_function function, which was missing from a previous release.

1.4.0

Added hooks. These can be registered to a callable via the register_function_hook and register_method_hook functions. Hooks can be called by a library function with call_function_hook or call_method_hook. Multiple hooks can be added to the same callable.
Future plan (possibly in 1.4.1): add a generic call_hook function, which would dispatch to call_function_hook or call_method_hook, depending on the caller.

1.3.2

Fixed an internal bug where an UnboundLocalError would appear when creating a new patch.

1.3.1

Added the PatchInfo utility class and two new fucntions, get_function_patches and get_method_patches. These two functions return informations about every patch that has been applied up until the call.

1.3.0

Slightly improved safeguards to provide an easy API. This does mean that the library dev needs to have Harmonify installed, which is not ideal.

1.2.3

Added injection safeguards. These should be applied in the target library.

1.2.2

Fixed a problem where the code would be injected before the target line.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

harmonify_patcher-3.2.0.tar.gz (16.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

harmonify_patcher-3.2.0-py3-none-any.whl (16.5 kB view details)

Uploaded Python 3

File details

Details for the file harmonify_patcher-3.2.0.tar.gz.

File metadata

  • Download URL: harmonify_patcher-3.2.0.tar.gz
  • Upload date:
  • Size: 16.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.2

File hashes

Hashes for harmonify_patcher-3.2.0.tar.gz
Algorithm Hash digest
SHA256 6182b9f8b7327cd66eb6cff52e629ae515370e2564f674bbea944ec07312ad14
MD5 252211d4d2ee3feef686194d81b018cd
BLAKE2b-256 625d8f975f0e397f6c3ff56ba0087fdf3f87642b8a847ca6ba907af7d2d0cda3

See more details on using hashes here.

File details

Details for the file harmonify_patcher-3.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for harmonify_patcher-3.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b162c72dee2e1a28bf267e14f6187caadd15c4cd75d256145524ad461a574336
MD5 1cab1b710a41d4c74c69b0509f2dcefe
BLAKE2b-256 3035751cfacfb4c5b4c24c5a1c7f0b7ff34b79e3d0897f49154a9d2d1af2984c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page