Skip to content

Commit

Permalink
Add hot_reload option for astrality.yaml
Browse files Browse the repository at this point in the history
  • Loading branch information
JakobGM committed Feb 16, 2018
1 parent 41b675d commit c6e0130
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 14 deletions.
4 changes: 1 addition & 3 deletions astrality/astrality.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,12 @@ def exit_handler(signal=None, frame=None) -> None:
config = user_configuration()

# Delay further actions if configuration says so
startup_delay = float(config.get('settings/general', {}).get('startup_delay', '0'))
startup_delay = float(config.get('settings/astrality', {}).get('startup_delay', 0))
time.sleep(startup_delay)

module_manager = ModuleManager(config)
module_manager.finish_tasks()

# TODO: Implement startup delay config option

while True:
if module_manager.has_unfinished_tasks():
# TODO: Log which new period which has been detected
Expand Down
11 changes: 7 additions & 4 deletions astrality/config/astrality.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
settings/general:
# You can delay astrality on startup if other tasks need to finish first,
# for example if you are automatically rearranging displays, which might
# interfere with conky. The delay is given in seconds.
settings/astrality:
# If hot_reload is enabled, modifications to this file automatically runs:
# 1) exit actions from the old configuration
# 2) startup actions from the new configuration
hot_reload: false

# You can delay astrality on startup. The delay is given in seconds.
startup_delay: 0


Expand Down
9 changes: 7 additions & 2 deletions astrality/filewatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,13 @@ def start(self) -> None:
def stop(self) -> None:
"""Stop watching the directory."""
if self.observer.is_alive():
self.observer.stop()
self.observer.join()
try:
self.observer.stop()
self.observer.join()
except RuntimeError:
# TODO: Understand exactly what join() does, and why
# it sometimes throws a RuntimeError
pass


