Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reloadr performance improvements #5

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 28 additions & 30 deletions reloadr.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
"""Reloadr - Python library for hot code reloading
(c) 2015-2017 Hugo Herter
(c) 2015-2019 Hugo Herter
"""

from os.path import dirname, abspath
import inspect
import logging
import redbaron
from baron.parser import ParsingError
import threading
import types
from time import sleep
import weakref

from baron.parser import ParsingError
from os.path import dirname, abspath
from time import sleep
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

__author__ = "Hugo Herter"
__version__ = '0.3.3'
__version__ = '0.3.4'


def get_new_source(target, kind, filepath=None):
Expand Down Expand Up @@ -43,53 +42,50 @@ def reload_target(target, kind, filepath=None):
assert kind in ('class', 'def')

source = get_new_source(target, kind, filepath)
source = source.replace('@{}'.format(autoreload.__name__), '') # Remove decorator
module = inspect.getmodule(target)
# We will populate these locals using exec()
locals_ = {}
# module.__dict__ is the namespace of the module
exec(source, module.__dict__, locals_)
# The result is expected to be decorated with @reloadr, so we return
# ._target, which corresponds to the class itself and not the Reloadr class
return locals_[target.__name__]._target
return locals_[target.__name__]


def reload_class(target):
"Get the new class object corresponding to the target class."
"""Get the new class object corresponding to the target class."""
return reload_target(target, 'class')


def reload_function(target, filepath: str):
"Get the new function object corresponding to the target function."
"""Get the new function object corresponding to the target function."""
return reload_target(target, 'def', filepath)


class GenericReloadr:

def _timer_reload(self, interval=1):
"Reload the target every `interval` seconds."
"""Reload the target every `interval` seconds."""
while True:
self._reload()
sleep(interval)

def _start_timer_reload(self, interval=1):
"Start a thread that reloads the target every `interval` seconds."
"""Start a thread that reloads the target every `interval` seconds."""
thread = threading.Thread(target=self._timer_reload)
thread.start()

def _start_watch_reload(self):
"Reload the target based on file changes in the directory"
"""Reload the target based on file changes in the directory"""
observer = Observer()
filepath = inspect.getsourcefile(self._target)
filedir = dirname(abspath(filepath))

this = self

class EventHandler(FileSystemEventHandler):
def on_modified(self, event):
this._reload()
if not event.is_directory and event.src_path == filepath:
this._reload()

# Sadly, watchdog only operates on directories and not on a file
# level, so any change within the directory will trigger a reload.
observer.schedule(EventHandler(), filedir, recursive=False)
observer.start()

Expand All @@ -102,27 +98,28 @@ def __init__(self, target):
self._instances = [] # For classes, keep a reference to all instances

def __call__(self, *args, **kwargs):
"Override instantiation in order to register a reference to the instance"
"""Override instantiation in order to register a reference to the instance"""
instance = self._target.__call__(*args, **kwargs)
# Register a reference to the instance
self._instances.append(weakref.ref(instance))
return instance

def __getattr__(self, name):
"Proxy inspection to the target"
"""Proxy inspection to the target"""
return self._target.__getattr__(name)

def _reload(self):
"Manually reload the class with its new code."
"""Manually reload the class with its new code."""
try:
self._target = reload_class(self._target)
# Replace the class reference of all instances with the new class
for ref in self._instances:
instance = ref() # We keep weak references to objects
if instance:
instance.__class__ = self._target
logging.info('Reloaded {}'.format(self._target.__name__))
except ParsingError as error:
print('ParsingError', error)
logging.error('Parsing error: {}'.format(error))


class FuncReloadr(GenericReloadr):
Expand All @@ -133,31 +130,32 @@ def __init__(self, target):
self._filepath = inspect.getsourcefile(target)

def __call__(self, *args, **kwargs):
"Proxy function call to the target"
"""Proxy function call to the target"""
return self._target.__call__(*args, **kwargs)

def __getattr__(self, name):
"Proxy inspection to the target"
"""Proxy inspection to the target"""
return self._target.__getattr__(name)

def _reload(self):
"Manually reload the function with its new code."
"""Manually reload the function with its new code."""
try:
self._target = reload_function(self._target, self._filepath)
logging.info('Reloaded {}'.format(self._target.__name__))
except ParsingError as error:
print('ParsingError', error)
logging.error('Parsing error: {}'.format(error))


def reloadr(target):
"Main decorator, forwards the target to the appropriate class."
"""Main decorator, forwards the target to the appropriate class."""
if isinstance(target, types.FunctionType):
return FuncReloadr(target)
else:
return ClassReloadr(target)


def autoreload(target):
"Decorator that immediately starts watching the source file in a thread."
def autoreload(target=None):
"""Decorator that immediately starts watching the source file in a thread."""
result = reloadr(target)
result._start_watch_reload()
return result