apetest.report module

Gathers and presents checker results in a report.

If a page was loaded, the results of checking it can be stored in a Report instance. If a page fails to load, the problems in trying to fetch it can be stored in a FetchFailure report.

Reports are logging.LoggerAdapter implementations, so you can call the usual info, warning and error logging methods on them to store checker results.

Scribe collects reports for multiple pages and can generate a combined report from them.

Source code
# SPDX-License-Identifier: BSD-3-Clause

"""Gathers and presents checker results in a report.

If a page was loaded, the results of checking it can be stored
in a `Report` instance. If a page fails to load, the problems
in trying to fetch it can be stored in a `FetchFailure` report.

Reports are `logging.LoggerAdapter` implementations, so you can
call the usual `info`, `warning` and `error` logging methods on
them to store checker results.

`Scribe` collects reports for multiple pages and can generate
a combined report from them.
"""

from collections import defaultdict
import logging
from urllib.parse import unquote_plus, urlsplit

from apetest.request import Request
from apetest.xmlgen import raw, xml

_STYLE_SHEET = raw('''
body {
    margin: 0;
    padding: 0;
    background-color: #FFFFFF;
    color: black;
    font-family: vera, arial, sans-serif;
}
a {
    color: black;
}
h1, h2 {
    border-top: 1px solid #808080;
    border-bottom: 1px solid #808080;
}
h1 {
    margin: 0 0 12pt 0;
    padding: 3pt 12pt;
    background-color: #E0E0E0;
}
h2 {
    padding: 2pt 12pt;
    background-color: #F0F0F0;
}
h3, p, dl {
    padding: 1pt 12pt;
}
h3.pass {
    background-color: #90FF90;
}
h3.fail {
    background-color: #FF9090;
}
li {
    line-height: 125%;
}
li.error {
    list-style-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 10 10" version="1.1"><path fill="%23e00" d="M0,5 a5,5 0 0 0 10,0 5,5 0 0 0 -10,0 Z M5,3.75 6.75,2 8,3.25 6.25,5 8,6.75 6.75,8 5,6.25 3.25,8 2,6.75 3.75,5 2,3.25 3.35,2 Z"/></svg>');
}
li.warning {
    list-style-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 10 10" version="1.1"><path fill="%23f70" d="M1,10 h8 a1,1 0 0 0 0.866,-1.5 l-4,-7 a1,1 0 0 0 -1.732,0 l-4,7 a1,1 0 0 0 0.866,1.5 Z M4.5,3 h1 v4 h-1 Z M5,7.75 a0.75,0.75 0 0 1 0,1.5 0.75,0.75 0 0 1 0,-1.5 Z"/></svg>');
}
li.info {
    list-style-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 10 10" version="1.1"><path fill="%2333f" d="M0,5 a5,5 0 0 0 10,0 5,5 0 0 0 -10,0 Z M5,1.25 a1,1 0 0 1 0,2 1,1 0 0 1 0,-2 Z M3.5,4.25 h2 v3 h1 v1 h-3 v-1 h1 v-2 h-1 Z"/></svg>');
}
span.extract {
    background: #FFFFB0;
}
code {
    background: #F0F0F0;
    color: #000000;
}
''')

class StoreHandler(logging.Handler):
    """A log handler that stores all logged records in a list.

    Used internally to store messages logged to reports.

    Log records handled by this handler must have a `url` property
    that contains the URL that the record applies to.
    """

    def __init__(self):
        logging.Handler.__init__(self)
        self.records = defaultdict(list)
        """Maps a URL to a collection of reports for that URL."""

    def emit(self, record):
        """Store a log record in our `records`."""
        self.format(record)
        self.records[record.url].append(record)

_LOG = logging.getLogger(__name__)
_LOG.setLevel(logging.INFO)
_HANDLER = StoreHandler()
_LOG.addHandler(_HANDLER)
_LOG.propagate = False

