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.1.1
to
0.2.0
+28
.github/workflows/build.yaml
name: Build
on: push
jobs:
Build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python env
uses: actions/setup-python@v3
with:
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flit
pip install .
- name: Publish
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
env:
FLIT_INDEX_URL: https://upload.pypi.org/legacy/
FLIT_USERNAME: ${{ secrets.PYPI_USERNAME }}
FLIT_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: flit publish
name: Test
on: [push, pull_request]
jobs:
Test:
runs-on: ${{ matrix.os }}
if: "! contains(toJSON(github.event.commits.*.message), '[skip-ci]')"
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: [3.8]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dev package
run: |
python -m pip install --upgrade pip
pip install -e .
- name: Test with pytest
run: |
pip install -r requirements.dev.txt
- name: Publish to test.ipynb pypi
env:
FLIT_INDEX_URL: https://test.ipynb.pypi.org/legacy/
FLIT_USERNAME: ${{ secrets.PYPI_USERNAME }}
FLIT_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: flit publish || exit 0
.idea/
.vscode/
*.ipynb
*.DS_Store
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
<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
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
[project]
name = "python-hmr"
authors = [{name = "Mr-Milk", email = "yb97643@um.edu.mo"}]
license = {file = "LICENSE"}
readme = "README.md"
classifiers = ["License :: OSI Approved :: MIT License"]
dynamic = ["version", "description"]
requires-python = ">=3.8"
urls = {Home="https://github.com/mr-milk/python-hmr"}
dependencies = ["watchdog"]
[tool.flit.module]
name = "hmr"
{
"extends": [
"config:base"
]
}
# package
watchdog
# test
pytest
pytest-cov
pytest-check
pytest-xdist
setuptools
import os
import platform
import shutil
from pathlib import Path
from typing import Tuple, Union
from uuid import uuid4
import pytest
TEST_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
# every function need to sleep for a while to wait for the reload to complete
def pytest_addoption(parser):
parser.addoption("--wait", action="store", default=0.3, )
def pytest_generate_tests(metafunc):
option_value = float(metafunc.config.option.wait)
if 'wait' in metafunc.fixturenames and option_value is not None:
metafunc.parametrize("wait", [option_value])
def read_replace_write(file: Union[str, Path], replace: Tuple):
with open(file, 'r') as f:
raw = f.read()
with open(file, 'w') as f:
text = raw.replace(*replace)
f.write(text)
# create a unique directory for each test
# we can't reload the same pkg name
@pytest.fixture
def pkg_name():
return f'my_pkg_{str(uuid4())}'
def copy_pkg(pkg_name):
pkg = TEST_DIR.parent / 'my_pkg'
dest = TEST_DIR / pkg_name
if dest.exists():
shutil.rmtree(dest)
shutil.copytree(pkg, dest)
@pytest.fixture(scope='function', autouse=True)
def create_package(pkg_name):
copy_pkg(pkg_name)
pkg_dir = TEST_DIR / pkg_name
yield
try:
shutil.rmtree(pkg_dir)
# When test in NSF file system this is a workaround
except Exception as e:
pass
try:
if platform.system() in ['Linux', 'Darwin']:
os.system(f"rm -rf {pkg_dir.absolute()}")
else:
os.system(f"rmdir \\Q \\S {pkg_dir.absolute()}")
except Exception as e:
pass
@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'
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'
@staticmethod
def raise_syntax_error():
read_replace_write(pkg_init, ("return", "return_"))
@staticmethod
def modify_module_func():
read_replace_write(pkg_init, ("Hi from func", "Hello from func"))
# with open(pkg_init, 'r') as f:
# print(f.read())
@staticmethod
def modify_module_decorated_func():
read_replace_write(pkg_init, ("return 100", "return 10"))
@staticmethod
def modify_module_class():
read_replace_write(pkg_init, ("v = 1", "v = 2"))
@staticmethod
def modify_file_module_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"))
@staticmethod
def modify_sub_module_decorated_func():
read_replace_write(pkg_sub_module_init, ("return 100", "return 10"))
@staticmethod
def modify_sub_module_class():
read_replace_write(pkg_sub_module_init, ("v = 1", "v = 2"))
@staticmethod
def modify_subsubmodule():
read_replace_write(pkg_subsub_module_init, ("x = 1", "x = 2"))
return Package()
import importlib
import sys
from pathlib import Path
from time import sleep
import pytest
from hmr import Reloader
sys.path.insert(0, str(Path(__file__).parent.resolve()))
# import X
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'])
assert my_pkg.func() == "Hi from func"
package.modify_module_func()
sleep(wait)
# check.equal(my_pkg.func(), "Hello from func")
assert my_pkg.func() == "Hello from func"
# import X.Y as A
def test_submodule(package, pkg_name, wait):
sub = importlib.import_module(f"{pkg_name}.sub_module")
sub = Reloader(sub)
assert sub.sub_func() == "Hi from sub_func"
package.modify_sub_module_func()
sleep(wait)
assert sub.sub_func() == "Hello from sub_func"
@pytest.mark.xfail
def test_syntax_error(package, pkg_name, wait):
my_pkg = importlib.import_module(pkg_name) # import pkg_name
my_pkg = Reloader(my_pkg)
# sleep(wait)
# check.equal(my_pkg.func(), "Hi from func")
assert my_pkg.func() == "Hi from func"
package.raise_syntax_error()
sleep(wait)
# check.equal(my_pkg.func(), "Hello from func")
assert my_pkg.func() == "Hello from func"
import importlib
import sys
from pathlib import Path
from time import sleep
import pytest
from hmr import Reloader
sys.path.insert(0, str(Path(__file__).parent.resolve()))
def check_func(func, modify_func, before, after, wait=0):
func = Reloader(func)
assert func() == before
modify_func()
sleep(wait)
assert func.__call__() == after
def check_cls(cls, modify_cls, before, after, wait=0):
cls = Reloader(cls)
c = cls()
assert c.v == before
modify_cls()
sleep(wait)
assert c.v == after
# from X import func
def test_func(package, pkg_name, wait):
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)
# from X.Y import func
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)
# from X import class
def test_class(package, pkg_name, wait):
Class = importlib.import_module(pkg_name).__getattribute__("Class")
check_cls(Class, package.modify_module_class, 1, 2, wait)
# from X.Y import class
def test_sub_class(package, pkg_name, wait):
SubClass = importlib.import_module(
f"{pkg_name}.sub_module").__getattribute__("SubClass")
check_cls(SubClass, package.modify_sub_module_class, 1, 2, wait)
# from X import var
@pytest.mark.xfail
def test_var(pkg_name):
var = importlib.import_module(pkg_name).__getattribute__("var")
var = Reloader(var)
# test ref object reload
def test_func_ref_reload(package, pkg_name, wait):
func = importlib.import_module(pkg_name).__getattribute__("func")
func = Reloader(func)
ref_f = func
assert func() == "Hi from func"
assert ref_f() == "Hi from func"
package.modify_module_func()
sleep(wait)
assert func() == "Hello from func"
assert ref_f() == "Hello from func"
def test_class_ref_reload(package, pkg_name, wait):
Class = importlib.import_module(pkg_name).__getattribute__("Class")
Class = Reloader(Class)
assert Class.v == 1
a = Class()
b = Class()
assert a.v == 1
assert b.v == 1
package.modify_module_class()
sleep(wait)
assert a.v == 2
assert b.v == 2
# test decorated function
def test_decoreated_function_with_signature(package, pkg_name, wait):
decorated_func = importlib.import_module(pkg_name).__getattribute__(
"decorated_func")
check_func(decorated_func, package.modify_module_decorated_func, 100, 10,
wait)
@pytest.mark.xfail
def test_decoreated_function_no_signature(package, pkg_name, wait):
dsf = importlib.import_module(f"{pkg_name}.sub_module").__getattribute__(
"decorated_sub_func")
check_func(dsf, package.modify_sub_module_decorated_func, 100, 10, wait)
+5
-1

