Metadata-Version: 2.4
Name: harmonify-patcher
Version: 3.1.2
Summary: Method behavior modification at runtime, made easy.
Author-email: Wind Rider <abcbcalol69@gmail.com>
License: MIT License
        
        Copyright (c) 2025 ProgrammingLover71
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/ProgrammingLover71/Harmonify
Keywords: patcher,python,library
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# 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`:

```shell
pip install harmonify-patcher
```
After that, Harmonify will be available globally!



## Example Programs

### Function Patching
#### my_library.py
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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.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. <br>
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. <br>
Changed some names in the injector module to make more sense. <br>
Allow hooks to use *ID*s 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. <br>
*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.