class Report(logging.LoggerAdapter):
    """Gathers check results for a document produced by one request."""

    def __init__(self, url):
        """Initialize a report that will be collecting results
        for the document at `url`.
        """
        logging.LoggerAdapter.__init__(self, _LOG, dict(url=url))

        self.url = url
        """The request URL to which this report applies."""

        self.ok = True # pylint: disable=invalid-name
        """`True` iff no warnings or errors were reported.

        This is initialized to `True` and will be set to `False`
        when a message with a level higher than `logging.INFO`
        (such as a warning or error) is logged on this report.
        """

        self.checked = False
        """`True` iff the content of the document has been checked.

        This is initialized to `False`. A checker should set it to
        `True` when it has checked the document.
        """

    def log(self, level, msg, *args, **kwargs):
        if level > logging.INFO:
            self.ok = False
        super().log(level, msg, *args, **kwargs)

    def process(self, msg, kwargs):
        """Process contextual information for a logged message.

        Our `url` will be inserted into the log record.
        """

        extra = kwargs.get('extra')
        if extra is None:
            extra = self.extra
        else:
            extra.update(self.extra)
        kwargs['extra'] = extra

        return msg, kwargs

    def present(self, scribe):
        """Yield an XHTML rendering of this report."""

        present_record = self.present_record
        yield xml.ul[(
            present_record(record)
            for record in _HANDLER.records[self.url]
            )]

        if not self.checked:
            yield xml.p['No content checks were performed']
        if not self.ok:
            yield xml.p['Referenced by:']
            # TODO: Store Request object instead of recreating it.
            request = Request.from_url(self.url)
            yield xml.ul[
                # pylint: disable=protected-access
                scribe._present_referrers(request)
                ]

    @staticmethod
    def present_record(record):
        """Return an XHTML rendering of one log record."""

        level = record.levelname.lower()
        html = getattr(record, 'html', record.message)
        return xml.li(class_=level)[html]

class FetchFailure(Report, Exception):
    """Records the details of a request that failed.

    This is an `Exception`, so it can be raised instead of returned,
    where that is appropriate.
    """

    def __init__(self, url, message, http_error=None):
        """Initialize the report and log `message` as an error.
        """
        Report.__init__(self, url)
        Exception.__init__(self, message)

        self.http_error = http_error
        """Optional `urllib.error.HTTPError` that caused this fetch
        failure.
        """

        self.error('Failed to fetch: %s', message)

class Page:
    """Information collected by `Scribe` about a single page.

    A page is identified by a URL minus query.
    """

    def __init__(self):
        """Initialize page with no reports."""

        self.query_to_report = {}
        """Maps a query string to the report for that query."""

        self.failures = 0
        """Number of reports that contain warnings or errors."""

    def add_report(self, report):
        """Add `Report` for this page.

        For each unique query, only one report can be added.
        Reports should only be added once final: after all checks
        for them are done.
        """

        scheme_, host_, path_, query, fragment_ = urlsplit(report.url)
        assert query not in self.query_to_report
        self.query_to_report[query] = report
        if not report.ok:
            self.failures += 1

    def present(self, scribe):
        """Yield an XHTML rendering of all reports for this page."""

        # Use more compact presentation for local files.
        if len(self.query_to_report) == 1:
            (query, report), = self.query_to_report.items()
            if query == '' and report.url.startswith('file:'):
                verdict = 'pass' if report.ok else 'fail'
                yield xml.h3(class_=verdict)[verdict]
                yield report.present(scribe)
                return

        # Use detailed presentation for pages served over HTTP.
        total = len(self.query_to_report)
        yield xml.p[
            '%d queries checked, %d passed, %d failed'
            % (total, total - self.failures, self.failures)
            ]
        for query, report in sorted(self.query_to_report.items()):
            yield xml.h3(class_='pass' if report.ok else 'fail')[
                xml.a(href=report.url)[
                    ' | '.join(
                        '%s = %s' % tuple(
                            unquote_plus(s) for s in elem.split('=')
                            )
                        for elem in query.split('&')
                        ) if query else '(no query)'
                    ]
                ]
            yield report.present(scribe)