class DirectoryEventHandler(FileSystemEventHandler):
Expand Down
27 changes: 26 additions & 1 deletion astrality/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from astrality import compiler
from astrality.compiler import context
from astrality.config import ApplicationConfig, insert_into
from astrality.config import ApplicationConfig, insert_into, user_configuration
from astrality.filewatcher import DirectoryWatcher
from astrality.timer import Timer, timer_factory
from astrality.utils import run_shell
Expand Down Expand Up @@ -558,7 +558,32 @@ def modified(self, modified: Path):
Run any context imports, compilations, and shell commands specified
within the on_modified event block of each module.
Also, if hot_reload is True, we reinstantiate the ModuleManager object
if the application configuration has been modified.
"""
if modified == self.application_config['_runtime']['config_directory'] / 'astrality.yaml':
# The application configuration file has been modified

if not self.application_config.get('settings/astrality', {}).get('hot_reload', False):
# Hot reloading is not enabled, so we return early
return

# Hot reloading is enabled, get the new configuration dict
new_application_config = user_configuration(
config_directory=modified.parent,
)

# Run all exit actions
self.exit()

# Reinstantiate this object
self = ModuleManager(new_application_config)

# Run startup commands
self.finish_tasks()
return

if not modified in self.managed_templates:
# The modified file is not specified in any of the modules
return
Expand Down
16 changes: 16 additions & 0 deletions astrality/tests/test_config/astrality1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
settings/astrality:
hot_reload: true

module/A:
templates:
template1:
source: templates/no_context.template
target: /tmp/astrality/target1

on_startup:
compile:
- template1

on_exit:
run:
- rm {template1}
16 changes: 16 additions & 0 deletions astrality/tests/test_config/astrality2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
settings/astrality:
hot_reload: true

module/A:
templates:
template1:
source: templates/no_context.template
target: /tmp/astrality/target2

on_startup:
compile:
- template1

on_exit:
run:
- rm {template1}
3 changes: 3 additions & 0 deletions astrality/tests/test_config/templates/no_context.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
one
two
three
65 changes: 64 additions & 1 deletion astrality/tests/test_module.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import shutil
import time
from datetime import datetime, timedelta
from pathlib import Path
Expand All @@ -8,7 +9,7 @@
import pytest

from astrality import timer
from astrality.config import generate_expanded_env_dict
from astrality.config import dict_from_config_file, generate_expanded_env_dict
from astrality.module import ContextSectionImport, Module, ModuleManager
from astrality.resolver import Resolver

Expand Down Expand Up @@ -948,6 +949,68 @@ def test_on_modified_event_in_module(self, modules_config):
# And that the new file has been touched
assert touch_target.is_file()

@pytest.yield_fixture
def test_template_targets(self):
template_target1 = Path('/tmp/astrality/target1')
template_target2 = Path('/tmp/astrality/target2')

yield template_target1, template_target2

if template_target1.is_file():
os.remove(template_target1)
if template_target2.is_file():
os.remove(template_target2)

@pytest.mark.slow
def test_hot_reloading(self, test_template_targets):
template_target1, template_target2 = test_template_targets
config_dir = Path(__file__).parent / 'test_config'
config1 = config_dir / 'astrality1.yaml'
config2 = config_dir / 'astrality2.yaml'
target_config = config_dir / 'astrality.yaml'
temp_directory = Path('/tmp/astrality')

# Copy the first configuration into place
shutil.copy(str(config1), str(target_config))

application_config1 = dict_from_config_file(config1)
application_config1['_runtime'] = {
'config_directory': config_dir,
'temp_directory': temp_directory,
}

module_manager = ModuleManager(application_config1)

# Before beginning, the template should not be compiled
assert not template_target1.is_file()

# But when we finalize tasks, it should be compiled
module_manager.finish_tasks()
assert template_target1.is_file()

# Also check that the filewatcher has been started
assert module_manager.directory_watcher.observer.is_alive()

# We now "edit" the configuration file
shutil.copy(str(config2), str(target_config))
time.sleep(0.7)

# Since hot reloading is enabled, the new template target should be
# compiled, and the old one cleaned up
assert template_target2.is_file()
assert not template_target1.is_file()

# And we switch back again
shutil.copy(str(config1), str(target_config))
time.sleep(0.7)
assert template_target1.is_file()
assert not template_target2.is_file()

# Cleanup config file
if target_config.is_file():
os.remove(target_config)



@pytest.yield_fixture
def two_test_file_paths():
Expand Down
34 changes: 32 additions & 2 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,43 @@ Astrality makes two non-standard additions to the ``YAML`` syntax, so-called int

Interpolations in ``astrality.yaml`` occur on Astrality startup, and will not reflect changes to environment variables and shell commands after startup.

Astrality configuration options
===============================

Global Astrality configuration options are specified in ``astrality.yaml`` within a dictionary named ``settings/astrality``, i.e.:

.. code-block:: yaml
# Source file: $ASTRALITY_CONFIG_HOME/astrality.yaml
settings/astrality:
option1: value1
option2: value2
...
**Avalable configuration options**:

``hot_reload:``
*Default:* ``false``

If enabled, Astrality will watch for modifications to ``astrality.yaml``.

When ``astrality.yaml`` is modified, Astrality will perform all :ref:`exit actions <module_events_on_exit>` in the old configuration, and then all :ref:`startup actions <module_events_on_startup>` from the new configuration.

Useful for quick feedback when editing :ref:`templates <templating>`.

``startup_delay:``
*Default:* ``0``

Delay Astrality on startup. The delay is given in seconds.

Where to go from here
=====================

What you should read of the documentation from here on depends on what you intend to solve by using Astrality. The most central concepts are:

* :doc:`modules` define which templates to compile, when to compile them, and which commands to run after they have been compiled.
* :doc:`timers` define when modules should change their behaviour.
* :doc:`templating` explains how to write configuration file templates.
* :doc:`modules` specify which templates to compile, when to compile them, and which commands to run after they have been compiled.
* :doc:`timers` define when modules should change their behaviour.

These concepts are relatively interdependent, and each documentation section assumes knowledge of concepts explained in earlier sections. If this is the first time you are reading this documentation, you should probably just continue reading the documentation in chronological order.
4 changes: 4 additions & 0 deletions docs/modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,14 @@ Events

When you want to assign :ref:`tasks <actions>` for Astrality to perform, you have to define *when* to perform them. This is done by defining those ``actions`` in one of four available ``event`` blocks.

.. _module_events_on_startup:

``on_startup``:
Tasks to be performed when Astrality first starts up.
Useful for compiling templates that don't need to change after they have been compiled.

.. _module_events_on_exit:

``on_exit``:
Tasks to be performed when you kill the Astrality process.
Useful for cleaning up any unwanted clutter.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def readme():

setup(
name='astrality',
version='0.3',
version='0.4',
packages=find_packages(),
install_requires=[
'astral',
Expand Down

0 comments on commit c6e0130

Please sign in to comment.