@@ -1,3 +0,7 @@

__all__ = ["Reloader"]
"""Hot module reload for python"""
__all__ = ["reload", "Reloader"]
__version__ = "0.2.0"
from .api import Reloader
reload = Reloader
+43
-52
__all__ = ['Reloader']
import sys
from importlib.util import find_spec, module_from_spec
from pathlib import Path
from datetime import datetime
from types import ModuleType
from typing import List, Callable, Optional, Union, Any
from typing import Callable, Any
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from watchdog.observers.api import ObservedWatch
from hmr.reload import ReloadModule, ReloadObject
from hmr.reload import ModuleReloader, ObjectReloader

@@ -19,7 +18,9 @@

_last_error = None
_updated = None
def on_any_event(self, event):
try:
self.reloader.reload()
self.reloader.fire()
except Exception as e:
_current = datetime.now()
# only fire the same error once

@@ -29,4 +30,9 @@ if 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:

@@ -36,67 +42,52 @@ """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, only works when obj is `ModuleType`
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
Methods:
reload: Reload the object
stop: Stop the monitor process
If you object happens to have the same attribute name as `reload` and `stop`, directly call obj.__getattr__(attr)
to access your original attribute instead of the reloader's methods.
"""
_module: Optional[ModuleType] = None
_object: Union[Callable, ReloadObject, None] = None
_object_type: Union[ModuleType, Callable, None] = None
_excluded: List = None
_last_error: str = None
_observer: Optional[Observer] = None
_watch: Optional[ObservedWatch] = None
_observer = None
def __init__(self,
obj: Any,
excluded: Optional[List[str]] = None
excluded=None
):
if isinstance(obj, ModuleType):
self._object_type = ModuleType
self._module = obj
self.reloader = ModuleReloader(obj, excluded)
elif isinstance(obj, Callable):
self._object_type = Callable
self._module = module_from_spec(find_spec(obj.__module__))
self._object = ReloadObject(obj, self._module)
self.reloader = ObjectReloader(obj, excluded)
else:
raise TypeError("Hot Module Reload are supported for Module, Function and Class; Do not pass"
"initialized class or function, use `func` not `func()`. "
"If it's a static variable since we can't resolve its source"
", try access it from the reloaded module. eg. my_module.variable")
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)
if isinstance(excluded, List):
self._excluded = excluded
path = self.reloader.get_module_path()
event_handler = EventsHandler()
event_handler.reloader = self.reloader
event_handler._updated = datetime.now()
path = Path(self._module.__spec__.origin).parent
event_handler = EventsHandler()
event_handler.reloader = self
observer = Observer()
self._observer = observer
self._watch = observer.schedule(event_handler, str(path), recursive=True)
observer.schedule(event_handler, str(path),
recursive=True)
observer.setDaemon(True)
observer.start()
def reload(self):
"""Reload the object"""
ReloadModule(self._module, excluded=self._excluded).fire(self._module)
if self._object is not None:
self._object.fire()
def __stop__(self):
"""Shutdown the monitor"""
if self._observer is not None:
self._observer.unschedule_all()
self._observer.stop()
def stop(self):
"""Stop the monitor and reload"""
self._observer.unschedule(self._watch)
def __del__(self):
return self.__stop__()
def __getattr__(self, name):
return getattr(self.reloader, name)
def __call__(self, *args, **kwargs):
return self._object.__call__(*args, **kwargs)
def __getattr__(self, name):
if self._object_type is ModuleType:
return getattr(self._module, name)
else:
return getattr(self._object, name)
return self.reloader.__call__(*args, **kwargs)
import sys
import warnings
import weakref
from importlib import reload
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
class ReloadModule:
module = None
excluded = []
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
if excluded is not None:
self.excluded = excluded
self.entry_module = entry_module
self.excluded = [] if excluded is None else excluded
def fire(self, module):
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 self.excluded:
self.fire(attr)
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__)
class ReloadObject:
def get_module_path(self):
return Path(self.entry_module.__spec__.origin).parent
def __init__(self, obj, module):
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 = module
self.object_file = module.__spec__.origin
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)