class Scribe:
    """Collects reports for multiple pages."""

    def __init__(self, base_url, spider, plugins):
        """Initialize scribe.

        Parameters:

        base_url
            Page URL at the base of the app or site that is being checked.
            The root URL will be computed from this by dropping the path
            element after the last directory level, if any.
        spider
            `apetest.spider.Spider` instance from which links between pages
            can be looked up.
        plugins
            Collection of `apetest.plugin.Plugin` instances that will receive
            notifications from this scribe.
        """
        scheme_, host_, base_path, query, fragment = urlsplit(base_url)
        assert query == ''
        assert fragment == ''
        # HTTP requires empty URL path to be mapped to "/".
        #   https://tools.ietf.org/html/rfc7230#section-5.3.1
        base_path = base_path or '/'
        self._base_path = base_path = base_path[ : base_path.rindex('/') + 1]

        self._spider = spider
        self._plugins = plugins
        self._pages = defaultdict(Page)

    def __url_to_name(self, url):
        path = urlsplit(url).path or '/'
        assert path.startswith(self._base_path)
        return path[len(self._base_path) : ]

    def add_report(self, report):
        """Add a `Report` to this scribe.

        Plugins are notified of the new report.
        """
        self._plugins.report_added(report)

        url = report.url
        page = self._pages[self.__url_to_name(url)]
        page.add_report(report)

    def get_pages(self):
        """Return a collection of `Page` objects describing the pages
        for which reports were added to this scribe.
        """
        return self._pages.values()

    def get_failed_pages(self):
        """Like `Scribe.get_pages`, but only pages for which warnings
        or error were reported are returned.
        """
        return [
            page for page in self._pages.values() if page.failures != 0
            ]

    def get_summary(self):
        """Return a short string summarizing the check results."""
        total = len(self._pages)
        num_failed_pages = len(self.get_failed_pages())
        return '%d pages checked, %d passed, %d failed' % (
            total, total - num_failed_pages, num_failed_pages
            )

    def postprocess(self):
        """Instruct the plugins to do their final processing."""
        self._plugins.postprocess(self)

    def present(self):
        """Yield an XHTML rendering of a combined report for all
        checked pages.
        """
        title = 'APE - Automated Page Exerciser'
        yield xml.html[
            xml.head[
                xml.title[title],
                xml.style(type='text/css')[_STYLE_SHEET]
                ],
            xml.body[
                xml.h1[title],
                xml.p[self.get_summary()],
                self._present_failed_index(),
                ((xml.h2[xml.a(name=name or 'base')[name or '(base)']],
                  page.present(self))
                 for name, page in sorted(self._pages.items()))
                ]
            ]

    def _present_failed_index(self):
        failed_page_names = [
            name for name, page in self._pages.items() if page.failures != 0
            ]
        if failed_page_names:
            yield xml.p['Failed pages:']
            yield xml.ul[(
                xml.li[
                    xml.a(href='#' + (name or 'base'))[name or '(base)']
                    ]
                for name in sorted(failed_page_names)
                )]

    def _present_referrers(self, req):
        # Note: Currently we only list the pages a request is referred from,
        #       but we know the exact requests.
        page_names = set(
            self.__url_to_name(source_req.page_url)
            for source_req in self._spider.iter_referring_requests(req)
            )
        for name in sorted(page_names):
            yield xml.li[xml.a(href='#' + (name or 'base'))[name or '(base)']]}

Classes

class FetchFailure (ancestors: Report, logging.LoggerAdapter, builtins.Exception, builtins.BaseException)

Records the details of a request that failed.

This is an Exception, so it can be raised instead of returned, where that is appropriate.

Source code
class FetchFailure(Report, Exception):
    """Records the details of a request that failed.

    This is an `Exception`, so it can be raised instead of returned,
    where that is appropriate.
    """

    def __init__(self, url, message, http_error=None):
        """Initialize the report and log `message` as an error.
        """
        Report.__init__(self, url)
        Exception.__init__(self, message)

        self.http_error = http_error
        """Optional `urllib.error.HTTPError` that caused this fetch
        failure.
        """

        self.error('Failed to fetch: %s', message)}

Instance variables

var http_error

Optional urllib.error.HTTPError that caused this fetch failure.

Methods

def __init__(self, url, message, http_error=None)

Initialize the report and log message as an error.

Source code
def __init__(self, url, message, http_error=None):
    """Initialize the report and log `message` as an error.
    """
    Report.__init__(self, url)
    Exception.__init__(self, message)

    self.http_error = http_error
    """Optional `urllib.error.HTTPError` that caused this fetch
    failure.
    """

    self.error('Failed to fetch: %s', message)}

