New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

python-hmr

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

python-hmr - pypi Package Compare versions

Comparing version
0.2.0
to
0.3.0
+8
assets/badge/logo.json
{
"label": "",
"message": "python-hmr",
"logoSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 675.78 675.78\"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{clip-path:url(#clip-path-2);}.cls-4{fill:#3c79ab;}.cls-5{fill:#fdd837;}.cls-6{fill:#fff;}</style><clipPath id=\"clip-path\" transform=\"translate(-276.95 -142.32)\"><rect class=\"cls-1\" width=\"1280\" height=\"960.41\"/></clipPath><clipPath id=\"clip-path-2\" transform=\"translate(-276.95 -142.32)\"><rect class=\"cls-1\" width=\"1280\" height=\"960.41\"/></clipPath></defs><title>Asset 1</title><g id=\"Layer_2\" data-name=\"Layer 2\"><g id=\"Layer_1-2\" data-name=\"Layer 1\"><g class=\"cls-2\"><g class=\"cls-3\"><path class=\"cls-4\" d=\"M614.84,142.32c-184,0-333.57,147-337.78,330C281,312.65,402.33,184.56,551.49,184.56,703.11,184.56,826,316.92,826,480.21a63.36,63.36,0,0,0,126.71,0c0-186.62-151.27-337.89-337.89-337.89\" transform=\"translate(-276.95 -142.32)\"/><path class=\"cls-5\" d=\"M614.84,818.09c184,0,333.58-147,337.79-329.94-3.9,159.61-125.27,287.71-274.43,287.71-151.63,0-274.54-132.37-274.54-295.65a63.36,63.36,0,1,0-126.71,0c0,186.61,151.28,337.88,337.89,337.88\" transform=\"translate(-276.95 -142.32)\"/><path class=\"cls-6\" d=\"M917.92,480.21a26.82,26.82,0,1,1-26.82-26.82,26.82,26.82,0,0,1,26.82,26.82\" transform=\"translate(-276.95 -142.32)\"/><path class=\"cls-6\" d=\"M366.05,493.1a26.82,26.82,0,1,1-26.82-26.82,26.83,26.83,0,0,1,26.82,26.82\" transform=\"translate(-276.95 -142.32)\"/></g></g></g></g></svg>",
"logoWidth": 15,
"labelColor": "white",
"color": "#FEC550"
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 675.78 675.78"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{clip-path:url(#clip-path-2);}.cls-4{fill:#3c79ab;}.cls-5{fill:#fdd837;}.cls-6{fill:#fff;}</style><clipPath id="clip-path" transform="translate(-276.95 -142.32)"><rect class="cls-1" width="1280" height="960.41"/></clipPath><clipPath id="clip-path-2" transform="translate(-276.95 -142.32)"><rect class="cls-1" width="1280" height="960.41"/></clipPath></defs><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g class="cls-2"><g class="cls-3"><path class="cls-4" d="M614.84,142.32c-184,0-333.57,147-337.78,330C281,312.65,402.33,184.56,551.49,184.56,703.11,184.56,826,316.92,826,480.21a63.36,63.36,0,0,0,126.71,0c0-186.62-151.27-337.89-337.89-337.89" transform="translate(-276.95 -142.32)"/><path class="cls-5" d="M614.84,818.09c184,0,333.58-147,337.79-329.94-3.9,159.61-125.27,287.71-274.43,287.71-151.63,0-274.54-132.37-274.54-295.65a63.36,63.36,0,1,0-126.71,0c0,186.61,151.28,337.88,337.89,337.88" transform="translate(-276.95 -142.32)"/><path class="cls-6" d="M917.92,480.21a26.82,26.82,0,1,1-26.82-26.82,26.82,26.82,0,0,1,26.82,26.82" transform="translate(-276.95 -142.32)"/><path class="cls-6" d="M366.05,493.1a26.82,26.82,0,1,1-26.82-26.82,26.83,26.83,0,0,1,26.82,26.82" transform="translate(-276.95 -142.32)"/></g></g></g></g></svg>

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

from __future__ import annotations
__all__ = ["reload"]
from types import ModuleType
from typing import List, Callable, Sequence, Union
from hmr._reload import ModuleReloader, ObjectReloader
Reloadable = Union[ModuleType, Callable]
Reloader = Union[ModuleReloader, ObjectReloader]
def reload(
*obj: Reloadable | Sequence[Reloadable], exclude: Sequence[str] = None
) -> Reloader | List[Reloader]:
"""The reloader to proxy reloaded object
Parameters
----------
obj : The object(s) to be monitored and
reloaded when file changes on the disk
exclude : Exclude the module that you don't want to be reloaded
"""
reloaders = []
if len(obj) == 1:
if isinstance(obj[0], (list, tuple)):
obj_list = obj[0]
else:
obj_list = obj
else:
obj_list = obj
for obj in obj_list:
if isinstance(obj, ModuleType):
reloader = ModuleReloader(obj, exclude)
elif isinstance(obj, Callable):
reloader = ObjectReloader(obj, exclude)
else:
msg = (
f"Operation failed: {obj} is either a constant value or "
f"an already initialized object and cannot be reloaded. "
"To resolve this issue: "
"1. If you're attempting to pass a function or class, "
"use its name without parentheses (e.g., `func` instead of `func()`). "
"2. To access a constant, refer to it directly from its module "
"using dot notation (e.g., `module.var`)."
)
raise TypeError(msg)
reloaders.append(reloader)
if len(reloaders) == 1:
return reloaders[0]
return reloaders
from __future__ import annotations
__all__ = ["ModuleReloader", "ObjectReloader", "BaseReloader"]
import sys
import warnings
import weakref
from dataclasses import dataclass
from importlib import reload, invalidate_caches, import_module
from importlib.util import module_from_spec, find_spec
from pathlib import Path
from types import ModuleType, FunctionType
from typing import Set
from ._watcher import Watchers
def _recursive_reload(module, exclude):
reload(sys.modules.get(module.__name__))
for attr in dir(module):
attr = getattr(module, attr)
if isinstance(attr, ModuleType):
if attr.__name__.startswith(module.__name__):
if attr.__name__ not in exclude:
_recursive_reload(attr, exclude)
def get_module_by_name(name):
return module_from_spec(find_spec(name))
@dataclass
class ProxyModule:
name: str
file: str
root_name: str
root_path: str
object: object | ModuleType | FunctionType
root_object: ModuleType
exclude: Set[str] = None
def is_func(self):
return isinstance(self.object, FunctionType)
def reload(self):
_recursive_reload(self.root_object, self.exclude)
self.root_object = import_module(self.root_name)
class BaseReloader(ModuleType):
__proxy_module__: ProxyModule
def __init__(self, proxy_obj):
obj = proxy_obj.object
super().__init__(obj.__name__, obj.__doc__)
self.__proxy_module__ = proxy_obj
Watchers.add_reload(self)
# Try to reload the module when the object is created
self.__reload__()
def __repr__(self):
return f"<HMR for {self.__proxy_module__.object}>"
def __getattr__(self, name):
return getattr(self.__proxy_module__.object, name)
def __del__(self):
Watchers.delete_reload(self)
# For IDE auto-completion
@property
def __all__(self) -> list:
if hasattr(self.__proxy_module__.object, "__all__"):
return self.__proxy_module__.object.__all__
return []
# For IDE auto-completion
def __dir__(self):
return self.__proxy_module__.object.__dir__()
def __reload__(self) -> None:
raise NotImplementedError
class ModuleReloader(BaseReloader):
def __init__(self, module, exclude=None):
# If user import a submodule
# we still need to monitor the whole module to reload
if exclude is None:
exclude = set()
else:
exclude = set(exclude)
root_module = get_module_by_name(module.__name__.split(".")[0])
root_path = Path(root_module.__spec__.origin).parent
proxy = ProxyModule(
name=module.__name__,
file=module.__spec__.origin,
root_name=root_module.__name__,
root_path=str(root_path),
object=module,
root_object=root_module,
exclude=exclude,
)
super().__init__(proxy)
def __reload__(self):
invalidate_caches()
self.__proxy_module__.reload()
class ObjectReloader(BaseReloader):
def __init__(self, obj, exclude=None):
root_module = get_module_by_name(obj.__module__)
root_path = Path(root_module.__spec__.origin).parent
if exclude is None:
exclude = set()
else:
exclude = set(exclude)
proxy = ProxyModule(
name=obj.__name__,
file=root_module.__spec__.origin,
root_name=root_module.__name__,
root_path=str(root_path),
object=obj,
root_object=get_module_by_name(obj.__module__),
exclude=exclude,
)
self.__ref_instances = [] # Keep references to all instances
super().__init__(proxy)
def __call__(self, *args, **kwargs):
# When user override the __call__ method in class
try:
instance = self.__proxy_module__.object.__call__(*args, **kwargs)
except TypeError:
instance = self.__proxy_module__.object(*args, **kwargs)
if not self.__proxy_module__.is_func():
# When the class initiate
# Register a reference to the instance
# So we can replace it later
self.__ref_instances.append(weakref.ref(instance))
return instance
def __reload__(self) -> None:
"""Reload the object"""
invalidate_caches()
self.__proxy_module__.reload()
with open(self.__proxy_module__.file, "r") as f:
source_code = f.read()
locals_: dict = {}
exec(source_code, self.__proxy_module__.root_object.__dict__, locals_)
updated_object = locals_.get(self.__proxy_module__.name, None)
if updated_object is None:
warnings.warn(
"Can't reload object. If it's a decorated function, "
"use functools.wraps to "
"preserve the function signature.",
UserWarning,
)
else:
self.__proxy_module__.object = updated_object
# Replace the old reference of all instances with the new one
if not self.__proxy_module__.is_func():
for ref in self.__ref_instances:
instance = ref() # We keep weak references to objects
if instance:
instance.__class__ = updated_object
from __future__ import annotations
__all__ = ["Watchers"]
import atexit
import sys
import traceback
from datetime import datetime
from typing import Dict, TYPE_CHECKING
if TYPE_CHECKING:
from ._reload import BaseReloader
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from watchdog.observers.api import ObservedWatch
class EventsHandler(FileSystemEventHandler):
reload_list = []
_last_error = None
def on_any_event(self, event):
# print(event.src_path, event.event_type, event.is_directory)
# print("Reload list", self.reload_list)
for reloader in self.reload_list:
try:
reloader.__reload__()
except Exception:
_current = datetime.now()
error_stack = traceback.extract_stack()
# only fire the same error once
if self._last_error is None or self._last_error != error_stack:
self._last_error = error_stack
traceback.print_exc(file=sys.stderr)
return
class WatcherStorage:
def __init__(self):
self._observer = None
self.watchers: Dict[str, (ObservedWatch, EventsHandler)] = {}
atexit.register(self.__del__)
@property
def observer(self) -> Observer:
if self._observer is None:
self._observer = Observer()
self._observer.daemon = True
self._observer.start()
return self._observer
def add_reload(self, reloader: BaseReloader):
root_path = reloader.__proxy_module__.root_path
watcher = self.watchers.get(root_path)
if watcher is None:
event_handler = EventsHandler()
event_handler.reload_list.append(reloader)
watch = self.observer.schedule(event_handler, root_path, recursive=True)
self.watchers[root_path] = (watch, event_handler)
else:
watch, event_handler = watcher
event_handler.reload_list.append(reloader)
def delete_reload(self, reloader: BaseReloader):
root_path = reloader.__proxy_module__.root_path
watcher = self.watchers.get(root_path)
if watcher is not None:
watch, event_handler = watcher
# This may be emitted multiple times
# Must wrap in a try-except block
try:
event_handler.reload_list.remove(reloader)
if not event_handler.reload_list:
self.observer.unschedule(watch)
del self.watchers[root_path]
except (ValueError, KeyError):
pass
def __del__(self):
if self._observer is not None:
self._observer.unschedule_all()
self._observer.stop()
self._observer.join()
Watchers = WatcherStorage()
# def get_watchers():
# return Watchers
from .file_module import file_func
from .sub_module import sub_func
from .wrap import work_wrap
def func():
return "Hi from func"
@work_wrap
@work_wrap
def decorated_func():
return 100
class Class:
v = 1
var = 1
class Complicate:
"""Complicate class for testing purposes."""
def __init__(self):
self.x = 12
# def __repr__(self):
# return f"Complicate(x={self.x})"
def __add__(self, other):
return self.x + other.x
def __call__(self, *args, **kwargs):
return self.add(*args)
def add(self, a, b):
return a + b + self.x
def file_func():
return "Hi from file_func"
from . import Class
cls = Class()
from tests.my_pkg.wrap import wrap
def sub_func():
return "Hi from sub_func"
@wrap
@wrap
def decorated_sub_func():
return 100
class SubClass:
v = 1
import functools
def wrap(f):
def args(*arg, **kwargs):
return f(*arg, **kwargs)
return args
def work_wrap(f):
@functools.wraps(f)
def args(*arg, **kwargs):
return f(*arg, **kwargs)
return args
+5
-5

@@ -10,7 +10,7 @@ name: Build

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python env
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: 3.8
python-version: 3.11

@@ -27,4 +27,4 @@ - name: Install dependencies

FLIT_INDEX_URL: https://upload.pypi.org/legacy/
FLIT_USERNAME: ${{ secrets.PYPI_USERNAME }}
FLIT_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
FLIT_USERNAME: __token__
FLIT_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: flit publish

@@ -18,3 +18,3 @@ name: Test

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:

@@ -21,0 +21,0 @@ python-version: ${{ matrix.python-version }}

"""Hot module reload for python"""
__all__ = ["reload", "Reloader"]
__version__ = "0.2.0"
__all__ = ["reload"]
__version__ = "0.3.0"
from .api import Reloader
reload = Reloader
from ._api import reload
MIT License
Copyright (c) 2023 Mr-Milk
Copyright (c) 2024 Mr-Milk

@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy

+69
-42
Metadata-Version: 2.1
Name: python-hmr
Version: 0.2.0
Version: 0.3.0
Summary: Hot module reload for python
Author-email: Mr-Milk <yb97643@um.edu.mo>
Author-email: Mr-Milk <zym.zym1220@gmail.com>
Requires-Python: >=3.8

@@ -10,17 +10,30 @@ Description-Content-Type: text/markdown

Requires-Dist: watchdog
Requires-Dist: pytest ; extra == "dev"
Requires-Dist: ruff ; extra == "dev"
Project-URL: Home, https://github.com/mr-milk/python-hmr
Provides-Extra: dev
<img src="https://raw.githubusercontent.com/Mr-Milk/python-hmr/d642a1054d5502a020f107bebecba41abeb4c7ea/img/logo.svg" alt="python-hmr logo" align="left" height="50" />
# Python Hot Module Reload
<p align="center">
<picture align="center">
<img src="https://raw.githubusercontent.com/Mr-Milk/python-hmr/main/assets/logo.svg"
alt="python-hmr logo" height="50"/>
</picture>
</p>
<p align="center">
<i>Better debugging experience with HMR</i>
</p>
![Test status](https://img.shields.io/github/actions/workflow/status/Mr-Milk/python-hmr/test.yaml?label=Test&logo=github&style=flat-square)
![pypi](https://img.shields.io/pypi/v/python-hmr?logoColor=white&style=flat-square)
![license-mit](https://img.shields.io/github/license/Mr-Milk/python-hmr?color=blue&style=flat-square)
![Endpoint Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Mr-Milk/python-hmr/main/assets/badge/logo.json&style=social)
Automatic reload your project when files are modified.
No need to modify your source code. Works at any environment.
No need to modify your source code. Works in any environment.
![reload](https://github.com/Mr-Milk/python-hmr/blob/main/img/reload_func.gif?raw=true)
![reload](https://github.com/Mr-Milk/python-hmr/blob/main/assets/showcase/reload_demo.gif?raw=true)

@@ -46,52 +59,54 @@ Supported Syntax:

Import your dev package as usual.
> [!CAUTION]
> From v0.3.0, there is only one API, `hmr.reload`.
Import your dev packages as usual. And add 2 lines
for automatically reloading.
```python
import my_pkg
import dev
import hmr
dev = hmr.reload(dev)
```
Add 2 lines to automatically reload your source code.
If you have multiple modules to reload, you can do it like this.
```python
import my_pkg
from dev import run1, run2
import hmr
my_pkg = hmr.reload(my_pkg)
run1, run2 = hmr.reload(run1, run2)
```
Now you are ready to go!
Now you are ready to go! Try to modify the `run1` or `run2`
and see the magic happen.
## Usage Manual
### Module/Submodule reload
## Detailed Usage
```python
import my_pkg.sub_module as sub
import hmr
sub = hmr.reload(sub)
```
### Function/Class instance
### Function/Class reload
When you try to add HMR for a function or class, remember to
pass the name of the function or class instance without parenthesis.
No difference to reloading module
```python
from my_pkg import func, Class
from dev import Runner
import hmr
func = hmr.reload(func)
Class = hmr.reload(Class)
Runner = hmr.reload(Runner)
a = Runner()
b = Runner()
```
If your have multiple class instance, they will all be updated.
Both `a` and `b` will be updated.
> [!IMPORTANT]
> Here, when both `a` and `b` will be updated after reloading. This may be helpful
> if you have an expansive state store within the class instance.
>
> However, it's suggested to reinitialize the class instance after reloading.
```python
a = Class()
b = Class()
```
### @Decorated Function reload
### @Decorated Function

@@ -101,6 +116,17 @@ Use [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps) to preserve

### State handling
```python
import functools
If your application is not stateless, it's suggested that you
group all your state variable into the same `.py` file like `state.py`
def work(f):
@functools.wraps(f)
def args(*arg, **kwargs):
return f(*arg, **kwargs)
return args
```
### Stateful application
If your application is stateful, you can exclude the state from being reloaded.
For simplicity, you can group all your state variable into the same `.py` file like `state.py`
and exclude that from being reloaded.

@@ -112,17 +138,17 @@

```python
import my_pkg
import dev
import hmr
my_pkg = hmr.reload(my_pkg, excluded=["my_pkg.state"])
dev = hmr.reload(dev, exclude=["dev.state"])
```
The `my_pkg/state.py` will not be reloaded, the state will persist.
In this way `dev/state.py` will not be reloaded, the state will persist.
The same apply when reloading a function or class.
This also apply when reloading a function or class.
```python
from my_pkg import func
from dev import run
import hmr
func = hmr.reload(func, excluded=["my_pkg.state"])
run = hmr.reload(run, exclude=["dev.state"])
```

@@ -137,1 +163,2 @@

- [reloadr](https://github.com/hoh/reloadr)

@@ -7,3 +7,3 @@ [build-system]

name = "python-hmr"
authors = [{name = "Mr-Milk", email = "yb97643@um.edu.mo"}]
authors = [{name = "Mr-Milk", email = "zym.zym1220@gmail.com"}]
license = {file = "LICENSE"}

@@ -18,2 +18,8 @@ readme = "README.md"

[tool.flit.module]
name = "hmr"
name = "hmr"
[project.optional-dependencies]
dev = [
"pytest",
"ruff",
]
+64
-41

@@ -1,14 +0,24 @@

<img src="https://raw.githubusercontent.com/Mr-Milk/python-hmr/d642a1054d5502a020f107bebecba41abeb4c7ea/img/logo.svg" alt="python-hmr logo" align="left" height="50" />
# Python Hot Module Reload
<p align="center">
<picture align="center">
<img src="https://raw.githubusercontent.com/Mr-Milk/python-hmr/main/assets/logo.svg"
alt="python-hmr logo" height="50"/>
</picture>
</p>
<p align="center">
<i>Better debugging experience with HMR</i>
</p>
![Test status](https://img.shields.io/github/actions/workflow/status/Mr-Milk/python-hmr/test.yaml?label=Test&logo=github&style=flat-square)
![pypi](https://img.shields.io/pypi/v/python-hmr?logoColor=white&style=flat-square)
![license-mit](https://img.shields.io/github/license/Mr-Milk/python-hmr?color=blue&style=flat-square)
![Endpoint Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Mr-Milk/python-hmr/main/assets/badge/logo.json&style=social)
Automatic reload your project when files are modified.
No need to modify your source code. Works at any environment.
No need to modify your source code. Works in any environment.
![reload](https://github.com/Mr-Milk/python-hmr/blob/main/img/reload_func.gif?raw=true)
![reload](https://github.com/Mr-Milk/python-hmr/blob/main/assets/showcase/reload_demo.gif?raw=true)

@@ -34,52 +44,54 @@ Supported Syntax:

Import your dev package as usual.
> [!CAUTION]
> From v0.3.0, there is only one API, `hmr.reload`.
Import your dev packages as usual. And add 2 lines
for automatically reloading.
```python
import my_pkg
import dev
import hmr
dev = hmr.reload(dev)
```
Add 2 lines to automatically reload your source code.
If you have multiple modules to reload, you can do it like this.
```python
import my_pkg
from dev import run1, run2
import hmr
my_pkg = hmr.reload(my_pkg)
run1, run2 = hmr.reload(run1, run2)
```
Now you are ready to go!
Now you are ready to go! Try to modify the `run1` or `run2`
and see the magic happen.
## Usage Manual
### Module/Submodule reload
## Detailed Usage
```python
import my_pkg.sub_module as sub
import hmr
sub = hmr.reload(sub)
```
### Function/Class instance
### Function/Class reload
When you try to add HMR for a function or class, remember to
pass the name of the function or class instance without parenthesis.
No difference to reloading module
```python
from my_pkg import func, Class
from dev import Runner
import hmr
func = hmr.reload(func)
Class = hmr.reload(Class)
Runner = hmr.reload(Runner)
a = Runner()
b = Runner()
```
If your have multiple class instance, they will all be updated.
Both `a` and `b` will be updated.
> [!IMPORTANT]
> Here, when both `a` and `b` will be updated after reloading. This may be helpful
> if you have an expansive state store within the class instance.
>
> However, it's suggested to reinitialize the class instance after reloading.
```python
a = Class()
b = Class()
```
### @Decorated Function reload
### @Decorated Function

@@ -89,6 +101,17 @@ Use [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps) to preserve

### State handling
```python
import functools
If your application is not stateless, it's suggested that you
group all your state variable into the same `.py` file like `state.py`
def work(f):
@functools.wraps(f)
def args(*arg, **kwargs):
return f(*arg, **kwargs)
return args
```
### Stateful application
If your application is stateful, you can exclude the state from being reloaded.
For simplicity, you can group all your state variable into the same `.py` file like `state.py`
and exclude that from being reloaded.

@@ -100,17 +123,17 @@

```python
import my_pkg
import dev
import hmr
my_pkg = hmr.reload(my_pkg, excluded=["my_pkg.state"])
dev = hmr.reload(dev, exclude=["dev.state"])
```
The `my_pkg/state.py` will not be reloaded, the state will persist.
In this way `dev/state.py` will not be reloaded, the state will persist.
The same apply when reloading a function or class.
This also apply when reloading a function or class.
```python
from my_pkg import func
from dev import run
import hmr
func = hmr.reload(func, excluded=["my_pkg.state"])
run = hmr.reload(run, exclude=["dev.state"])
```

@@ -124,2 +147,2 @@

- [auto-reloader](https://github.com/moisutsu/auto-reloader)
- [reloadr](https://github.com/hoh/reloadr)
- [reloadr](https://github.com/hoh/reloadr)

@@ -10,3 +10,3 @@ import os

TEST_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
TEST_DIR = Path(__file__).parent

@@ -16,3 +16,7 @@

def pytest_addoption(parser):
parser.addoption("--wait", action="store", default=0.3, )
parser.addoption(
"--wait",
action="store",
default=0.3,
)

@@ -22,3 +26,3 @@

option_value = float(metafunc.config.option.wait)
if 'wait' in metafunc.fixturenames and option_value is not None:
if "wait" in metafunc.fixturenames and option_value is not None:
metafunc.parametrize("wait", [option_value])

@@ -28,6 +32,6 @@

def read_replace_write(file: Union[str, Path], replace: Tuple):
with open(file, 'r') as f:
with open(file, "r") as f:
raw = f.read()
with open(file, 'w') as f:
with open(file, "w") as f:
text = raw.replace(*replace)

@@ -41,7 +45,7 @@ f.write(text)

def pkg_name():
return f'my_pkg_{str(uuid4())}'
return f"my_pkg_{str(uuid4())}"
def copy_pkg(pkg_name):
pkg = TEST_DIR.parent / 'my_pkg'
pkg = TEST_DIR / "my_pkg"
dest = TEST_DIR / pkg_name

@@ -53,3 +57,3 @@ if dest.exists():

@pytest.fixture(scope='function', autouse=True)
@pytest.fixture(scope="function", autouse=True)
def create_package(pkg_name):

@@ -65,3 +69,3 @@ copy_pkg(pkg_name)

try:
if platform.system() in ['Linux', 'Darwin']:
if platform.system() in ["Linux", "Darwin"]:
os.system(f"rm -rf {pkg_dir.absolute()}")

@@ -74,16 +78,20 @@ else:

@pytest.fixture(scope='function')
@pytest.fixture(scope="function")
def package(pkg_name):
pkg_dir = TEST_DIR / pkg_name
pkg_init: Path = pkg_dir / '__init__.py'
pkg_file_module: Path = pkg_dir / 'file_module.py'
pkg_sub_module_init: Path = pkg_dir / 'sub_module' / '__init__.py'
pkg_subsub_module_init: Path = pkg_dir / 'sub_module' / 'subsub_module' / '__init__.py'
pkg_init: Path = pkg_dir / "__init__.py"
pkg_file_module: Path = pkg_dir / "file_module.py"
pkg_sub_module_init: Path = pkg_dir / "sub_module" / "__init__.py"
pkg_subsub_module_init: Path = (
pkg_dir / "sub_module" / "subsub_module" / "__init__.py"
)
class Package:
pkg_name = pkg_name
pkg_init: Path = pkg_dir / '__init__.py'
pkg_file_module: Path = pkg_dir / 'file_module.py'
pkg_sub_module_init: Path = pkg_dir / 'sub_module' / '__init__.py'
pkg_subsub_module_init: Path = pkg_dir / 'sub_module' / 'subsub_module' / '__init__.py'
pkg_init: Path = pkg_dir / "__init__.py"
pkg_file_module: Path = pkg_dir / "file_module.py"
pkg_sub_module_init: Path = pkg_dir / "sub_module" / "__init__.py"
pkg_subsub_module_init: Path = (
pkg_dir / "sub_module" / "subsub_module" / "__init__.py"
)

@@ -110,7 +118,11 @@ @staticmethod

def modify_file_module_func():
read_replace_write(pkg_file_module, ("Hi from file_func", "Hello from file_func"))
read_replace_write(
pkg_file_module, ("Hi from file_func", "Hello from file_func")
)
@staticmethod
def modify_sub_module_func():
read_replace_write(pkg_sub_module_init, ("Hi from sub_func", "Hello from sub_func"))
read_replace_write(
pkg_sub_module_init, ("Hi from sub_func", "Hello from sub_func")
)

@@ -117,0 +129,0 @@ @staticmethod

@@ -8,3 +8,3 @@ import importlib

from hmr import Reloader
from hmr import reload

@@ -16,6 +16,7 @@ sys.path.insert(0, str(Path(__file__).parent.resolve()))

def test_module(package, pkg_name, wait):
my_pkg = importlib.import_module(pkg_name) # import pkg_name
my_pkg = Reloader(my_pkg, excluded=['my_pkg.sub_module'])
my_pkg = reload(my_pkg, exclude=["my_pkg.sub_module"])
assert my_pkg.func() == "Hi from func"

@@ -31,3 +32,3 @@ package.modify_module_func()

sub = importlib.import_module(f"{pkg_name}.sub_module")
sub = Reloader(sub)
sub = reload(sub)

@@ -44,3 +45,3 @@ assert sub.sub_func() == "Hi from sub_func"

my_pkg = importlib.import_module(pkg_name) # import pkg_name
my_pkg = Reloader(my_pkg)
my_pkg = reload(my_pkg)
# sleep(wait)

@@ -47,0 +48,0 @@ # check.equal(my_pkg.func(), "Hi from func")

@@ -8,3 +8,3 @@ import importlib

from hmr import Reloader
from hmr import reload

@@ -15,3 +15,3 @@ sys.path.insert(0, str(Path(__file__).parent.resolve()))

def check_func(func, modify_func, before, after, wait=0):
func = Reloader(func)
func = reload(func)
assert func() == before

@@ -25,3 +25,3 @@

def check_cls(cls, modify_cls, before, after, wait=0):
cls = Reloader(cls)
cls = reload(cls)
c = cls()

@@ -38,5 +38,7 @@ assert c.v == before

func = importlib.import_module(pkg_name).__getattribute__(
"func") # from x import func
check_func(func, package.modify_module_func, "Hi from func",
"Hello from func", wait)
"func"
) # from x import func
check_func(
func, package.modify_module_func, "Hi from func", "Hello from func", wait
)

@@ -46,6 +48,12 @@

def test_sub_func(package, pkg_name, wait):
sub_func = importlib.import_module(
f"{pkg_name}.sub_module").__getattribute__("sub_func")
check_func(sub_func, package.modify_sub_module_func, "Hi from sub_func",
"Hello from sub_func", wait)
sub_func = importlib.import_module(f"{pkg_name}.sub_module").__getattribute__(
"sub_func"
)
check_func(
sub_func,
package.modify_sub_module_func,
"Hi from sub_func",
"Hello from sub_func",
wait,
)

@@ -61,4 +69,5 @@

def test_sub_class(package, pkg_name, wait):
SubClass = importlib.import_module(
f"{pkg_name}.sub_module").__getattribute__("SubClass")
SubClass = importlib.import_module(f"{pkg_name}.sub_module").__getattribute__(
"SubClass"
)
check_cls(SubClass, package.modify_sub_module_class, 1, 2, wait)

@@ -71,3 +80,3 @@

var = importlib.import_module(pkg_name).__getattribute__("var")
var = Reloader(var)
var = reload(var)

@@ -78,3 +87,3 @@

func = importlib.import_module(pkg_name).__getattribute__("func")
func = Reloader(func)
func = reload(func)
ref_f = func

@@ -93,3 +102,3 @@

Class = importlib.import_module(pkg_name).__getattribute__("Class")
Class = Reloader(Class)
Class = reload(Class)
assert Class.v == 1

@@ -111,5 +120,5 @@ a = Class()

decorated_func = importlib.import_module(pkg_name).__getattribute__(
"decorated_func")
check_func(decorated_func, package.modify_module_decorated_func, 100, 10,
wait)
"decorated_func"
)
check_func(decorated_func, package.modify_module_decorated_func, 100, 10, wait)

@@ -120,3 +129,4 @@

dsf = importlib.import_module(f"{pkg_name}.sub_module").__getattribute__(
"decorated_sub_func")
"decorated_sub_func"
)
check_func(dsf, package.modify_sub_module_decorated_func, 100, 10, wait)
__all__ = ['Reloader']
import sys
from datetime import datetime
from types import ModuleType
from typing import Callable, Any
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from hmr.reload import ModuleReloader, ObjectReloader
class EventsHandler(FileSystemEventHandler):
reloader = None
_last_error = None
_updated = None
def on_any_event(self, event):
try:
self.reloader.fire()
except Exception as e:
_current = datetime.now()
# only fire the same error once
if self._last_error != str(e):
self._last_error = str(e)
print(e, file=sys.stderr)
return
if (_current - self._updated).total_seconds() > 1:
print(e, file=sys.stderr)
return
class Reloader:
"""The reloader to proxy reloaded object
Args:
obj: The object to be monitored and
reloaded when file changes on the disk
excluded: Excluded the module that you don't want to be reloaded
"""
_observer = None
def __init__(self,
obj: Any,
excluded=None
):
if isinstance(obj, ModuleType):
self.reloader = ModuleReloader(obj, excluded)
elif isinstance(obj, Callable):
self.reloader = ObjectReloader(obj, excluded)
else:
msg = "Hot Module Reload supports Module, Function and Class; " \
"Do not pass initialize class or function, " \
"use `func` not `func()`. " \
"For static variable " \
"access it from the module like `module.var`"
raise TypeError(msg)
path = self.reloader.get_module_path()
event_handler = EventsHandler()
event_handler.reloader = self.reloader
event_handler._updated = datetime.now()
observer = Observer()
self._observer = observer
observer.schedule(event_handler, str(path),
recursive=True)
observer.setDaemon(True)
observer.start()
def __stop__(self):
"""Shutdown the monitor"""
if self._observer is not None:
self._observer.unschedule_all()
self._observer.stop()
def __del__(self):
return self.__stop__()
def __getattr__(self, name):
return getattr(self.reloader, name)
def __call__(self, *args, **kwargs):
return self.reloader.__call__(*args, **kwargs)
import sys
import warnings
import weakref
from importlib import reload, invalidate_caches, import_module
from importlib.util import module_from_spec, find_spec
from pathlib import Path
from types import ModuleType, FunctionType
def _recursive_reload(module, excluded):
reload(sys.modules.get(module.__name__))
for attr in dir(module):
attr = getattr(module, attr)
if isinstance(attr, ModuleType):
if attr.__name__.startswith(module.__name__):
if attr.__name__ not in excluded:
_recursive_reload(attr, excluded)
def get_module_by_name(name):
return module_from_spec(find_spec(name))
class ModuleReloader:
def __init__(self, module, excluded=None):
# If user import a submodule
# we still need to monitor the whole module for rerun
entry_module = get_module_by_name(module.__name__.split(".")[0])
self.module = module
self.entry_module = entry_module
self.excluded = [] if excluded is None else excluded
def __getattr__(self, name):
return getattr(self.module, name)
def fire(self):
invalidate_caches()
_recursive_reload(self.entry_module, self.excluded)
self.module = import_module(self.module.__name__)
self.entry_module = import_module(self.entry_module.__name__)
def get_module_path(self):
return Path(self.entry_module.__spec__.origin).parent
class ObjectReloader(ModuleReloader):
def __init__(self, obj, excluded=None):
self.object = obj
self.is_func = isinstance(obj, FunctionType)
self.object_name = obj.__name__
self.object_module = get_module_by_name(obj.__module__)
self.object_file = self.object_module.__spec__.origin
self.original_object = obj
self._instances = [] # Keep references to all instances
super().__init__(self.object_module, excluded=excluded)
def __call__(self, *args, **kwargs):
instance = self.object.__call__(*args, **kwargs)
if not self.is_func:
# When the class initiate
# Register a reference to the instance
# So we can replace it later
self._instances.append(weakref.ref(instance))
return instance
def __getattr__(self, name):
return getattr(self.object, name)
def fire(self) -> None:
"""Reload the object"""
super().fire()
with open(self.object_file, 'r') as f:
source_code = f.read()
locals_: dict = {}
exec(source_code, self.module.__dict__, locals_)
self.object = locals_.get(self.object_name, None)
if self.object is None:
self.object = self.original_object
warnings.warn("Can't reload object. If it's a decorated function, "
"use functools.wraps to "
"preserve the function signature.", UserWarning)
# Replace the old reference of all instances with the new one
if not self.is_func:
for ref in self._instances:
instance = ref() # We keep weak references to objects
if instance:
instance.__class__ = self.object
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 675.78 675.78"><defs><style>.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{clip-path:url(#clip-path-2);}.cls-4{fill:#3c79ab;}.cls-5{fill:#fdd837;}.cls-6{fill:#fff;}</style><clipPath id="clip-path" transform="translate(-276.95 -142.32)"><rect class="cls-1" width="1280" height="960.41"/></clipPath><clipPath id="clip-path-2" transform="translate(-276.95 -142.32)"><rect class="cls-1" width="1280" height="960.41"/></clipPath></defs><title>Asset 1</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g class="cls-2"><g class="cls-3"><path class="cls-4" d="M614.84,142.32c-184,0-333.57,147-337.78,330C281,312.65,402.33,184.56,551.49,184.56,703.11,184.56,826,316.92,826,480.21a63.36,63.36,0,0,0,126.71,0c0-186.62-151.27-337.89-337.89-337.89" transform="translate(-276.95 -142.32)"/><path class="cls-5" d="M614.84,818.09c184,0,333.58-147,337.79-329.94-3.9,159.61-125.27,287.71-274.43,287.71-151.63,0-274.54-132.37-274.54-295.65a63.36,63.36,0,1,0-126.71,0c0,186.61,151.28,337.88,337.89,337.88" transform="translate(-276.95 -142.32)"/><path class="cls-6" d="M917.92,480.21a26.82,26.82,0,1,1-26.82-26.82,26.82,26.82,0,0,1,26.82,26.82" transform="translate(-276.95 -142.32)"/><path class="cls-6" d="M366.05,493.1a26.82,26.82,0,1,1-26.82-26.82,26.83,26.83,0,0,1,26.82,26.82" transform="translate(-276.95 -142.32)"/></g></g></g></g></svg>

Sorry, the diff of this file is not supported yet

from .file_module import file_func
from .sub_module import sub_func
from .wrap import work_wrap
def func():
return "Hi from func"
@work_wrap
@work_wrap
def decorated_func():
return 100
class Class:
v = 1
var = 1
def file_func():
return "Hi from file_func"
from . import Class
cls = Class()
from my_pkg.wrap import wrap
def sub_func():
return "Hi from sub_func"
@wrap
@wrap
def decorated_sub_func():
return 100
class SubClass:
v = 1
import functools
def wrap(f):
def args(*arg, **kwargs):
return f(*arg, **kwargs)
return args
def work_wrap(f):
@functools.wraps(f)
def args(*arg, **kwargs):
return f(*arg, **kwargs)
return args