Source code for pythonkss.parser

import fnmatch
import os

from pythonkss.comment import CommentParser
from pythonkss.exceptions import SectionDoesNotExist, DuplicateReferenceError, ExtendReferenceDoesNotExistError, \
    ReplaceReferenceDoesNotExistError, NotSectionError
from pythonkss.section import Section
from pythonkss.sectiontree import SectionTree


class MultiCommentBlockParser(object):
    def __init__(self):
        self._finished = False
        self._sections = {}
        self._extend_sections_map = {}
        self._replace_sections_map = {}

        # For debugging
        self._ignored_extend_sections = []
        self._replaced_sections = []

    def _add_section_to_list_in_dict(self, dct, section):
        if section.reference not in dct:
            dct[section.reference] = []
        dct[section.reference].append(section)

    def _add_section_type_extend(self, section):
        self._add_section_to_list_in_dict(
            dct=self._extend_sections_map,
            section=section
        )

    def _add_section_type_replace(self, section):
        self._add_section_to_list_in_dict(
            dct=self._replace_sections_map,
            section=section
        )

    def _add_section_type_default(self, section):
        if section.reference in self._sections:
            first_defined_section = self._sections[section.reference]
            raise DuplicateReferenceError(
                reference=section.reference,
                first_defined_section=first_defined_section,
                duplicate_section=section
            )
        else:
            self._sections[section.reference] = section

    def _add_section(self, section):
        if section.section_type == Section.TYPE_DEFAULT:
            self._add_section_type_default(section)
        elif section.section_type in Section.EXTEND_TYPES:
            self._add_section_type_extend(section)
        elif section.section_type == Section.TYPE_REPLACE:
            self._add_section_type_replace(section)

    def parse_commentblock(self, commentblock, filepath):
        section = Section(commentblock, filepath=filepath)
        try:
            section.parse()
        except NotSectionError:
            pass
        else:
            self._add_section(section)

    def _replace_sections(self):
        for reference, sections in self._replace_sections_map.items():
            if reference in self._sections:
                self._replaced_sections.append(
                    self._sections.pop(reference))
                if reference in self._extend_sections_map:
                    self._ignored_extend_sections.extend(
                        self._extend_sections_map.pop(reference))
                self._sections[reference] = sections[-1]
            else:
                raise ReplaceReferenceDoesNotExistError(
                    'Invalid "StyleguideReplace {reference}". '
                    'No normal section with this reference exists. '
                    'In file: {filepath}.'.format(
                        reference=reference,
                        filepath=sections[0].filepath))

    def _merge_sections(self):
        for reference, source_sections in self._extend_sections_map.items():
            try:
                target_section = self._sections[reference]
            except KeyError:
                raise ExtendReferenceDoesNotExistError(
                    'Invalid "Styleguide{section_type} {reference}". '
                    'No normal section with this reference exists. '
                    'In file: {filepath}.'.format(
                        section_type=source_sections[0].section_type,
                        filepath=source_sections[0].filepath,
                        reference=reference
                    ))
            else:
                for source_section in source_sections:
                    source_section.merge_into_section(target_section=target_section)

    def finish(self):
        """
        Handle merge and replace, and set ``self.sections`` to
        the resulting dict of sections.
        """
        self._replace_sections()
        self._merge_sections()
        self._finished = True

    def __check_finished(self, property_):
        if not self._finished:
            raise Exception('You have to call finish() before using '
                            'the {} property'.format(property_))

    @property
    def sections(self):
        self.__check_finished('sections')
        return self._sections

    @property
    def ignored_extend_sections(self):
        self.__check_finished('ignored_extend_sections')
        return self._ignored_extend_sections

    @property
    def replaced_sections(self):
        self.__check_finished('replaced_sections')
        return self._replaced_sections