Inherited members

class Page

Information collected by Scribe about a single page.

A page is identified by a URL minus query.

Source code
class Page:
    """Information collected by `Scribe` about a single page.

    A page is identified by a URL minus query.
    """

    def __init__(self):
        """Initialize page with no reports."""

        self.query_to_report = {}
        """Maps a query string to the report for that query."""

        self.failures = 0
        """Number of reports that contain warnings or errors."""

    def add_report(self, report):
        """Add `Report` for this page.

        For each unique query, only one report can be added.
        Reports should only be added once final: after all checks
        for them are done.
        """

        scheme_, host_, path_, query, fragment_ = urlsplit(report.url)
        assert query not in self.query_to_report
        self.query_to_report[query] = report
        if not report.ok:
            self.failures += 1

    def present(self, scribe):
        """Yield an XHTML rendering of all reports for this page."""

        # Use more compact presentation for local files.
        if len(self.query_to_report) == 1:
            (query, report), = self.query_to_report.items()
            if query == '' and report.url.startswith('file:'):
                verdict = 'pass' if report.ok else 'fail'
                yield xml.h3(class_=verdict)[verdict]
                yield report.present(scribe)
                return

        # Use detailed presentation for pages served over HTTP.
        total = len(self.query_to_report)
        yield xml.p[
            '%d queries checked, %d passed, %d failed'
            % (total, total - self.failures, self.failures)
            ]
        for query, report in sorted(self.query_to_report.items()):
            yield xml.h3(class_='pass' if report.ok else 'fail')[
                xml.a(href=report.url)[
                    ' | '.join(
                        '%s = %s' % tuple(
                            unquote_plus(s) for s in elem.split('=')
                            )
                        for elem in query.split('&')
                        ) if query else '(no query)'
                    ]
                ]
            yield report.present(scribe)}

Instance variables

var failures

Number of reports that contain warnings or errors.

var query_to_report

Maps a query string to the report for that query.

Methods

def __init__(self)

Initialize page with no reports.

Source code
def __init__(self):
    """Initialize page with no reports."""

    self.query_to_report = {}
    """Maps a query string to the report for that query."""

    self.failures = 0
    """Number of reports that contain warnings or errors."""}
def add_report(self, report)

Add Report for this page.

For each unique query, only one report can be added. Reports should only be added once final: after all checks for them are done.

Source code
def add_report(self, report):
    """Add `Report` for this page.

    For each unique query, only one report can be added.
    Reports should only be added once final: after all checks
    for them are done.
    """

    scheme_, host_, path_, query, fragment_ = urlsplit(report.url)
    assert query not in self.query_to_report
    self.query_to_report[query] = report
    if not report.ok:
        self.failures += 1}
def present(self, scribe)

Yield an XHTML rendering of all reports for this page.

Source code
def present(self, scribe):
    """Yield an XHTML rendering of all reports for this page."""

    # Use more compact presentation for local files.
    if len(self.query_to_report) == 1:
        (query, report), = self.query_to_report.items()
        if query == '' and report.url.startswith('file:'):
            verdict = 'pass' if report.ok else 'fail'
            yield xml.h3(class_=verdict)[verdict]
            yield report.present(scribe)
            return

    # Use detailed presentation for pages served over HTTP.
    total = len(self.query_to_report)
    yield xml.p[
        '%d queries checked, %d passed, %d failed'
        % (total, total - self.failures, self.failures)
        ]
    for query, report in sorted(self.query_to_report.items()):
        yield xml.h3(class_='pass' if report.ok else 'fail')[
            xml.a(href=report.url)[
                ' | '.join(
                    '%s = %s' % tuple(
                        unquote_plus(s) for s in elem.split('=')
                        )
                    for elem in query.split('&')
                    ) if query else '(no query)'
                ]
            ]
        yield report.present(scribe)}
class Report (ancestors: logging.LoggerAdapter)

Gathers check results for a document produced by one request.