@@ -41,3 +65,5 @@ def __call__(self, *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))

@@ -51,10 +77,12 @@ return instance

"""Reload the object"""
super().fire()
with open(self.object_file, 'r') as f:
source_code = f.read()
locals_: dict = {}
exec(source_code, self.object_module.__dict__, locals_)
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 "
warnings.warn("Can't reload object. If it's a decorated function, "
"use functools.wraps to "
"preserve the function signature.", UserWarning)

@@ -61,0 +89,0 @@

MIT License
Copyright (c) 2021 Mr-Milk
Copyright (c) 2023 Mr-Milk

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

+128
-149
Metadata-Version: 2.1
Name: python-hmr
Version: 0.1.1
Version: 0.2.0
Summary: Hot module reload for python
Home-page: https://github.com/Mr-Milk/python-hmr
Author: Mr-Milk
Author-email: zym.zym1220@gmail.com
License: MIT License
Description: <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
![Test status](https://img.shields.io/github/workflow/status/Mr-Milk/python-hmr/Test?label=Test&logo=github&style=flat-square)
![codecov](https://img.shields.io/codecov/c/github/Mr-Milk/python-hmr?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)
Automatic reload your project when files are modified.
No need to modify your source code.
![reload](https://github.com/Mr-Milk/python-hmr/blob/main/img/reload_func.gif?raw=true)
Supported Syntax:
- ✅ ```import X```
- ✅ ```import X as Y```
- ✅ ```from X import Y```
- ✅ ```from X import Y as A```
Supported Types:
- ✅ `Module`
- ✅ `Function`
- ✅ `Class`
## Installation
```shell
pip install python-hmr
```
## Usage
Just import your developing package and replace it with `Reloader`.
Then you can use it exactly like how you use a module before.
```python
import my_pkg
from hmr import Reloader
my_pkg = Reloader(my_pkg)
my_pkg.func()
# >>> "Hi from func"
```
Or you can manually reload it
```python
my_pkg.reload()
```
To stop the reloading
```python
my_pkg.stop()
```
### Module/Submodule reload
```python
import my_pkg.sub_module as sub
from hmr import Reloader
sub = Reloader(sub)
```
### Function/Class reload
No difference to reloading module
```python
from my_pkg import func, Class
from hmr import Reloader
func = Reloader(func)
Class = Reloader(Class)
```
If your have multiple class instance, they will all be updated.
Both `a` and `b` will be updated.
```python
a = Class()
b = Class()
```
### @Decorated Function reload
Use [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps) to preserve
signature of your function, or the function information will be replaced by the decorator itself.
### State handling
If your application contains submodule that handle state,
you can exclude it from reloading. You need to move it to
a new `.py` file like `state.py` and everything from that
file will not be reloaded.
> Make sure you know what you are doing.
> This could lead to unexpected behavior and unreproducible bugs.
```python
import my_pkg
from hmr import Reloader
my_pkg = Reloader(my_pkg, excluded=["my_pkg.state"])
```
This will exclude the `my_pkg/state.py` from reloading.
Even you only want to reload a submodule or a function, you
still need to provide the `excluded` argument.
```python
import my_pkg.sub_module as sub
from my_pkg import func
from hmr import Reloader
sub = Reloader(sub, excluded=["my_pkg.state"])
func = Reloader(func, excluded=["my_pkg.state"])
```
## Implementation
Current implementation is relied on the `importlib.reload`,
which is not very graceful when handling state. Direct reading of
AST may be a better solution for hot module reload in python,
but it's too complicated, I might try it in the future.
## Acknowledgement
Inspired from the following package.
- [auto-reloader](https://github.com/moisutsu/auto-reloader)
- [reloadr](https://github.com/hoh/reloadr)
Platform: UNKNOWN
Author-email: Mr-Milk <yb97643@um.edu.mo>
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Requires-Dist: watchdog
Project-URL: Home, https://github.com/mr-milk/python-hmr
<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
![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)
Automatic reload your project when files are modified.
No need to modify your source code. Works at any environment.
![reload](https://github.com/Mr-Milk/python-hmr/blob/main/img/reload_func.gif?raw=true)
Supported Syntax:
- ✅ ```import X```
- ✅ ```from X import Y```
Supported Types:
- ✅ `Module`
- ✅ `Function`
- ✅ `Class`
## Installation
```shell
pip install python-hmr
```
## Quick Start
Import your dev package as usual.
```python
import my_pkg
```
Add 2 lines to automatically reload your source code.
```python
import my_pkg
import hmr
my_pkg = hmr.reload(my_pkg)
```
Now you are ready to go!
## Usage Manual
### Module/Submodule reload
```python
import my_pkg.sub_module as sub
import hmr
sub = hmr.reload(sub)
```
### Function/Class reload
No difference to reloading module
```python
from my_pkg import func, Class
import hmr
func = hmr.reload(func)
Class = hmr.reload(Class)
```
If your have multiple class instance, they will all be updated.
Both `a` and `b` will be updated.
```python
a = Class()
b = Class()
```
### @Decorated Function reload
Use [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps) to preserve
signature of your function, or the function information will be replaced by the decorator itself.
### State handling
If your application is not stateless, it's suggested that you
group all your state variable into the same `.py` file like `state.py`
and exclude that from being reloaded.
> Make sure you know what you are doing.
> This could lead to unexpected behavior and unreproducible bugs.
```python
import my_pkg
import hmr
my_pkg = hmr.reload(my_pkg, excluded=["my_pkg.state"])
```
The `my_pkg/state.py` will not be reloaded, the state will persist.
The same apply when reloading a function or class.
```python
from my_pkg import func
import hmr
func = hmr.reload(func, excluded=["my_pkg.state"])
```
## Acknowledgement
Inspired from the following package.
- [auto-reloader](https://github.com/moisutsu/auto-reloader)
- [reloadr](https://github.com/hoh/reloadr)
+26
-45

@@ -5,4 +5,3 @@ <img src="https://raw.githubusercontent.com/Mr-Milk/python-hmr/d642a1054d5502a020f107bebecba41abeb4c7ea/img/logo.svg" alt="python-hmr logo" align="left" height="50" />

![Test status](https://img.shields.io/github/workflow/status/Mr-Milk/python-hmr/Test?label=Test&logo=github&style=flat-square)
![codecov](https://img.shields.io/codecov/c/github/Mr-Milk/python-hmr?style=flat-square)
![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)

@@ -13,3 +12,3 @@ ![license-mit](https://img.shields.io/github/license/Mr-Milk/python-hmr?color=blue&style=flat-square)

No need to modify your source code.
No need to modify your source code. Works at any environment.

@@ -21,5 +20,3 @@ ![reload](https://github.com/Mr-Milk/python-hmr/blob/main/img/reload_func.gif?raw=true)

- ✅ ```import X```
- ✅ ```import X as Y```
- ✅ ```from X import Y```
- ✅ ```from X import Y as A```

@@ -38,29 +35,23 @@ Supported Types:

## Usage
## Quick Start
Just import your developing package and replace it with `Reloader`.
Import your dev package as usual.
Then you can use it exactly like how you use a module before.
```python
import my_pkg
```
from hmr import Reloader
my_pkg = Reloader(my_pkg)
Add 2 lines to automatically reload your source code.
my_pkg.func()
# >>> "Hi from func"
```
Or you can manually reload it
```python
import my_pkg
```python
my_pkg.reload()
import hmr
my_pkg = hmr.reload(my_pkg)
```
To stop the reloading
Now you are ready to go!
```python
my_pkg.stop()
```
## Usage Manual

@@ -72,4 +63,4 @@ ### Module/Submodule reload

from hmr import Reloader
sub = Reloader(sub)
import hmr
sub = hmr.reload(sub)
```

@@ -84,5 +75,5 @@

from hmr import Reloader
func = Reloader(func)
Class = Reloader(Class)
import hmr
func = hmr.reload(func)
Class = hmr.reload(Class)
```

@@ -105,6 +96,5 @@

If your application contains submodule that handle state,
you can exclude it from reloading. You need to move it to
a new `.py` file like `state.py` and everything from that
file will not be reloaded.
If your application is not stateless, it's suggested that you
group all your state variable into the same `.py` file like `state.py`
and exclude that from being reloaded.

@@ -117,27 +107,18 @@ > Make sure you know what you are doing.

from hmr import Reloader
my_pkg = Reloader(my_pkg, excluded=["my_pkg.state"])
import hmr
my_pkg = hmr.reload(my_pkg, excluded=["my_pkg.state"])
```
This will exclude the `my_pkg/state.py` from reloading.
The `my_pkg/state.py` will not be reloaded, the state will persist.
Even you only want to reload a submodule or a function, you
still need to provide the `excluded` argument.
The same apply when reloading a function or class.
```python
import my_pkg.sub_module as sub
from my_pkg import func
from hmr import Reloader
sub = Reloader(sub, excluded=["my_pkg.state"])
func = Reloader(func, excluded=["my_pkg.state"])
import hmr
func = hmr.reload(func, excluded=["my_pkg.state"])
```
## Implementation
Current implementation is relied on the `importlib.reload`,
which is not very graceful when handling state. Direct reading of
AST may be a better solution for hot module reload in python,
but it's too complicated, I might try it in the future.
## Acknowledgement

@@ -144,0 +125,0 @@

@@ -7,18 +7,2 @@ from pathlib import Path

setup(name="python-hmr",
packages=['hmr'],
description="Hot module reload for python",
long_description=README,
long_description_content_type="text/markdown",
version="0.1.1",
author="Mr-Milk",
url="https://github.com/Mr-Milk/python-hmr",
author_email="zym.zym1220@gmail.com",
license="MIT License",
classifiers=[
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
],
python_requires='>=3.6',
install_requires=['watchdog'],
)
setup(name="python-hmr")
Metadata-Version: 2.1
Name: python-hmr
Version: 0.1.1
Summary: Hot module reload for python
Home-page: https://github.com/Mr-Milk/python-hmr
Author: Mr-Milk
Author-email: zym.zym1220@gmail.com
License: MIT License
Description: <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
![Test status](https://img.shields.io/github/workflow/status/Mr-Milk/python-hmr/Test?label=Test&logo=github&style=flat-square)
![codecov](https://img.shields.io/codecov/c/github/Mr-Milk/python-hmr?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)
Automatic reload your project when files are modified.
No need to modify your source code.
![reload](https://github.com/Mr-Milk/python-hmr/blob/main/img/reload_func.gif?raw=true)
Supported Syntax:
- ✅ ```import X```
- ✅ ```import X as Y```
- ✅ ```from X import Y```
- ✅ ```from X import Y as A```
Supported Types:
- ✅ `Module`
- ✅ `Function`
- ✅ `Class`
## Installation
```shell
pip install python-hmr
```
## Usage
Just import your developing package and replace it with `Reloader`.
Then you can use it exactly like how you use a module before.
```python
import my_pkg
from hmr import Reloader
my_pkg = Reloader(my_pkg)
my_pkg.func()
# >>> "Hi from func"
```
Or you can manually reload it
```python
my_pkg.reload()
```
To stop the reloading
```python
my_pkg.stop()
```
### Module/Submodule reload
```python
import my_pkg.sub_module as sub
from hmr import Reloader
sub = Reloader(sub)
```
### Function/Class reload
No difference to reloading module
```python
from my_pkg import func, Class
from hmr import Reloader
func = Reloader(func)
Class = Reloader(Class)
```
If your have multiple class instance, they will all be updated.
Both `a` and `b` will be updated.
```python
a = Class()
b = Class()
```
### @Decorated Function reload
Use [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps) to preserve
signature of your function, or the function information will be replaced by the decorator itself.
### State handling
If your application contains submodule that handle state,
you can exclude it from reloading. You need to move it to
a new `.py` file like `state.py` and everything from that
file will not be reloaded.
> Make sure you know what you are doing.
> This could lead to unexpected behavior and unreproducible bugs.
```python
import my_pkg
from hmr import Reloader
my_pkg = Reloader(my_pkg, excluded=["my_pkg.state"])
```
This will exclude the `my_pkg/state.py` from reloading.
Even you only want to reload a submodule or a function, you
still need to provide the `excluded` argument.
```python
import my_pkg.sub_module as sub
from my_pkg import func
from hmr import Reloader
sub = Reloader(sub, excluded=["my_pkg.state"])
func = Reloader(func, excluded=["my_pkg.state"])
```
## Implementation
Current implementation is relied on the `importlib.reload`,
which is not very graceful when handling state. Direct reading of
AST may be a better solution for hot module reload in python,
but it's too complicated, I might try it in the future.
## Acknowledgement
Inspired from the following package.
- [auto-reloader](https://github.com/moisutsu/auto-reloader)
- [reloadr](https://github.com/hoh/reloadr)
Platform: UNKNOWN
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.6
Description-Content-Type: text/markdown
LICENSE
README.md
setup.py
hmr/__init__.py
hmr/api.py
hmr/reload.py
python_hmr.egg-info/PKG-INFO
python_hmr.egg-info/SOURCES.txt
python_hmr.egg-info/dependency_links.txt
python_hmr.egg-info/requires.txt
python_hmr.egg-info/top_level.txt
[egg_info]
tag_build =
tag_date = 0