Skip to content

Commit

Permalink
Merge pull request #33 from sveetch/better_conf_discovering
Browse files Browse the repository at this point in the history
Add settings file discovering.
  • Loading branch information
sveetch authored Sep 30, 2018
2 parents f6456b4 + e0fc424 commit 4e5337d
Show file tree
Hide file tree
Showing 27 changed files with 675 additions and 200 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Features
* **Watch mode** for no waste of time during web design integration;
* **Full Python stack**, no Ruby or Node.js stuff needed;
* **Expose a Core API** to use it from Python code;
* Support for **Python2.7** and **Python3.4**;
* Support for **Python2.7** and **Python>=3.5**;

Links
*****
Expand Down
2 changes: 1 addition & 1 deletion boussole/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""Commandline interface to build Sass projects using libsass-python"""
__version__ = '1.2.3'
__version__ = '1.3.0'
31 changes: 18 additions & 13 deletions boussole/cli/compile.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# -*- coding: utf-8 -*-
import os
import click
import logging
import os

import six

from boussole.exceptions import BoussoleBaseException, SettingsBackendError
from boussole.finder import ScssFinder
from boussole.compiler import SassCompileHelper
from boussole.conf.discovery import Discover
from boussole.conf.json_backend import SettingsBackendJson
from boussole.conf.yaml_backend import SettingsBackendYaml
from boussole.exceptions import BoussoleBaseException
from boussole.finder import ScssFinder
from boussole.project import ProjectBase


Expand All @@ -27,22 +30,24 @@ def compile_command(context, backend, config):
logger = logging.getLogger("boussole")
logger.info(u"Building project")

# Load settings file
# Discover settings file
try:
project = ProjectBase(backend_name=backend, basedir=os.getcwd())

# If not given, config file name is setted from backend default
# filename
if not config:
config = project.backend_engine._default_filename
discovering = Discover(backends=[SettingsBackendJson,
SettingsBackendYaml])
config_filepath, config_engine = discovering.search(
filepath=config,
basedir=os.getcwd(),
kind=backend
)

settings = project.backend_engine.load(filepath=config)
except SettingsBackendError as e:
project = ProjectBase(backend_name=config_engine._kind_name)
settings = project.backend_engine.load(filepath=config_filepath)
except BoussoleBaseException as e:
logger.critical(six.text_type(e))
raise click.Abort()

logger.debug(u"Settings file: {} ({})".format(
config, backend))
config_filepath, config_engine._kind_name))
logger.debug(u"Project sources directory: {}".format(
settings.SOURCES_PATH))
logger.debug(u"Project destination directory: {}".format(
Expand Down
31 changes: 19 additions & 12 deletions boussole/cli/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
from watchdog.observers import Observer
from watchdog.observers.polling import PollingObserver

from boussole.exceptions import SettingsBackendError
from boussole.conf.discovery import Discover
from boussole.conf.json_backend import SettingsBackendJson
from boussole.conf.yaml_backend import SettingsBackendYaml
from boussole.exceptions import BoussoleBaseException
from boussole.inspector import ScssInspector
from boussole.project import ProjectBase
from boussole.watcher import (WatchdogLibraryEventHandler,
WatchdogProjectEventHandler)
from boussole.project import ProjectBase


@click.command('watch', short_help='Watch for change on your Sass project.')
Expand Down Expand Up @@ -48,20 +51,24 @@ def watch_command(context, backend, config, poll):
logger = logging.getLogger("boussole")
logger.info("Watching project")

# Load settings file
# Discover settings file
try:
project = ProjectBase(backend_name=backend, basedir=os.getcwd())

# If not given, config file name is setted from backend default
# filename
if not config:
config = project.backend_engine._default_filename

settings = project.backend_engine.load(filepath=config)
except SettingsBackendError as e:
discovering = Discover(backends=[SettingsBackendJson,
SettingsBackendYaml])
config_filepath, config_engine = discovering.search(
filepath=config,
basedir=os.getcwd(),
kind=backend
)

project = ProjectBase(backend_name=config_engine._kind_name)
settings = project.backend_engine.load(filepath=config_filepath)
except BoussoleBaseException as e:
logger.critical(six.text_type(e))
raise click.Abort()

logger.debug(u"Settings file: {} ({})".format(
config_filepath, config_engine._kind_name))
logger.debug(u"Project sources directory: {}".format(
settings.SOURCES_PATH))
logger.debug(u"Project destination directory: {}".format(
Expand Down
59 changes: 0 additions & 59 deletions boussole/conf/discover.py

This file was deleted.

191 changes: 191 additions & 0 deletions boussole/conf/discovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
"""
Backend discover
================
"""
import os
from collections import OrderedDict

from boussole.exceptions import SettingsDiscoveryError


class Discover:
"""
Should be able to find a settings file without any specific backend given,
just a directory path (the base dir) is required.
So:
* If a file name is explicitely given, use it to find backend;
* If no file name is given but a backend is, use its default file name;
* If file name nor backend is given, start full discover process:
* Get all backend default settings file name;
* Search for any of these available settings file names;
* If no available settings file name if finded, discovering fail;
* If one file name is given assume backend from file name;
backends (list): List of backend engines to get available backend engines.
"""

def __init__(self, backends=[]):
self.backends = backends

indexes = self.scan_backends(self.backends)
self.engines, self.filenames, self.extensions = indexes

def scan_backends(self, backends):
"""
From given backends create and return engine, filename and extension
indexes.
Arguments:
backends (list): List of backend engines to scan. Order does matter
since resulted indexes are stored in an ``OrderedDict``. So
discovering will stop its job if it meets the first item.
Returns:
tuple: Engine, filename and extension indexes where:
* Engines are indexed on their kind name with their backend object
as value;
* Filenames are indexed on their filename with engine kind name as
value;
* Extensions are indexed on their extension with engine kind name
as value;
"""
engines = OrderedDict()
filenames = OrderedDict()
extensions = OrderedDict()

for item in backends:
engines[item._kind_name] = item
filenames[item._default_filename] = item._kind_name
extensions[item._file_extension] = item._kind_name

return engines, filenames, extensions

def get_engine(self, filepath, kind=None):
"""
From given filepath try to discover which backend format to use.
Discovering is pretty naive as it find format from file extension.
Args:
filepath (str): Settings filepath or filename.
Keyword Arguments:
kind (str): A format name to enforce a specific backend. Can be any
value from attribute ``_kind_name`` of available backend
engines.
Raises:
boussole.exceptions.SettingsDiscoveryError: If extension is
unknowed or if given format name is unknowed.
Returns:
object: Backend engine class.
"""
if not kind:
extension = os.path.splitext(filepath)[1]
if not extension:
msg = ("Unable to discover settings format from an empty file "
"extension: {}")
raise SettingsDiscoveryError(msg.format(filepath))
elif extension[1:] not in self.extensions:
msg = ("Settings file extension is unknowed from available "
"backends: {}")
raise SettingsDiscoveryError(msg.format(filepath))
kind = self.extensions[extension[1:]]
elif kind not in self.engines:
msg = "Given settings format is unknow: {}"
raise SettingsDiscoveryError(msg.format(kind))

return self.engines[kind]

def guess_filename(self, basedir, kind=None):
"""
Try to find existing settings filename from base directory using
default filename from available engines.
First finded filename from available engines win. So registred engines
order matter.
Arguments:
basedir (string): Directory path where to search for.
Keyword Arguments:
kind (string): Backend engine kind name to search for default
settings filename. If not given, search will be made for
default settings filename from all available backend engines.
Returns:
tuple: Absolute filepath and backend engine class.
"""
if kind:
filepath = os.path.join(basedir,
self.engines[kind]._default_filename)
if os.path.exists(filepath):
return filepath, self.engines[kind]

for filename, kind in self.filenames.items():
filepath = os.path.join(basedir, filename)
if os.path.exists(filepath):
return filepath, self.engines[kind]

msg = "Unable to find any settings in directory: {}"
raise SettingsDiscoveryError(msg.format(basedir))

def search(self, filepath=None, basedir=None, kind=None):
"""
Search for a settings file.
Keyword Arguments:
filepath (string): Path to a config file, either absolute or
relative. If absolute set its directory as basedir (omitting
given basedir argument). If relative join it to basedir.
basedir (string): Directory path where to search for.
kind (string): Backend engine kind name (value of attribute
``_kind_name``) to help discovering with empty or relative
filepath. Also if explicit absolute filepath is given, this
will enforce the backend engine (such as yaml kind will be
forced for a ``foo.json`` file).
Returns:
tuple: Absolute filepath and backend engine class.
"""
# None values would cause trouble with path joining
if filepath is None:
filepath = ''
if basedir is None:
basedir = '.'

if not basedir and not filepath:
msg = "Either basedir or filepath is required for discovering"
raise SettingsDiscoveryError(msg)

if kind and kind not in self.engines:
msg = "Given settings format is unknow: {}"
raise SettingsDiscoveryError(msg.format(kind))

# Implicit filename to find from backend
if not filepath:
filename, engine = self.guess_filename(basedir, kind)
filepath = os.path.join(basedir, filename)
# Explicit filename dont have to search for default backend file and
# blindly force given backend if any
else:
if os.path.isabs(filepath):
basedir, filename = os.path.split(filepath)
else:
filepath = os.path.join(basedir, filepath)

if not os.path.exists(filepath):
msg = "Given settings file does not exists: {}"
raise SettingsDiscoveryError(msg.format(filepath))

engine = self.get_engine(filepath, kind)

return filepath, engine
4 changes: 2 additions & 2 deletions boussole/conf/post_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
Post processing methods are used to modify or validate given settings items.
Backends inherit from ``SettingsPostProcessor`` to be able to use it in their
``clean()`` method.
Base backend inherit from ``SettingsPostProcessor`` to be able to use it in
its ``clean()`` method.
"""
import os

Expand Down
8 changes: 8 additions & 0 deletions boussole/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ class CircularImport(BoussoleBaseException):
pass


class SettingsDiscoveryError(BoussoleBaseException):
"""
Exception to be raised when config discovery has failed to find settings
file.
"""
pass


class SettingsBackendError(BoussoleBaseException):
"""
Exception to be raised when config loading has failed from a backend.
Expand Down
Loading

0 comments on commit 4e5337d

Please sign in to comment.