Source code
class Report(logging.LoggerAdapter):
    """Gathers check results for a document produced by one request."""

    def __init__(self, url):
        """Initialize a report that will be collecting results
        for the document at `url`.
        """
        logging.LoggerAdapter.__init__(self, _LOG, dict(url=url))

        self.url = url
        """The request URL to which this report applies."""

        self.ok = True # pylint: disable=invalid-name
        """`True` iff no warnings or errors were reported.

        This is initialized to `True` and will be set to `False`
        when a message with a level higher than `logging.INFO`
        (such as a warning or error) is logged on this report.
        """

        self.checked = False
        """`True` iff the content of the document has been checked.

        This is initialized to `False`. A checker should set it to
        `True` when it has checked the document.
        """

    def log(self, level, msg, *args, **kwargs):
        if level > logging.INFO:
            self.ok = False
        super().log(level, msg, *args, **kwargs)

    def process(self, msg, kwargs):
        """Process contextual information for a logged message.

        Our `url` will be inserted into the log record.
        """

        extra = kwargs.get('extra')
        if extra is None:
            extra = self.extra
        else:
            extra.update(self.extra)
        kwargs['extra'] = extra

        return msg, kwargs

    def present(self, scribe):
        """Yield an XHTML rendering of this report."""

        present_record = self.present_record
        yield xml.ul[(
            present_record(record)
            for record in _HANDLER.records[self.url]
            )]

        if not self.checked:
            yield xml.p['No content checks were performed']
        if not self.ok:
            yield xml.p['Referenced by:']
            # TODO: Store Request object instead of recreating it.
            request = Request.from_url(self.url)
            yield xml.ul[
                # pylint: disable=protected-access
                scribe._present_referrers(request)
                ]

    @staticmethod
    def present_record(record):
        """Return an XHTML rendering of one log record."""

        level = record.levelname.lower()
        html = getattr(record, 'html', record.message)
        return xml.li(class_=level)[html]}

Subclasses

Static methods

def present_record(record)

Return an XHTML rendering of one log record.

Source code
@staticmethod
def present_record(record):
    """Return an XHTML rendering of one log record."""

    level = record.levelname.lower()
    html = getattr(record, 'html', record.message)
    return xml.li(class_=level)[html]}

Instance variables

var checked

True iff the content of the document has been checked.

This is initialized to False. A checker should set it to True when it has checked the document.

var ok

True iff no warnings or errors were reported.

This is initialized to True and will be set to False when a message with a level higher than logging.INFO (such as a warning or error) is logged on this report.

var url

The request URL to which this report applies.

Methods

def __init__(self, url)

Initialize a report that will be collecting results for the document at url.

Source code
def __init__(self, url):
    """Initialize a report that will be collecting results
    for the document at `url`.
    """
    logging.LoggerAdapter.__init__(self, _LOG, dict(url=url))

    self.url = url
    """The request URL to which this report applies."""

    self.ok = True # pylint: disable=invalid-name
    """`True` iff no warnings or errors were reported.

    This is initialized to `True` and will be set to `False`
    when a message with a level higher than `logging.INFO`
    (such as a warning or error) is logged on this report.
    """

    self.checked = False
    """`True` iff the content of the document has been checked.

    This is initialized to `False`. A checker should set it to
    `True` when it has checked the document.
    """}
def log(self, level, msg, *args, **kwargs)

Delegate a log call to the underlying logger, after adding contextual information from this adapter instance.

Source code
def log(self, level, msg, *args, **kwargs):
    if level > logging.INFO:
        self.ok = False
    super().log(level, msg, *args, **kwargs)}
def present(self, scribe)

Yield an XHTML rendering of this report.

Source code
def present(self, scribe):
    """Yield an XHTML rendering of this report."""

    present_record = self.present_record
    yield xml.ul[(
        present_record(record)
        for record in _HANDLER.records[self.url]
        )]

    if not self.checked:
        yield xml.p['No content checks were performed']
    if not self.ok:
        yield xml.p['Referenced by:']
        # TODO: Store Request object instead of recreating it.
        request = Request.from_url(self.url)
        yield xml.ul[
            # pylint: disable=protected-access
            scribe._present_referrers(request)
            ]}
def process(self, msg, kwargs)

Process contextual information for a logged message.

Our url will be inserted into the log record.

