Skip to content

Commit

Permalink
Merge pull request #29 from T4rk1n/10-reloader
Browse files Browse the repository at this point in the history
Hot reload
  • Loading branch information
T4rk1n committed Nov 3, 2019
2 parents 3eb6f80 + 850efbb commit 28f8320
Show file tree
Hide file tree
Showing 35 changed files with 903 additions and 282 deletions.
5 changes: 5 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,10 @@
"ignore": [-1, 0, 1, 2, 3, 100, 10, 0.5]
}],
"no-underscore-dangle": ["off"]
},
"settings": {
"react": {
"version": "detect"
}
}
}
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ disable=print-statement,
logging-fstring-interpolation,
too-many-ancestors,
too-many-arguments,
line-too-long
line-too-long,
duplicate-code

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

Versions follow [Semantic Versioning](https://www.semver.org)

## Unreleased
## [0.2.0]
### Added

- :hot_pepper: Add hot reload. [#29](https://github.com/T4rk1n/dazzler/pull/29)
- :rice: Add PopUp extra component. [#14](https://github.com/T4rk1n/dazzler/issues/14)
- :hammer: Connect websocket only if page has bindings. [#7](https://github.com/T4rk1n/dazzler/issues/7)
- :hammer: Generate react-docs metadata from dazzler command. [#4](https://github.com/T4rk1n/dazzler/issues/4)
Expand Down
5 changes: 5 additions & 0 deletions dazzler/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Run cli so you can call `python -m dazzler` to ensure virtual env
# with sys.executable when running with reloading on.
from ._dazzler import cli

cli()
25 changes: 25 additions & 0 deletions dazzler/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,30 @@ class Authentication(Nestable):

authentication: Authentication

class Development(Nestable):
reload = ConfigProperty(
default=False,
config_type=bool,
auto_global=True,
global_name='reload',
comment='Enable hot reload when files used by the application'
' are changed.'
)

reload_interval = ConfigProperty(
default=0.5,
config_type=float,
comment='Interval at which the reload checks for file changes.'
)

reload_threshold = ConfigProperty(
default=3.0,
config_type=float,
comment='Time to wait from first detected change '
'to actual reload.'
)

development: Development

def __init__(self):
super().__init__(root_name='dazzler', config_format=ConfigFormat.TOML)
151 changes: 130 additions & 21 deletions dazzler/_dazzler.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@
from ._server import Server
from ._version import __version__
from .errors import (
PageConflictError, ServerStartedError, SessionError, AuthError
PageConflictError, ServerStartedError, SessionError, AuthError,
NoInstanceFoundError
)
from ._assets import assets_path
from ._reloader import start_reloader


class Dazzler(precept.Precept):
class Dazzler(precept.Precept): # pylint: disable=too-many-instance-attributes
"""
Dazzler web application & cli.
Expand All @@ -57,12 +59,18 @@ class Dazzler(precept.Precept):
help='Path to the application',
type=str
),
precept.Argument(
'--reloaded',
action='store_true'
),
]
server: Server
auth: DazzlerAuth

def __init__(self, module_name, app_name=None):
module = importlib.import_module(module_name)
self.module_file = module.__file__
self.module_name = module_name
self.root_path = os.path.dirname(module.__file__)
self.app_name = app_name or \
os.path.basename(module.__file__).rstrip('.py')
Expand Down Expand Up @@ -104,38 +112,69 @@ def add_page(self, *pages: Page):
self.pages[page.name] = page

async def stop(self):
await self.server.site.stop()
if self.server.site:
await self.server.site.stop()
self.stop_event.set()

# pylint: disable=arguments-differ
async def main(
self,
application=None,
blocking=True,
debug=False,
reload=False,
reloaded=False,
start_event=None,
**kwargs
):
if application:
# When running from the command line need to insert the path.
# Otherwise it will never find it.
sys.path.insert(0, '.')
mod_path, app_name = application.split(':')
module = importlib.import_module(mod_path)
app: Dazzler = getattr(module, app_name)
module = importlib.import_module(application)

app: typing.Optional[Dazzler] = None

# Search for instance so you don't have to supply a name
# with weird syntax.
for instance in vars(module).values():
if isinstance(instance, Dazzler):
app: Dazzler = instance

if app is None:
raise NoInstanceFoundError(
f'No dazzler application found in {application}'
)

# Set cli for arguments
app.cli = self.cli
await app._on_parse(self._args)
else:
app = self

await app.setup_server(debug=debug or app.config.debug)
reloader = None

await app.server.start(
app.config.host, app.config.port,
)
if reload:
app.config.development.reload = True
reloader = self.loop.create_task(
start_reloader(app, reloaded, start_event)
)

if not reload or (reload and reloaded):
if debug:
app.config.debug = debug
await app.setup_server(debug=app.config.debug)

await app.server.start(
app.config.host, app.config.port,
)

# Test needs it to run without loop
# Otherwise the server closes when the event loop ends.
if blocking:
if blocking and not reloader:
await self._loop()
elif blocking and reloader:
await reloader

async def _loop(self):
while not self.stop_event.is_set():
Expand Down Expand Up @@ -224,6 +263,41 @@ async def application(self):
await self.setup_server()
return self.server.app

def collect_requirements(self) -> typing.Set[Requirement]:
requirements = [
package.requirements for package
in Package.package_registry.values()
]
requirements += [self.requirements]
requirements += [
page.requirements for page in self.pages.values()
]

return set(itertools.chain(*requirements))

def requirement_from_file(self, file) -> typing.Optional[Requirement]:
for requirement in self.collect_requirements():
if any(
file in r for r in (requirement.internal, requirement.dev)
if r
):
return requirement
return None

def _remove_requirement(self, requirement: Requirement):
# Remove from page requirement most probably
for page in self.pages.values():
if requirement in page.requirements:
page.requirements.remove(requirement)
return
if requirement in self.requirements:
self.requirements.remove(requirement)
return
for package in Package.package_registry.values():
if requirement in package.requirements:
package.requirements.remove(requirement)
return

# Commands

@precept.Command(
Expand Down Expand Up @@ -261,16 +335,7 @@ async def copy_requirements(self, packages=tuple()):

futures = []

requirements = [
package.requirements for package
in Package.package_registry.values()
]
requirements += [self.requirements]
requirements += [
page.requirements for page in self.pages.values()
]

for requirement in itertools.chain(*requirements):
for requirement in self.collect_requirements():
if not requirement.internal:
continue
destination = os.path.join(
Expand Down Expand Up @@ -313,10 +378,54 @@ async def copy_requirements(self, packages=tuple()):
)
await asyncio.gather(*futures)

# Handlers

async def on_file_change(
self,
filenames: typing.List[str],
deleted: typing.List[str]
):
hot = False # Restart the server & Reload the page api/layout.
refresh = False # Refresh the page because the root bundles changed.
files = set()
deleted_files = set()
for filename in filenames:
if filename.endswith('.py'):
hot = True
elif filename.endswith('.js') or filename.endswith('.css'):
if 'dazzler_renderer' in filename:
refresh = True
requirement = self.requirement_from_file(filename)
if requirement:
files.add(requirement)
else:
requirement = Requirement(internal=filename)
files.add(requirement)
self.requirements.append(requirement)

for removed in deleted:
requirement = self.requirement_from_file(removed)
if removed.endswith('.js'):
refresh = True
if requirement:
self._remove_requirement(requirement)
deleted_files.add(requirement)

if not hot:
await self.copy_requirements()
await self.server.send_reload(
[r.prepare(dev=self.config.debug) for r in files],
hot,
refresh,
[r.prepare(dev=self.config.debug) for r in deleted_files]
)
return hot

async def _on_startup(self, _):
await self.events.dispatch('dazzler_start', application=self)

async def _on_shutdown(self, _):
self.stop_event.set()
await self.events.dispatch('dazzler_stop', application=self)

async def _enable_auth(self):
Expand Down

0 comments on commit 28f8320

Please sign in to comment.