python-hmr
Advanced tools
| 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 |
+143
| .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) |
+48
-20
| 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 @@ |
+1
-1
| 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 | ||
|  | ||
|  | ||
|  | ||
|  | ||
| Automatic reload your project when files are modified. | ||
| No need to modify your source code. | ||
|  | ||
| 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 | ||
|  | ||
|  | ||
|  | ||
| Automatic reload your project when files are modified. | ||
| No need to modify your source code. Works at any environment. | ||
|  | ||
| 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" /> | ||
|  | ||
|  | ||
|  | ||
|  | ||
@@ -13,3 +12,3 @@  | ||
| No need to modify your source code. | ||
| No need to modify your source code. Works at any environment. | ||
@@ -21,5 +20,3 @@  | ||
| - ✅ ```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 @@ |
+1
-17
@@ -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 | ||
|  | ||
|  | ||
|  | ||
|  | ||
| Automatic reload your project when files are modified. | ||
| No need to modify your source code. | ||
|  | ||
| 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 |
| watchdog |
| 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 | ||
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
714296
3352.37%25
92.31%399
155.77%