Source code
def process(self, msg, kwargs):
    """Process contextual information for a logged message.

    Our `url` will be inserted into the log record.
    """

    extra = kwargs.get('extra')
    if extra is None:
        extra = self.extra
    else:
        extra.update(self.extra)
    kwargs['extra'] = extra

    return msg, kwargs}
class Scribe

Collects reports for multiple pages.

Source code
class Scribe:
    """Collects reports for multiple pages."""

    def __init__(self, base_url, spider, plugins):
        """Initialize scribe.

        Parameters:

        base_url
            Page URL at the base of the app or site that is being checked.
            The root URL will be computed from this by dropping the path
            element after the last directory level, if any.
        spider
            `apetest.spider.Spider` instance from which links between pages
            can be looked up.
        plugins
            Collection of `apetest.plugin.Plugin` instances that will receive
            notifications from this scribe.
        """
        scheme_, host_, base_path, query, fragment = urlsplit(base_url)
        assert query == ''
        assert fragment == ''
        # HTTP requires empty URL path to be mapped to "/".
        #   https://tools.ietf.org/html/rfc7230#section-5.3.1
        base_path = base_path or '/'
        self._base_path = base_path = base_path[ : base_path.rindex('/') + 1]

        self._spider = spider
        self._plugins = plugins
        self._pages = defaultdict(Page)

    def __url_to_name(self, url):
        path = urlsplit(url).path or '/'
        assert path.startswith(self._base_path)
        return path[len(self._base_path) : ]

    def add_report(self, report):
        """Add a `Report` to this scribe.

        Plugins are notified of the new report.
        """
        self._plugins.report_added(report)

        url = report.url
        page = self._pages[self.__url_to_name(url)]
        page.add_report(report)

    def get_pages(self):
        """Return a collection of `Page` objects describing the pages
        for which reports were added to this scribe.
        """
        return self._pages.values()

    def get_failed_pages(self):
        """Like `Scribe.get_pages`, but only pages for which warnings
        or error were reported are returned.
        """
        return [
            page for page in self._pages.values() if page.failures != 0
            ]

    def get_summary(self):
        """Return a short string summarizing the check results."""
        total = len(self._pages)
        num_failed_pages = len(self.get_failed_pages())
        return '%d pages checked, %d passed, %d failed' % (
            total, total - num_failed_pages, num_failed_pages
            )

    def postprocess(self):
        """Instruct the plugins to do their final processing."""
        self._plugins.postprocess(self)

    def present(self):
        """Yield an XHTML rendering of a combined report for all
        checked pages.
        """
        title = 'APE - Automated Page Exerciser'
        yield xml.html[
            xml.head[
                xml.title[title],
                xml.style(type='text/css')[_STYLE_SHEET]
                ],
            xml.body[
                xml.h1[title],
                xml.p[self.get_summary()],
                self._present_failed_index(),
                ((xml.h2[xml.a(name=name or 'base')[name or '(base)']],
                  page.present(self))
                 for name, page in sorted(self._pages.items()))
                ]
            ]

    def _present_failed_index(self):
        failed_page_names = [
            name for name, page in self._pages.items() if page.failures != 0
            ]
        if failed_page_names:
            yield xml.p['Failed pages:']
            yield xml.ul[(
                xml.li[
                    xml.a(href='#' + (name or 'base'))[name or '(base)']
                    ]
                for name in sorted(failed_page_names)
                )]

    def _present_referrers(self, req):
        # Note: Currently we only list the pages a request is referred from,
        #       but we know the exact requests.
        page_names = set(
            self.__url_to_name(source_req.page_url)
            for source_req in self._spider.iter_referring_requests(req)
            )
        for name in sorted(page_names):
            yield xml.li[xml.a(href='#' + (name or 'base'))[name or '(base)']]}

Methods

def __init__(self, base_url, spider, plugins)

Initialize scribe.

Parameters

