class ClassSymbol(StructSymbol):
    """
    Banana banana
    """
    __tablename__ = 'classes'

    def __init__(self, **kwargs):
        self.hierarchy = []
        self.children = {}
        StructSymbol.__init__(self, **kwargs)

    def get_type_name(self):
        return "Class"

    def get_children_symbols(self):
        return self.hierarchy + list(
            self.children.values()) + super().get_children_symbols() class FunctionSymbol(Symbol):
    """
    Banana banana
    """
    __tablename__ = 'functions'

    def __init__(self, **kwargs):
        self.parameters = []
        self.return_value = [None]
        self.throws = False
        self.is_ctor_for = None
        Symbol.__init__(self, **kwargs)

    def get_children_symbols(self):
        return self.parameters + self.return_value + super().get_children_symbols()

    def get_type_name(self):
        return 'Function' class Extension(Configurable):
    """
    All extensions should inherit from this base class

    Attributes:
        extension_name: str, the unique name of this extension, should
            be overriden and namespaced appropriately.
        project: project.Project, the Project instance which documentation
            hotdoc is working on.
        formatter: formatter.Formatter, may be subclassed.
        argument_prefix (str): Short name of this extension, used as a prefix
            to add to automatically generated command-line arguments.
    """
    # pylint: disable=unused-argument
    extension_name = "base-extension"
    argument_prefix = ''
    paths_arguments = {}
    path_arguments = {}
    written_out_sitemaps = set()

    def __init__(self, app, project):
        """Constructor for `Extension`.

        This should never get called directly.

        Args:
            project: The `project.Project` instance which documentation
                is being generated.
        """
        self.project = project
        self.app = app
        self.sources = set()
        self.smart_sources = []
        self.index = None
        self.source_roots = OrderedSet()
        self._created_symbols = DefaultOrderedDict(OrderedSet)
        self.__package_root = None
        self.__toplevel_comments = OrderedSet()

        self.formatter = self._make_formatter()

    # pylint: disable=no-self-use
    def warn(self, code, message):
        """
        Shortcut function for `loggable.warn`

        Args:
            code: see `utils.loggable.warn`
            message: see `utils.loggable.warn`
        """
        warn(code, message)

    # pylint: disable=no-self-use
    def error(self, code, message):
        """
        Shortcut function for `utils.loggable.error`

        Args:
            code: see `utils.loggable.error`
            message: see `utils.loggable.error`
        """
        error(code, message)

    def debug(self, message, domain=None):
        """
        Shortcut function for `utils.loggable.debug`

        Args:
            message: see `utils.loggable.debug`
            domain: see `utils.loggable.debug`
        """
        if domain is None:
            domain = self.extension_name
        debug(message, domain)

    def info(self, message, domain=None):
        """
        Shortcut function for `utils.loggable.info`

        Args:
            message: see `utils.loggable.info`
            domain: see `utils.loggable.info`
        """
        if domain is None:
            domain = self.extension_name
        info(message, domain)

    def reset(self):
        """
        This function is only useful for testing purposes, at least
        for now.
        """
        self._created_symbols = DefaultOrderedDict(OrderedSet)
        self.__package_root = None

    def get_pagename(self, name):
        self.__find_package_root()
        # Find the longest prefix
        longest = None
        for path in OrderedSet([self.__package_root]) | self.source_roots:
            commonprefix = os.path.commonprefix([path, name])
            if commonprefix == path and (longest is None or len(path) > len(longest)):
                longest = path

        if longest is not None:
            return os.path.relpath(name, longest)

        return name

    def get_symbol_page(self, symbol_name, symbol_pages, smart_pages, section_links):
        if symbol_name in symbol_pages:
            return symbol_pages[symbol_name]

        symbol = self.app.database.get_symbol(symbol_name)
        assert symbol is not None

        if symbol.parent_name and symbol.parent_name != symbol_name:
            page = self.get_symbol_page(
                symbol.parent_name, symbol_pages, smart_pages, section_links)
        else:
            smart_key = self._get_smart_key(symbol)
            if smart_key is None:
                return None

            if smart_key in smart_pages:
                page = smart_pages[smart_key]
            else:
                pagename = self.get_pagename(smart_key)
                page = Page(smart_key, True, self.project.sanitized_name, self.extension_name,
                            output_path=os.path.dirname(pagename))
                if page.link.ref in section_links:
                    self.warn('output-page-conflict',
                              'Creating a page for symbol %s would overwrite the page '
                              'declared in a toplevel comment (%s)' % (symbol_name, page.link.ref))
                    page = None
                else:
                    smart_pages[smart_key] = page

        if page is not None:
            symbol_pages[symbol_name] = page

        return page

    def _get_toplevel_comments(self):
        return self.__toplevel_comments

    # pylint: disable=too-many-locals
    # pylint: disable=too-many-branches
    # pylint: disable=too-many-statements
    def make_pages(self):
        # All symbol names that no longer need to be assigned to a page
        dispatched_symbol_names = set()

        # Map symbol names with pages
        # This is used for assigning symbols with a parent to the page
        # where their parent will be rendered, unless the symbol has been
        # explicitly assigned or ignored
        symbol_pages = {}

        smart_pages = OrderedDict()

        # Map pages with the sources they explicitly import
        imported_sources = {}

        # This is simply used as a conflict detection mechanism, see
        # hotdoc.core.tests.test_doc_tree.TestTree.test_section_and_path_conflict
        section_links = set()

        # These are used as a duplicate detection mechanism, map
        # relocated or ignored symbols to the source files where they were initially
        # listed
        relocated_symbols = {}
        private_symbols = {}

        # First we make one page per toplevel comment (eg. SECTION comment)
        # This is the highest priority mechanism for sorting symbols
        for comment in self._get_toplevel_comments():
            # Programming error from extension author
            assert comment.name
            symbol_names = comment.meta.pop('symbols', [])
            private_symbol_names = comment.meta.pop('private-symbols', [])
            sources = comment.meta.pop('sources', None)

            page = Page(comment.name, True, self.project.sanitized_name, self.extension_name,
                        comment=comment)

            for symbol_name in symbol_names:
                if symbol_name in relocated_symbols:
                    self.warn('symbol-listed-twice',
                              'Symbol %s listed in %s was already listed in %s' %
                              (symbol_name, comment.filename, relocated_symbols[symbol_name]))
                    continue
                elif symbol_name in private_symbols:
                    self.warn('symbol-listed-twice',
                              'Symbol %s listed in %s was marked as private in %s' %
                              (symbol_name, comment.filename, private_symbols[symbol_name]))
                    continue
                else:
                    page.symbol_names.add(symbol_name)
                    symbol_pages[symbol_name] = page
                    relocated_symbols[symbol_name] = comment.filename
                    dispatched_symbol_names.add(symbol_name)

            for symbol_name in private_symbol_names:
                if symbol_name in relocated_symbols:
                    self.warn('symbol-listed-twice',
                              'Symbol %s marked private in %s was already listed in %s' %
                              (symbol_name, comment.filename, relocated_symbols[symbol_name]))
                    continue
                elif symbol_name in private_symbols:
                    self.warn('symbol-listed-twice',
                              'Symbol %s marked as private in %s was '
                              'already marked as private in %s' %
                              (symbol_name, comment.filename, private_symbols[symbol_name]))
                    continue
                private_symbols[symbol_name] = comment.filename
                symbol_pages[symbol_name] = None
                dispatched_symbol_names.add(symbol_name)

            section_links.add(page.link.ref)
            smart_key = self._get_comment_smart_key(comment)
            if smart_key in smart_pages:
                smart_pages[comment.name] = page
            else:
                smart_pages[smart_key] = page

            if sources is not None:
                abs_sources = []
                for source in sources:
                    if os.path.isabs(source):
                        abs_sources.append(source)
                    else:
                        abs_sources.append(os.path.abspath(os.path.join(
                            os.path.dirname(comment.filename), source)))
                imported_sources[page] = abs_sources

        # Used as a duplicate detection mechanism
        relocated_sources = {}

        # We now browse all the pages with explicitly imported sources
        # Importing sources has a lower level of priority than importing
        # symbols, which is why we do that in a separate loop
        for page, sources in imported_sources.items():
            for source in sources:
                if source not in self._get_all_sources():
                    self.warn('invalid-relocated-source',
                              'Source %s does not exist but is relocated in %s' %
                              (source, page.name))
                    continue

                if source in relocated_sources:
                    self.warn('invalid-relocated-source',
                              'Source %s relocated in %s was already relocated in %s' %
                              (source, page.name, relocated_sources[source]))
                    continue

                if source in self._created_symbols:
                    symbol_names = OrderedSet(
                        self._created_symbols[source]) - dispatched_symbol_names
                    page.symbol_names |= symbol_names
                    dispatched_symbol_names |= symbol_names

                relocated_sources[source] = page.name

        # We now browse all the symbols we have created
        for _, symbol_names in self._created_symbols.items():
            for symbol_name in symbol_names:
                if symbol_name in dispatched_symbol_names:
                    continue

                page = self.get_symbol_page(
                    symbol_name, symbol_pages, smart_pages, section_links)

                # Can be None if creating a page to hold the symbol conflicts with
                # a page explicitly declared in a toplevel comment or a parent has been
                # marked as private
                if page is None:
                    continue

                page.symbol_names.add(symbol_name)
                dispatched_symbol_names.add(symbol_name)

        # Finally we make our index page
        if self.index:
            index_page = self.project.tree.parse_page(
                self.index, next(iter(self.project.include_paths)), self.extension_name)
        else:
            index_page = Page('%s-index' % self.argument_prefix, True, self.project.sanitized_name,
                              self.extension_name)

        if not index_page.title:
            index_page.title = self._get_smart_index_title()

        smart_pages['%s-index' % self.argument_prefix] = index_page

        return smart_pages

    def setup(self):
        """
        Extension subclasses should implement this to scan whatever
        source files they have to scan, and connect to the various
        signals they have to connect to.

        Note that this will be called *after* the `tree.Tree`
        of this instance's `Extension.project` has been fully
        constructed, but before its `tree.Tree.resolve_symbols`
        method has been called.
        """
        pass

    @staticmethod
    def get_dependencies():
        """
        Override this to return the list of extensions this extension
        depends on if needed.

        Returns:
            list: A list of `ExtDependency` instances.
        """
        return []

    def parse_toplevel_config(self, config):
        """Parses and make use of the toplevel configuration."""
        self.formatter.parse_toplevel_config(config)

    def parse_config(self, config):
        """
        Override this, making sure to chain up first, if your extension adds
        its own custom command line arguments, or you want to do any further
        processing on the automatically added arguments.

        The default implementation will set attributes on the extension:
        - 'sources': a set of absolute paths to source files for this extension
        - 'index': absolute path to the index for this extension

        Additionally, it will set an attribute for each argument added with
        `Extension.add_path_argument` or `Extension.add_paths_argument`, with
        the extension's `Extension.argument_prefix` stripped, and dashes
        changed to underscores.

        Args:
            config: a `config.Config` instance
        """
        prefix = self.argument_prefix
        self.sources = config.get_sources(prefix)
        self.smart_sources = [
            self._get_smart_filename(s) for s in self.sources]
        self.index = config.get_index(prefix)
        self.source_roots = OrderedSet(
            config.get_paths('%s_source_roots' % prefix))

        for arg, dest in list(self.paths_arguments.items()):
            val = config.get_paths(arg)
            setattr(self, dest, val)

        for arg, dest in list(self.path_arguments.items()):
            val = config.get_path(arg)
            setattr(self, dest, val)

        self.formatter.parse_config(config)

    def add_attrs(self, symbol, **kwargs):
        """
        Helper for setting symbol extension attributes
        """
        for key, val in kwargs.items():
            symbol.add_extension_attribute(self.extension_name, key, val)

    def get_attr(self, symbol, attrname):
        """
        Helper for getting symbol extension attributes
        """
        return symbol.extension_attributes.get(self.extension_name, {}).get(
            attrname, None)

    @staticmethod
    def add_arguments(parser):
        """
        Subclasses may implement this method to add their own arguments to
        the hotdoc binary.

        In this function, you should add an argument group to the passed-in
        parser, corresponding to your extension.
        You can then add arguments to that argument group.

        Example::

            @staticmethod
            def add_arguments(parser):
                group = parser.add_argument_group('Chilidoc',
                    'Delicious Hotdoc extension')
                Chilidoc.add_sources_argument(group)
                group.add_argument('--chili-spicy', action='store_true',
                    help='Whether to add extra pepper')

        Args:
            parser (argparse.ArgumentParser): Main hotdoc argument parser
        """
        pass

    @classmethod
    def get_assets_licensing(cls):
        return {}

    @classmethod
    def add_index_argument(cls, group):
        """
        Subclasses may call this to add an index argument.

        Args:
            group: arparse.ArgumentGroup, the extension argument group
            prefix: str, arguments have to be namespaced
        """
        prefix = cls.argument_prefix

        group.add_argument(
            '--%s-index' % prefix, action="store",
            dest="%s_index" % prefix,
            help=("Name of the %s root markdown file, can be None" % (
                cls.extension_name)))

    @classmethod
    def add_sources_argument(cls, group, allow_filters=True, prefix=None, add_root_paths=False):
        """
        Subclasses may call this to add sources and source_filters arguments.

        Args:
            group: arparse.ArgumentGroup, the extension argument group
            allow_filters: bool,  Whether the extension wishes to expose a
                source_filters argument.
            prefix: str, arguments have to be namespaced.
        """
        prefix = prefix or cls.argument_prefix

        group.add_argument("--%s-sources" % prefix,
                           action="store", nargs="+",
                           dest="%s_sources" % prefix.replace('-', '_'),
                           help="%s source files to parse" % prefix)

        if allow_filters:
            group.add_argument("--%s-source-filters" % prefix,
                               action="store", nargs="+",
                               dest="%s_source_filters" % prefix.replace(
                                   '-', '_'),
                               help="%s source files to ignore" % prefix)

        if add_root_paths:
            group.add_argument("--%s-source-roots" % prefix,
                               action="store", nargs="+",
                               dest="%s_source_roots" % prefix.replace(
                                   '-', '_'),
                               help="%s source root directories allowing files "
                                    "to be referenced relatively to those" % prefix)

    @classmethod
    def add_path_argument(cls, group, argname, dest=None, help_=None):
        """
        Subclasses may call this to expose a path argument.

        Args:
            group: arparse.ArgumentGroup, the extension argument group
            argname: str, the name of the argument, will be namespaced.
            dest: str, similar to the `dest` argument of
                `argparse.ArgumentParser.add_argument`, will be namespaced.
            help_: str, similar to the `help` argument of
                `argparse.ArgumentParser.add_argument`.
        """
        prefixed = '%s-%s' % (cls.argument_prefix, argname)
        if dest is None:
            dest = prefixed.replace('-', '_')
            final_dest = dest[len(cls.argument_prefix) + 1:]
        else:
            final_dest = dest
            dest = '%s_%s' % (cls.argument_prefix, dest)

        group.add_argument('--%s' % prefixed, action='store',
                           dest=dest, help=help_)
        cls.path_arguments[dest] = final_dest

    @classmethod
    def add_paths_argument(cls, group, argname, dest=None, help_=None):
        """
        Subclasses may call this to expose a paths argument.

        Args:
            group: arparse.ArgumentGroup, the extension argument group
            argname: str, the name of the argument, will be namespaced.
            dest: str, similar to the `dest` argument of
                `argparse.ArgumentParser.add_argument`, will be namespaced.
            help_: str, similar to the `help` argument of
                `argparse.ArgumentParser.add_argument`.
        """
        prefixed = '%s-%s' % (cls.argument_prefix, argname)
        if dest is None:
            dest = prefixed.replace('-', '_')
            final_dest = dest[len(cls.argument_prefix) + 1:]
        else:
            final_dest = dest
            dest = '%s_%s' % (cls.argument_prefix, dest)

        group.add_argument('--%s' % prefixed, action='store', nargs='+',
                           dest=dest, help=help_)
        cls.paths_arguments[dest] = final_dest

    def add_comment(self, comment):
        if comment.toplevel:
            self.__toplevel_comments.add(comment)
        else:
            self.app.database.add_comment(comment)

    def create_symbol(self, *args, **kwargs):
        """
        Extensions that discover and create instances of `symbols.Symbol`
        should do this through this method, as it will keep an index
        of these which can be used when generating a "naive index".

        See `database.Database.create_symbol` for more
        information.

        Args:
            args: see `database.Database.create_symbol`
            kwargs: see `database.Database.create_symbol`

        Returns:
            symbols.Symbol: the created symbol, or `None`.
        """
        if not kwargs.get('project_name'):
            kwargs['project_name'] = self.project.project_name

        sym = self.app.database.create_symbol(*args, **kwargs)
        if sym:
            # pylint: disable=unidiomatic-typecheck
            if type(sym) != Symbol:
                self._created_symbols[sym.filename].add(sym.unique_name)

        return sym

    def rename_symbol(self, unique_name, target):
        sym = self.app.database.rename_symbol(unique_name, target)
        # pylint: disable=unidiomatic-typecheck
        if sym and type(sym) != Symbol:
            self._created_symbols[sym.filename].remove(target)
            self._created_symbols[sym.filename].add(sym.unique_name)

    def _make_formatter(self):
        return Formatter(self)

    def get_possible_path(self, name):
        self.__find_package_root()

        for path in OrderedSet([self.__package_root]) | self.source_roots:
            possible_path = os.path.join(path, name)
            if possible_path in self._get_all_sources():
                return self._get_smart_filename(possible_path)
        return None

    def __find_package_root(self):
        if self.__package_root:
            return

        commonprefix = os.path.commonprefix(
            list(self._get_all_sources()) + list(self.source_roots))
        self.__package_root = os.path.dirname(commonprefix)

    def _get_smart_index_title(self):
        return 'Reference Manual'

    def _get_all_sources(self):
        return self.sources

    def _get_smart_key(self, symbol):
        return symbol.filename

    def _get_smart_filename(self, filename):
        return filename

    def _get_comment_smart_key(self, comment):
        return comment.filename

    def format_page(self, page, link_resolver, output):
        """
        Called by `project.Project.format_page`, to leave full control
        to extensions over the formatting of the pages they are
        responsible of.

        Args:
            page: tree.Page, the page to format.
            link_resolver: links.LinkResolver, object responsible
                for resolving links potentially mentioned in `page`
            output: str, path to the output directory.
        """
        debug('Formatting page %s' % page.link.ref, 'formatting')

        if output:
            actual_output = os.path.join(output,
                                         'html')
            if not os.path.exists(actual_output):
                os.makedirs(actual_output)
        else:
            actual_output = None

        page.format(self.formatter, link_resolver, actual_output)

    def write_out_sitemap(self, opath):
        """
        Banana banana
        """
        if opath not in self.written_out_sitemaps:
            Extension.formatted_sitemap = self.formatter.format_navigation(
                self.app.project)
            if Extension.formatted_sitemap:
                escaped_sitemap = Extension.formatted_sitemap.replace(
                    '\\', '\\\\').replace('"', '\\"').replace('\n', '')
                js_wrapper = 'sitemap_downloaded_cb("%s");' % escaped_sitemap
                with open(opath, 'w') as _:
                    _.write(js_wrapper)

        self.written_out_sitemaps.add(opath)

    # pylint: disable=too-many-locals
    def write_out_page(self, output, page):
        """
        Banana banana
        """
        subpages = OrderedDict({})
        all_pages = self.project.tree.get_pages()
        subpage_names = self.get_subpages_sorted(all_pages, page)
        for pagename in subpage_names:
            proj = self.project.subprojects.get(pagename)

            if not proj:
                cpage = all_pages[pagename]
                sub_formatter = self.project.extensions[
                    cpage.extension_name].formatter
            else:
                cpage = proj.tree.root
                sub_formatter = proj.extensions[cpage.extension_name].formatter

            subpage_link, _ = cpage.link.get_link(self.app.link_resolver)
            prefix = sub_formatter.get_output_folder(cpage)
            if prefix:
                subpage_link = '%s/%s' % (prefix, subpage_link)
            subpages[subpage_link] = cpage

        html_subpages = self.formatter.format_subpages(page, subpages)

        js_dir = os.path.join(output, 'html', 'assets', 'js')
        if not os.path.exists(js_dir):
            os.makedirs(js_dir)
        sm_path = os.path.join(js_dir, 'sitemap.js')
        self.write_out_sitemap(sm_path)

        self.formatter.write_out(page, html_subpages, output)

    def get_subpages_sorted(self, pages, page):
        """Get @page subpages sorted appropriately."""

        sorted_pages = []
        to_sort = []
        for subpage in page.subpages:
            # Do not resort subprojects even if they are
            # 'generated'.
            if pages[subpage].pre_sorted:
                sorted_pages.append(subpage)
            else:
                to_sort.append(subpage)

        return sorted_pages + sorted(
            to_sort, key=lambda p: pages[p].get_title().lower()) class OrderedSet(MutableSet):
    """
    Banana banana
    """

    def __init__(self, iterable=None):
        self.end = end = []
        end += [None, end, end]         # sentinel node for doubly linked list
        self.map = {}                   # key --> [key, prev, next]
        if iterable is not None:
            self |= iterable

    def __len__(self):
        return len(self.map)

    def __contains__(self, key):
        return key in self.map

    # pylint: disable=arguments-differ
    def add(self, key):
        """
        Banana banana
        """
        if key not in self.map:
            end = self.end
            curr = end[1]
            curr[2] = end[1] = self.map[key] = [key, curr, end]

    def __getstate__(self):
        if not self:
            # The state can't be an empty list.
            # We need to return a truthy value, or else
            # __setstate__ won't be run.
            #
            # This could have been done more gracefully by always putting
            # the state in a tuple, but this way is backwards- and forwards-
            # compatible with previous versions of OrderedSet.
            return (None,)
        return list(self)

    def __setstate__(self, state):
        if state == (None,):
            self.__init__([])
        else:
            self.__init__(state)

    # pylint: disable=arguments-differ
    def discard(self, key):
        """
        Banana banana
        """
        if key in self.map:
            key, prev, nxt = self.map.pop(key)
            prev[2] = nxt
            nxt[1] = prev

    def __iter__(self):
        end = self.end
        curr = end[2]
        while curr is not end:
            yield curr[0]
            curr = curr[2]

    def __reversed__(self):
        end = self.end
        curr = end[1]
        while curr is not end:
            yield curr[0]
            curr = curr[1]

    # pylint: disable=arguments-differ
    def pop(self, last=True):
        """
        Banana banana
        """
        if not self:
            raise KeyError('set is empty')
        key = self.end[1][0] if last else self.end[2][0]
        self.discard(key)
        return key

    def __repr__(self):
        if not self:
            return '%s()' % (self.__class__.__name__,)
        return '%s(%r)' % (self.__class__.__name__, list(self))

    def __eq__(self, other):
        if isinstance(other, OrderedSet):
            return len(self) == len(other) and list(self) == list(other)
        return set(self) == set(other) class Logger(Configurable):

    """Subclasses can inherit from this class to report recoverable errors."""

    _error_code_to_exception = defaultdict()
    _domain_codes = defaultdict(set)
    _warning_code_to_exception = defaultdict()
    journal = []
    fatal_warnings = False
    raise_on_fatal_warnings = False
    _ignored_codes = set()
    _ignored_domains = set()
    _last_checkpoint = 0
    _verbosity = WARNING
    silent = False
    n_fatal_warnings = 0

    @staticmethod
    def register_error_code(code, exception_type, domain='core'):
        """Register a new error code"""
        Logger._error_code_to_exception[code] = (exception_type, domain)
        Logger._domain_codes[domain].add(code)

    @staticmethod
    def register_warning_code(code, exception_type, domain='core'):
        """Register a new warning code"""
        Logger._warning_code_to_exception[code] = (exception_type, domain)
        Logger._domain_codes[domain].add(code)

    @staticmethod
    def _log(code, message, level, domain):
        """Call this to add an entry in the journal"""
        entry = LogEntry(level, domain, code, message)
        Logger.journal.append(entry)

        if Logger.silent:
            return

        if level >= Logger._verbosity:
            _print_entry(entry)

    @staticmethod
    def error(code, message, **kwargs):
        """Call this to raise an exception and have it stored in the journal"""
        assert code in Logger._error_code_to_exception
        exc_type, domain = Logger._error_code_to_exception[code]
        exc = exc_type(message, **kwargs)
        Logger._log(code, exc.message, ERROR, domain)
        raise exc

    @staticmethod
    def warn(code, message, **kwargs):
        """
        Call this to store a warning in the journal.

        Will raise if `Logger.fatal_warnings` is set to True and
        `Logger.raise_on_fatal_warnings` as well.
        """

        if code in Logger._ignored_codes:
            return

        assert code in Logger._warning_code_to_exception
        exc_type, domain = Logger._warning_code_to_exception[code]

        if domain in Logger._ignored_domains:
            return

        level = WARNING
        if Logger.fatal_warnings:
            level = ERROR

        exc = exc_type(message, **kwargs)

        Logger._log(code, exc.message, level, domain)

        if Logger.fatal_warnings:
            if Logger.raise_on_fatal_warnings:
                raise exc
            else:
                Logger.n_fatal_warnings += 1

    @staticmethod
    def debug(message, domain):
        """Log debugging information"""
        if domain in Logger._ignored_domains:
            return

        Logger._log(None, message, DEBUG, domain)

    @staticmethod
    def info(message, domain):
        """Log simple info"""
        if domain in Logger._ignored_domains:
            return

        Logger._log(None, message, INFO, domain)

    @staticmethod
    def add_ignored_code(code):
        """Add a code to ignore. Errors cannot be ignored."""
        Logger._ignored_codes.add(code)

    @staticmethod
    def add_ignored_domain(code):
        """Add a domain to ignore. Errors cannot be ignored."""
        Logger._ignored_domains.add(code)

    @staticmethod
    def checkpoint():
        """Add a checkpoint"""
        Logger._last_checkpoint = len(Logger.journal)

    @staticmethod
    def since_checkpoint():
        """Get journal since last checkpoint"""
        return Logger.journal[Logger._last_checkpoint:]

    @staticmethod
    def get_issues():
        """Get actual issues in the journal."""
        issues = []
        for entry in Logger.journal:
            if entry.level >= WARNING:
                issues.append(entry)
        return issues

    @staticmethod
    def reset():
        """Resets Logger to its initial state"""
        Logger.journal = []
        Logger.fatal_warnings = False
        Logger.raise_on_fatal_warnings = False
        Logger._ignored_codes = set()
        Logger._ignored_domains = set()
        Logger._verbosity = 2
        Logger._last_checkpoint = 0
        Logger.n_fatal_warnings = 0

    @staticmethod
    def add_arguments(parser):
        """Banana banana
        """
        group = parser.add_argument_group(
            'Logger', 'logging options')
        group.add_argument("--verbose", '-v', action="count",
                           dest="verbose",
                           help="Turn on verbosity, -vv for debug")
        group.add_argument("--fatal-warnings", action="store_true",
                           dest="fatal_warnings", help="Make warnings fatal")

    @staticmethod
    def set_verbosity(verbosity):
        """Banana banana
        """
        Logger._verbosity = min(max(0, WARNING - verbosity), 2)
        debug("Verbosity set to %d" % (WARNING - Logger._verbosity), 'logging')

    @staticmethod
    def parse_config(config):
        Logger._verbosity = max(0, Logger._verbosity - (
            config.get('verbose') or 0))

        Logger.fatal_warnings = bool(config.get("fatal_warnings")) class Application(Configurable):
    """
    Banana banana
    """
    # pylint: disable=too-many-instance-attributes

    def __init__(self, extension_classes):
        self.extension_classes = OrderedDict(
            {CoreExtension.extension_name: CoreExtension})
        for ext_class in extension_classes:
            self.extension_classes[ext_class.extension_name] = ext_class
        self.output = None
        self.private_folder = None
        self.database = None
        self.link_resolver = None
        self.dry = False
        self.hostname = None
        self.config = None
        self.project = None
        self.formatted_signal = Signal()
        self.__all_projects = {}

    @staticmethod
    def add_arguments(parser):
        parser.add_argument('--dry',
                            help='Dry run, nothing will be output',
                            dest='dry', action='store_true')
        parser.add_argument('--deps-file-dest',
                            help='Where to output the dependencies file')
        parser.add_argument('--deps-file-target',
                            help='Name of the dependencies target',
                            default='doc.stamp.d')
        parser.add_argument("-o", "--output", action="store",
                            dest="output",
                            help="where to output the rendered "
                            "documentation")
        parser.add_argument('--hostname', dest='hostname', action='store',
                            help='Where the documentation will be hosted, '
                            'for example <http://hotdoc.com>. When provided, '
                            'an XML sitemap will be generated for SEO '
                            'purposes.')

    def parse_config(self, config):
        self.config = config
        self.output = config.get_path('output')
        self.dry = config.get('dry')
        self.hostname = config.get('hostname')
        self.project = Project(self)
        self.project.parse_name_from_config(self.config)
        self.private_folder = os.path.abspath(
            'hotdoc-private-%s' % self.project.sanitized_name)
        shutil.rmtree(self.private_folder, ignore_errors=True)
        self.project.parse_config(self.config, toplevel=True)

        self.__setup_private_folder()
        self.__setup_database()

    def run(self):
        """
        Banana banana
        """
        self.project.setup()
        self.__retrieve_all_projects(self.project)

        self.link_resolver.get_link_signal.connect_after(self.__get_link_cb)
        self.project.format(self.link_resolver, self.output)
        self.project.write_out(self.output)

        # Generating an XML sitemap makes no sense without a hostname
        if self.hostname:
            self.project.write_seo_sitemap(self.hostname, self.output)

        self.link_resolver.get_link_signal.disconnect(self.__get_link_cb)

        self.formatted_signal(self)
        self.__persist()

    def __get_link_cb(self, link_resolver, name):
        url_components = urlparse(name)

        project = self.__all_projects.get(url_components.path)
        if not project:
            return None

        page = project.tree.root
        ext = project.extensions[page.extension_name]
        formatter = ext.formatter
        prefix = formatter.get_output_folder(page)
        ref = page.link.ref
        if url_components.fragment:
            ref += '#%s' % url_components.fragment

        return Link(os.path.join(prefix, ref), page.link.get_title(), None)

    def __retrieve_all_projects(self, project):
        self.__all_projects[project.project_name] = project

        for subproj in project.subprojects.values():
            self.__retrieve_all_projects(subproj)

    def __persist(self):
        if self.dry:
            return

        info('Persisting database and private files', 'persisting')

        self.database.persist()
        self.__dump_deps_file(self.project)

    def finalize(self):
        """
        Banana banana
        """
        self.project.finalize()

    def __setup_private_folder(self):
        if os.path.exists(self.private_folder):
            if not os.path.isdir(self.private_folder):
                error('setup-issue',
                      '%s exists but is not a directory' % self.private_folder)
        else:
            os.mkdir(self.private_folder)

    def __setup_database(self):
        self.database = Database(self.private_folder)
        self.link_resolver = LinkResolver(self.database)

    def __dump_project_deps_file(self, project, deps_file, empty_targets):
        for page in list(project.tree.get_pages().values()):
            if not page.generated:
                empty_targets.append(page.source_file)
                deps_file.write(u'%s ' % page.source_file)

        for subproj in project.subprojects.values():
            self.__dump_project_deps_file(subproj, deps_file, empty_targets)

    def __dump_deps_file(self, project):
        dest = self.config.get('deps_file_dest', None)
        target = self.config.get('deps_file_target')

        if dest is None:
            info("Not dumping deps file")
            return

        info("Dumping deps file to %s with target %s" %
             (dest, target))

        destdir = os.path.dirname(dest)
        if not os.path.exists(destdir):
            os.makedirs(destdir)

        empty_targets = []
        with open(dest, 'w', encoding='utf-8') as _:
            _.write(u'%s: ' % target)

            for dep in self.config.get_dependencies():
                empty_targets.append(dep)
                _.write(u'%s ' % dep.replace(' ', '\\ '))

            self.__dump_project_deps_file(project, _, empty_targets)

            for empty_target in empty_targets:
                _.write(u'\n\n%s:' % empty_target) class InvalidOutputException(HotdocException):
    """Banana banana"""
    pass class SymbolListedTwiceException(InvalidPageMetadata):
    pass class InvalidRelocatedSourceException(InvalidPageMetadata):
    pass class PageNotFoundException(HotdocSourceException):
    """
    Raised when a subpage listed in the sitemap file could not be found
    in any of the include paths.
    """
    pass class IndexExtensionNotFoundException(HotdocSourceException):
    """
    Raised when the extension was not found for an index placeholder
    """
    pass
