# -*- coding: utf-8 -*-
"""
Resolver
========
Resolver is in charge to resolve path in import rules. Resolving is done using
given source directory and libraries directories paths.
"""
import os
from six import string_types
from boussole.exceptions import UnresolvablePath
from boussole.exceptions import UnclearResolution
[docs]class ImportPathsResolver(object):
"""
Import paths resolver.
Resolve given paths from SCSS source to absolute paths.
It's a mixin, meaning without own ``__init__`` method so it's should be
safe enough to inherit it from another class.
Attributes:
CANDIDATE_EXTENSIONS (list): List of extensions available to build
candidate paths. Beware, order does matter, the first extension
will be the top candidate.
STRICT_PATH_VALIDATION (bool): A switch to enabled (``True``) or
disable (``False``) exception raising when a path can not be
resolved.
"""
CANDIDATE_EXTENSIONS = ['scss', 'sass', 'css', ]
STRICT_PATH_VALIDATION = True
[docs] def candidate_paths(self, filepath):
"""
Return candidates path for given path
* If Filename does not starts with ``_``, will build a candidate for
both with and without ``_`` prefix;
* Will build For each available extensions if filename does not have
an explicit extension;
* Leading path directory is preserved;
Args:
filepath (str): Relative path as finded in an import rule from a
SCSS source.
Returns:
list: Builded candidate paths (as relative paths).
"""
filelead, filetail = os.path.split(filepath)
name, extension = os.path.splitext(filetail)
# Removed leading dot from extension
if extension:
extension = extension[1:]
filenames = [name]
# If underscore prefix is present, dont need to double underscore
if not name.startswith('_'):
filenames.append("_{}".format(name))
# If explicit extension, dont need to add more candidate extensions
if extension and extension in self.CANDIDATE_EXTENSIONS:
filenames = [".".join([k, extension]) for k in filenames]
# Else if no extension or not candidate, add candidate extensions
else:
# Restore uncandidate extensions if any
if extension:
filenames = [".".join([k, extension]) for k in filenames]
new = []
for ext in self.CANDIDATE_EXTENSIONS:
new.extend([".".join([k, ext]) for k in filenames])
filenames = new
# Return candidates with restored leading path if any
return [os.path.join(filelead, v) for v in filenames]
[docs] def check_candidate_exists(self, basepath, candidates):
"""
Check that at least one candidate exist into a directory.
Args:
basepath (str): Directory path where to search for candidate.
candidates (list): List of candidate file paths.
Returns:
list: List of existing candidates.
"""
checked = []
for item in candidates:
abspath = os.path.join(basepath, item)
if os.path.exists(abspath):
checked.append(abspath)
return checked
[docs] def resolve(self, sourcepath, paths, library_paths=None):
"""
Resolve given paths from given base paths
Return resolved path list.
Note:
Resolving strategy is made like libsass do, meaning paths in
import rules are resolved from the source file where the import
rules have been finded.
If import rule is not explicit enough and two file are candidates
for the same rule, it will raises an error. But contrary to
libsass, this happen also for files from given libraries in
``library_paths`` (oposed to libsass just silently taking the
first candidate).
Args:
sourcepath (str): Source file path, its directory is used to
resolve given paths. The path must be an absolute path to
avoid errors on resolving.
paths (list): Relative paths (from ``sourcepath``) to resolve.
library_paths (list): List of directory paths for libraries to
resolve paths if resolving fails on the base source path.
Default to None.
Raises:
UnresolvablePath: If a path does not exist and
``STRICT_PATH_VALIDATION`` attribute is ``True``.
Returns:
list: List of resolved path.
"""
# Split basedir/filename from sourcepath, so the first resolving
# basepath is the sourcepath directory, then the optionnal
# given libraries
basedir, filename = os.path.split(sourcepath)
basepaths = [basedir]
resolved_paths = []
# Add given library paths to the basepaths for resolving
# Accept a string if not allready in basepaths
if library_paths and isinstance(library_paths, string_types) and \
library_paths not in basepaths:
basepaths.append(library_paths)
# Add path item from list if not allready in basepaths
elif library_paths:
for k in list(library_paths):
if k not in basepaths:
basepaths.append(k)
for import_rule in paths:
candidates = self.candidate_paths(import_rule)
# Search all existing candidates:
# * If more than one candidate raise an error;
# * If only one, accept it;
# * If no existing candidate raise an error;
stack = []
for i, basepath in enumerate(basepaths):
checked = self.check_candidate_exists(basepath, candidates)
if checked:
stack.extend(checked)
# More than one existing candidate
if len(stack) > 1:
raise UnclearResolution(
"rule '{}' This is not clear for these paths: {}".format(
import_rule, ', '.join(stack)
)
)
# Accept the single one
elif len(stack) == 1:
resolved_paths.append(os.path.normpath(stack[0]))
# No validated candidate
else:
if self.STRICT_PATH_VALIDATION:
raise UnresolvablePath(
"Imported path '{}' does not exist in '{}'".format(
import_rule, basedir
)
)
return resolved_paths