base_url
Page URL at the base of the app or site that is being checked. The root URL will be computed from this by dropping the path element after the last directory level, if any.
spider
Spider instance from which links between pages can be looked up.
plugins
Collection of Plugin instances that will receive notifications from this scribe.
Source code
def __init__(self, base_url, spider, plugins):
    """Initialize scribe.

    Parameters:

    base_url
        Page URL at the base of the app or site that is being checked.
        The root URL will be computed from this by dropping the path
        element after the last directory level, if any.
    spider
        `apetest.spider.Spider` instance from which links between pages
        can be looked up.
    plugins
        Collection of `apetest.plugin.Plugin` instances that will receive
        notifications from this scribe.
    """
    scheme_, host_, base_path, query, fragment = urlsplit(base_url)
    assert query == ''
    assert fragment == ''
    # HTTP requires empty URL path to be mapped to "/".
    #   https://tools.ietf.org/html/rfc7230#section-5.3.1
    base_path = base_path or '/'
    self._base_path = base_path = base_path[ : base_path.rindex('/') + 1]

    self._spider = spider
    self._plugins = plugins
    self._pages = defaultdict(Page)}
def add_report(self, report)

Add a Report to this scribe.

Plugins are notified of the new report.

Source code
def add_report(self, report):
    """Add a `Report` to this scribe.

    Plugins are notified of the new report.
    """
    self._plugins.report_added(report)

    url = report.url
    page = self._pages[self.__url_to_name(url)]
    page.add_report(report)}
def get_failed_pages(self)

Like Scribe.get_pages(), but only pages for which warnings or error were reported are returned.

Source code
def get_failed_pages(self):
    """Like `Scribe.get_pages`, but only pages for which warnings
    or error were reported are returned.
    """
    return [
        page for page in self._pages.values() if page.failures != 0
        ]}
def get_pages(self)

Return a collection of Page objects describing the pages for which reports were added to this scribe.

Source code
def get_pages(self):
    """Return a collection of `Page` objects describing the pages
    for which reports were added to this scribe.
    """
    return self._pages.values()}
def get_summary(self)

Return a short string summarizing the check results.

Source code
def get_summary(self):
    """Return a short string summarizing the check results."""
    total = len(self._pages)
    num_failed_pages = len(self.get_failed_pages())
    return '%d pages checked, %d passed, %d failed' % (
        total, total - num_failed_pages, num_failed_pages
        )}
def postprocess(self)

Instruct the plugins to do their final processing.

Source code
def postprocess(self):
    """Instruct the plugins to do their final processing."""
    self._plugins.postprocess(self)}
def present(self)

Yield an XHTML rendering of a combined report for all checked pages.

Source code
def present(self):
    """Yield an XHTML rendering of a combined report for all
    checked pages.
    """
    title = 'APE - Automated Page Exerciser'
    yield xml.html[
        xml.head[
            xml.title[title],
            xml.style(type='text/css')[_STYLE_SHEET]
            ],
        xml.body[
            xml.h1[title],
            xml.p[self.get_summary()],
            self._present_failed_index(),
            ((xml.h2[xml.a(name=name or 'base')[name or '(base)']],
              page.present(self))
             for name, page in sorted(self._pages.items()))
            ]
        ]}
class StoreHandler (ancestors: logging.Handler, logging.Filterer)

A log handler that stores all logged records in a list.

Used internally to store messages logged to reports.

Log records handled by this handler must have a url property that contains the URL that the record applies to.

Source code
class StoreHandler(logging.Handler):
    """A log handler that stores all logged records in a list.

    Used internally to store messages logged to reports.

    Log records handled by this handler must have a `url` property
    that contains the URL that the record applies to.
    """

    def __init__(self):
        logging.Handler.__init__(self)
        self.records = defaultdict(list)
        """Maps a URL to a collection of reports for that URL."""

    def emit(self, record):
        """Store a log record in our `records`."""
        self.format(record)
        self.records[record.url].append(record)}

Instance variables

var records

Maps a URL to a collection of reports for that URL.

Methods

def __init__(self)

Initializes the instance - basically setting the formatter to None and the filter list to empty.

Source code
def __init__(self):
    logging.Handler.__init__(self)
    self.records = defaultdict(list)
    """Maps a URL to a collection of reports for that URL."""}
def emit(self, record)

Store a log record in our records.

Source code
def emit(self, record):
    """Store a log record in our `records`."""
    self.format(record)
    self.records[record.url].append(record)}