# -*- coding: utf-8 -*-
"""
Inspector
=========
Inspector is in charge to inspect a project about Sass stylesheets to search
for their dependencies.
"""
import io
import os
from collections import defaultdict
from .exceptions import CircularImport
from .parser import ScssImportsParser, SassImportsParser
from .resolver import ImportPathsResolver
[docs]class ScssInspector(ImportPathsResolver, ScssImportsParser):
"""
Project inspector for SCSS sources
Inspector is stateful, meaning you will need to invoke ``reset()`` then
``inspect()`` each time a project change, else the parents and children
maps will be eventually incorrects.
``__init__`` method use ``reset`` method to initialize some internal
buffers.
Attributes:
_CHILDREN_MAP: Dictionnary of finded direct children for each
inspected sources.
_PARENTS_MAP: Dictionnary of finded direct parents for each inspected
sources.
parsers: Dictionnary of available Sass format parsers, where key is the
file extension related to the format and value is a parser
instance.
"""
parsers = {
"scss": ScssImportsParser(),
"sass": SassImportsParser(),
}
def __init__(self, *args, **kwargs):
self.reset()
[docs] def get_parser(self, path):
"""
Returns parser depending of extension from given file path.
Arguments:
path (str): Path to split to find extension which will select the
right parser. If file path does not have any extension or does not
match any enabled extension, ``scss`` will be assumed on default.
Returns:
parser: Either ``ScssImportsParser`` or ``SassImportsParser``
instance depending of filepath.
"""
filepath, ext = os.path.splitext(path)
# Remove leading dot from splitext to be able to match to
# ``self.parsers`` keys
if ext and ext.startswith("."):
ext = ext[1:]
if not ext or ext not in self.parsers:
ext = "scss"
return self.parsers.get(ext)
[docs] def reset(self):
"""
Reset internal buffers ``_CHILDREN_MAP`` and ``_PARENTS_MAP``.
"""
self._CHILDREN_MAP = {}
self._PARENTS_MAP = defaultdict(set)
[docs] def look_source(self, sourcepath, library_paths=None):
"""
Open a SCSS file (sourcepath) and find all involved files from
import rules.
This will fill internal buffers ``_CHILDREN_MAP`` and ``_PARENTS_MAP``.
Arguments:
sourcepath (str): Source file path to start searching for imports.
Keyword Arguments:
library_paths (list): List of directory paths for libraries to
resolve paths if resolving fails on the base source path.
Default to None.
"""
# Don't inspect again source that has allready be inspected as a
# children of a previous source
if sourcepath not in self._CHILDREN_MAP:
parser = self.get_parser(sourcepath)
with io.open(sourcepath, "r", encoding="utf-8") as fp:
finded_paths = parser.parse(fp.read())
children = self.resolve(sourcepath, finded_paths,
library_paths=library_paths)
# Those files that are imported by the sourcepath
self._CHILDREN_MAP[sourcepath] = children
# Those files that import the sourcepath
for p in children:
self._PARENTS_MAP[p].add(sourcepath)
# Start recursive finding through each resolved path that has not
# been collected yet
for path in children:
if path not in self._CHILDREN_MAP:
self.look_source(path, library_paths=library_paths)
return
[docs] def inspect(self, *args, **kwargs):
"""
Recursively inspect all given SCSS files to find imported dependencies.
This does not return anything. Just fill internal buffers about
inspected files.
Note:
This will ignore orphan files (files that are not imported from
any of given SCSS files).
Arguments:
*args: One or multiple arguments, each one for a source file path
to inspect.
Keyword Arguments:
library_paths (list): List of directory paths for libraries to
resolve paths if resolving fails on the base source path.
Default to None.
"""
library_paths = kwargs.get("library_paths", None)
for sourcepath in args:
self.look_source(sourcepath, library_paths=library_paths)
[docs] def _get_recursive_dependancies(self, dependencies_map, sourcepath,
recursive=True):
"""
Return all dependencies of a source, recursively searching through its
dependencies.
This is a common method used by ``children`` and ``parents`` methods.
Arguments:
dependencies_map (dict): Internal buffer (internal buffers
``_CHILDREN_MAP`` or ``_PARENTS_MAP``) to use for searching.
sourcepath (str): Source file path to start searching for
dependencies.
Keyword Arguments:
recursive (bool): Switch to enable recursive finding (if True).
Default to True.
Raises:
CircularImport: If circular error is detected from a source.
Returns:
set: List of dependencies paths.
"""
# Direct dependencies
collected = set([])
collected.update(dependencies_map.get(sourcepath, []))
# Sequence of 'dependencies_map' items to explore
sequence = collected.copy()
# Exploration list
walkthrough = []
# Recursive search starting from direct dependencies
if recursive:
while True:
if not sequence:
break
item = sequence.pop()
# Add current source to the explorated source list
walkthrough.append(item)
# Current item children
current_item_dependancies = dependencies_map.get(item, [])
for dependency in current_item_dependancies:
# Allready visited item, ignore and continue to the new
# item
if dependency in walkthrough:
continue
# Unvisited item yet, add its children to dependencies and
# item to explore
else:
collected.add(dependency)
sequence.add(dependency)
# Sourcepath has allready been visited but present itself
# again, assume it's a circular import
if sourcepath in walkthrough:
msg = "A circular import has occured by '{}'"
raise CircularImport(msg.format(current_item_dependancies))
# No more item to explore, break loop
if not sequence:
break
return collected
[docs] def children(self, sourcepath, recursive=True):
"""
Recursively find all children that are imported from the given source
path.
Arguments:
sourcepath (str): Source file path to search for.
Keyword Arguments:
recursive (bool): Switch to enabled recursive finding (if True).
Default to True.
Returns:
set: List of finded parents path.
"""
return self._get_recursive_dependancies(
self._CHILDREN_MAP,
sourcepath,
recursive=True
)
[docs] def parents(self, sourcepath, recursive=True):
"""
Recursively find all parents that import the given source path.
Arguments:
sourcepath (str): Source file path to search for.
Keyword Arguments:
recursive (bool): Switch to enabled recursive finding (if True).
Default to True.
Returns:
set: List of finded parents path.
"""
return self._get_recursive_dependancies(
self._PARENTS_MAP,
sourcepath,
recursive=True
)