Skip to content

Commit

Permalink
Add option for modules outside astrality.yaml
Browse files Browse the repository at this point in the history
  • Loading branch information
JakobGM committed Feb 22, 2018
1 parent 8473ed1 commit a9abdb3
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 18 deletions.
189 changes: 187 additions & 2 deletions astrality/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@
from distutils.dir_util import copy_tree
from io import StringIO
from pathlib import Path
from typing import Any, Dict, Match, MutableMapping, Optional, Tuple
from typing import (
Any,
Dict,
List,
Match,
MutableMapping,
Optional,
Tuple,
Iterable,
)

from mypy_extensions import TypedDict

from astrality import compiler
from astrality.resolver import Resolver
Expand Down Expand Up @@ -92,7 +103,7 @@ def infer_config_location(

def dict_from_config_file(
config_file: Path,
with_env: Optional[bool] = True,
with_env: bool = True,
) -> ApplicationConfig:
"""
Return a dictionary that reflects the contents of `config_file`.
Expand Down Expand Up @@ -331,3 +342,177 @@ def create_config_directory(path: Optional[Path]=None, empty=False) -> Path:
logger.warning(f'Path "{str(path)}" already exists! Delete it first.')

return path

def expand_path(path: Path, config_directory: Path) -> Path:
"""
Return an absolute path from a (possibly) relative path.
Relative paths are relative to $ASTRALITY_CONFIG_HOME, and ~ is
expanded to the home directory of $USER.
"""

path = Path.expanduser(path)

if not path.is_absolute():
path = Path(
config_directory,
path,
)

return path


class ExternalModuleDictRequired(TypedDict):
name: str


class ExternalModuleDict(ExternalModuleDictRequired, total=False):
"""Dictionary defining an externally defined module."""
safe: bool


ModuleConfig = Dict[str, Any]


class ExternalModuleSource:
"""
Module defined outside of `$ASTRALITY_CONFIG_HOME/astrality.yaml.
The module is represented by a directory, and possibly in the future by
an url.
"""
# The name of the module
name: str

# The directory containing all the files which represent the module,
# by default $ASTRALITY_CONFIG_HOME/modules/{name}
directory: Path

# Path to the YAML configuration of the module:
# Default: {directory}/modules.yaml
config_file: Path

# If the module is considered safe (i.e. allows shell execution)
safe: bool

def __init__(
self,
config: ExternalModuleDict,
config_directory: Path,
modules_directory_path: Path,
) -> None:
"""
Initialize an ExternalModule object.
"""
self.name = config['name']
self.directory = expand_path(
path=modules_directory_path / Path(self.name),
config_directory=config_directory,
)
self.config_file = self.directory / 'modules.yaml'
self.safe = config.get('safe', False)

def module_config_dict(self) -> ModuleConfig:
"""Return the contents of `modules.yaml` as a dictionary."""
modules_dict = dict_from_config_file(
config_file=self.config_file,
with_env=False,
)

# We rename each module to module/module_name[path_to_module_directory]
# in order to prevent naming conflicts when using modules provided
# from a third party with the same name as another managed module.
# This way you can use a module named "conky" from two third parties,
# in addition to providing your own.
for section in tuple(modules_dict.keys()):
if len(section) > 7 and section[:7].lower() == 'module/':
non_conflicting_module_name = str(section) + f'[{self.directory}]'
module_section = modules_dict.pop(section)
modules_dict[non_conflicting_module_name] = module_section

return modules_dict

def __repr__(self):
"""Human-readable representation of a ExternalModuleSource object."""
return f'ExternalModuleSource(name={self.name}, directory={self.directory}, safe={self.safe})'

def __eq__(self, other) -> bool:
"""
Return true if two ExternalModuleSource objects represents the same Module.
Entirily determined by the source directory of the module.
"""
try:
return self.directory == other.directory
except:
return False


class GlobalModulesConfigDict(TypedDict, total=False):
"""Dictionary defining configuration options for Modules."""
modules_directory_path: str
enabled: List[ExternalModuleDict]


class GlobalModulesConfig:
"""User modules configuration."""

# The path to the modules directory, by default:
# $ASTRALITY_CONFIG_HOME/modules
modules_directory_path: Path

# All extenally managed modules
external_module_sources: Iterable[ExternalModuleSource]

# The absolute path to all module config files of name `modules.yaml`
external_module_config_files: Iterable[Path]

def __init__(
self,
config: GlobalModulesConfigDict,
config_directory: Path,
) -> None:
"""Initialize a GlobalModulesConfig object from a dictionary."""

# Determine the directory which contains external modules
if 'modules_directory_path' in config:
# Custom modules folder
modules_path = expand_path(
path=Path(config['modules_directory_path']),
config_directory=config_directory,
)
self.modules_directory_path = modules_path
else:
# Default modules folder: $ASTRALITY_CONFIG_HOME/modules
self.modules_directory_path = config_directory / 'modules'

# Initialize all the external module sources
self._external_module_sources: Dict[Path, ExternalModuleSource] = {}
for external_module_dict in config.get('enabled', []):
external_module_source = ExternalModuleSource(
config=external_module_dict,
modules_directory_path=self.modules_directory_path,
config_directory=config_directory,
)
self._external_module_sources[external_module_source.directory] = \
external_module_source

@property
def external_module_sources(self) -> Iterable[ExternalModuleSource]:
"""Return an iterator yielding all ExternalModuleSource objects."""
for external_module_source in self._external_module_sources.values():
yield external_module_source

@property
def external_module_config_files(self) -> Iterable[Path]:
"""Return an iterator yielding all absolute paths to module config files."""
for external_module_source in self._external_module_sources.values():
yield external_module_source.config_file

def module_configs_dict(self) -> ModuleConfig:
"""Return a merged dictionary of all the externally managed module configs."""
module_config_dict = {}
for external_module_source in self._external_module_sources.values():
module_config_dict.update(external_module_source.module_config_dict())

return module_config_dict
44 changes: 29 additions & 15 deletions astrality/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@

from astrality import compiler
from astrality.compiler import context
from astrality.config import ApplicationConfig, insert_into, user_configuration
from astrality.filewatcher import DirectoryWatcher
from astrality.config import (
ApplicationConfig,
GlobalModulesConfig,
expand_path,
insert_into,
user_configuration,
)
from astrality.event_listener import EventListener, event_listener_factory
from astrality.filewatcher import DirectoryWatcher
from astrality.utils import run_shell


Expand Down Expand Up @@ -68,7 +74,7 @@ def __init__(self, module_config: ModuleConfig) -> None:
assert len(module_config) == 1

section = next(iter(module_config.keys()))
self.name: str = section.split('/')[1]
self.name: str = section[7:]

self.module_config = module_config[section]
self.populate_event_blocks()
Expand Down Expand Up @@ -287,7 +293,7 @@ def valid_class_section(

try:
module_name = next(iter(section.keys()))
valid_module_name = module_name.split('/')[0] == 'module' # type: ignore
valid_module_name = module_name.split('/')[0].lower() == 'module' # type: ignore
enabled = section[module_name].get('enabled', True)
if not (valid_module_name and enabled):
return False
Expand Down Expand Up @@ -319,10 +325,24 @@ class ModuleManager:
"""A manager for operating on a set of modules."""

def __init__(self, config: ApplicationConfig) -> None:
self.application_config = config
"""Initialize a ModuleManager object from `astrality.yaml` dict."""

self.config_directory = Path(config['_runtime']['config_directory'])
self.temp_directory = Path(config['_runtime']['temp_directory'])

# Get module configurations which are externally defined
global_modules_config = GlobalModulesConfig( # type: ignore
config=config.get('config/modules', {}),
config_directory=self.config_directory,
).module_configs_dict()

# Variables in `astrality.yaml` should have preference if naming conflicts
# occur.
global_modules_config.update(config)
config = global_modules_config

self.application_config = config

self.application_context = context(config)
self.modules: Dict[str, Module] = {}
self.startup_done = False
Expand Down Expand Up @@ -782,16 +802,10 @@ def expand_path(self, path: Path) -> Path:
Relative paths are relative to $ASTRALITY_CONFIG_HOME, and ~ is
expanded to the home directory of $USER.
"""

path = Path.expanduser(path)

if not path.is_absolute():
path = Path(
self.config_directory,
path,
)

return path
return expand_path(
path=path,
config_directory=self.config_directory,
)

def interpolate_string(self, string: str) -> str:
"""Replace all template placeholders with the compilation path."""
Expand Down

0 comments on commit a9abdb3

Please sign in to comment.