import unittest
import shutil
import io
import os
from hotdoc.core.symbols import ClassSymbol, FunctionSymbol
from hotdoc.parsers import cmark
from hotdoc.core.extension import Extension
from hotdoc.utils.utils import OrderedSet
from hotdoc.utils.loggable import Logger
from hotdoc.core.config import Config
from hotdoc.run_hotdoc import Application
from hotdoc.core.comment import Comment
from hotdoc.core.exceptions import InvalidOutputException
from hotdoc.core.extension import (SymbolListedTwiceException,
                                   InvalidRelocatedSourceException)
from hotdoc.core.tree import (PageNotFoundException,
                              IndexExtensionNotFoundException)

        self.__create_test_layout(
            sitemap=sitemap, symbols=symbols,
            source_roots=[os.path.join(self.__src_dir, 'a'),
                          os.path.join(self.__src_dir, 'b')])

        pages = self.app.project.tree.get_pages()
        self.assertIn('source1.test', pages)

        source1 = self.app.project.tree.get_pages()['source1.test']
        self.assertEqual(source1.symbol_names, ['symbol_a', 'symbol_b'])

        # source2 should not appear in the sitemap
        self.assertNotIn('source2.test', pages)

    def test_relocation_from_source(self):
        sitemap = ('index.markdown\n'
                   '\ttest-index\n')

        symbols = [
            (
                [FunctionSymbol],
                {
                    'unique_name': 'symbol_a',
                    'filename': os.path.join('a', 'source1.test')
                }
            ),
            (
                [FunctionSymbol],
                {
                    'unique_name': 'symbol_b',
                    'filename': os.path.join('b', 'source2.test')
                }
            ),
        ]

        # symbol_b should be documented in page_1
        comments = [
            Comment(name='page_1',
                    filename=os.path.join(self.__src_dir, 'a', 'source1.test'),
                    meta={'sources': [os.path.join(os.pardir, 'b', 'source2.test')]}, toplevel=True),
        ]

        self.__create_test_layout(
            sitemap=sitemap, symbols=symbols, comments=comments,
            output=self.__output_dir)

        pages = self.app.project.tree.get_pages()
        self.assertIn('page_1', pages)

        source1 = self.app.project.tree.get_pages()['page_1']
        self.assertEqual(source1.symbol_names, ['symbol_a', 'symbol_b'])

        # source2 should not appear in the with only its section
        self.assertNotIn(os.path.join('b', 'source2.test'), pages)
        self.assertEqual(len(pages), 3)

    def test_invalid_relocated_source(self):
        sitemap = (u'index.markdown\n'
                   '\ttest-index\n')
        index_path = self.__create_md_file(
            'index.markdown',
            (u'# My documentation\n'))

        a_source_path = 'source_a.test'
        b_source_path = 'source_b.test'

        symbols = [
            (
                [FunctionSymbol],
                {
                    'unique_name': 'symbol_a',
                    'filename': a_source_path,
                }
            ),
        ]

        comments = [
            Comment(name='page_1',
                    meta={'sources': ['does-not-exist.test']},
                    filename=os.path.join(self.__src_dir, b_source_path),
                    toplevel=True),
        ]

        with self.assertRaises(InvalidRelocatedSourceException):
            self.__make_project(sitemap, index_path, symbols=symbols, comments=comments,
                    output=self.__output_dir)

    def test_source_relocated_twice(self):
        sitemap = (u'index.markdown\n'
                   '\ttest-index\n')
        index_path = self.__create_md_file(
            'index.markdown',
            (u'# My documentation\n'))

        a_source_path = 'source_a.test'
        b_source_path = 'source_b.test'
        c_source_path = 'source_c.test'

        symbols = [
            (
                [FunctionSymbol],
                {
                    'unique_name': 'symbol_a',
                    'filename': a_source_path,
                }
            ),
        ]

        comments = [
            Comment(name='page_1',
                    meta={'sources': ['source_a.test']},
                    filename=os.path.join(self.__src_dir, b_source_path),
                    toplevel=True),
            Comment(name='page_1',
                    meta={'sources': ['source_a.test']},
                    filename=os.path.join(self.__src_dir, c_source_path),
                    toplevel=True),
        ]


        with self.assertRaises(InvalidRelocatedSourceException):