[docs]class Parser(object): """ Parses one or more directories of style files. Examples: Parse a directory and print a styleguide (nice getting started exmaple for generating your own styleguide):: parser = pythonkss.Parser('/path/to/my/styles/') for section in parser.iter_sorted_sections(): print() print('*' * 70) print(section.reference, section.title) print('*' * 70) print() for modifier in section.modifiers: print('-- ', modifier.name, ' --') print(modifier.description_html) if section.description: print() print(section.description_html) if section.has_examples() or section.has_markups(): print() print('Usage:') print('=' * 70) print() for example in section.examples: if example.title: print('-- ', example.title, ' --') print(example.html) for markup in section.markups: if markup.title: print('-- ', markup.title, ' --') print(markup.html) """ def __init__(self, *paths, **kwargs): """ Args: *paths: One or more directories to search for style files. extensions: List of file extensions to search for. Optional - defaults to ``['.less', '.css', '.sass', '.scss']``. filename_patterns: Optional list of :func:`fnmatch.fnmatchcase` patterns for files to include in the styleguide. If this is not provided, all files in the directories specified in ``*paths`` is included. If this is provided, it must be a list/iterable of :func:`fnmatch.fnmatchcase` patterns. All file-paths matching any of the patterns in the list is included. Example:: filename_patterns=[ '*mytheme/*', '*external_theme/essentials/*', '*external_theme/advanced/menu.scss', '*external_theme/advanced/navbar.scss', ] The check agains filename_patterns is performed after the check for filename extensions (see ``extensions`` kwarg). So if a filename does not match the required extensions, putting it in ``filename_patterns`` does not help at all. variables (dict): Dict that maps variables to values. Variables can be used anywhere in the comments, and they are applied before any other parsing of the comments. variablepattern: The pattern used to insert variables. Defaults to ``{{% {variable} %}}``, which means that you would insert a variable added as ``$my-variable`` via the ``variables`` parameter with something like this:: /* My title The value of $my-variable is {% $my-variable %}. Styleguide 1.1 */ """ self.paths = paths self.variables = kwargs.pop('variables', None) self.filename_patterns = kwargs.pop('filename_patterns', None) self.variablepattern = kwargs.pop('variablepattern', '{{% {variable} %}}') extensions = kwargs.pop('extensions', None) if extensions is None: extensions = ['.less', '.css', '.sass', '.scss'] self.extensions = extensions def _make_variablemap(self): variablemap = {} if not self.variables: return variablemap for variable, value in self.variables.items(): mapkey = self.variablepattern.format(variable=variable) variablemap[mapkey] = value return variablemap def _has_match_in_filename_patterns(self, filepath): for pattern in self.filename_patterns: if fnmatch.fnmatchcase(filepath, pattern): return True return False def should_include_file(self, filepath): if self.filename_patterns is None: return True if self._has_match_in_filename_patterns(filepath): return True else: return False
[docs] def find_files(self): """ Find files in `paths` which match valid extensions. Returns: iterator: An iterable yielding file paths. """ for path in self.paths: for subpath, dirs, files in os.walk(path): for filename in files: (name, ext) = os.path.splitext(filename) if ext in self.extensions: filepath = os.path.join(subpath, filename) if self.should_include_file(filepath): yield filepath
def parse(self): variablemap = self._make_variablemap() multiblockparser = MultiCommentBlockParser() for filepath in self.find_files(): commentparser = CommentParser(filepath, variablemap=variablemap) for commentblock in commentparser.blocks: multiblockparser.parse_commentblock( commentblock=commentblock, filepath=filepath) multiblockparser.finish() return multiblockparser @property def multiblockparser(self): if not hasattr(self, '_multiblockparser'): self._multiblockparser = self.parse() return self._multiblockparser @property def sections(self): """ A dict of sections with :meth:`~pythonkss.section.Section.reference` as key and :class:`:meth:`~pythonkss.section.Section` objects as value. """ return self.multiblockparser.sections
[docs] def get_sections(self, referenceprefix=None): """ Get sections, optionally only sections with :meth:`~pythonkss.section.Section.reference` starting with ``referenceprefix``. Args: referenceprefix: If this is provided, only sections with :meth:`~pythonkss.section.Section.reference` starting with ``referenceprefix`` is included. Returns: list: A list of sections. """ sections = self.sections.values() if referenceprefix: sections = filter(lambda s: s.reference.startswith(referenceprefix), sections) return sections
[docs] def iter_sorted_sections(self, referenceprefix=None): """ Iterate sections sorted by :meth:`pythonkss.section.Section.reference`. Args: referenceprefix: See :meth:`.get_sections`. Returns: generator: Iterable of :class:`pythonkss.section.Section` objects. """ return sorted(self.get_sections(referenceprefix=referenceprefix), key=lambda s: s.reference)
[docs] def as_tree(self): """ Get sections organized in :class:`pythonkss.sectiontree.SectionTree`. Returns: pythonkss.sectiontree.SectionTree: The built tree. """ if not hasattr(self, '_built_tree'): self._built_tree = SectionTree(sections=self.iter_sorted_sections()) return self._built_tree
[docs] def get_section_by_reference(self, reference): """ Get a section by its :meth:`pythonkss.section.Section.reference`. Raises: KeyError: If no section with the provided reference exists. """ try: return self.sections[reference] except KeyError: raise SectionDoesNotExist('Section "%s" does not exist.' % reference)