Source code for sphinx.application

# -*- coding: utf-8 -*-

    Sphinx application object.

    Gracefully adapted from the TextPress system by Armin.

    :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
from __future__ import print_function

import os
import sys
import types
import posixpath
import traceback
from os import path
from collections import deque

from six import iteritems, itervalues, text_type
from six.moves import cStringIO
from docutils import nodes
from docutils.parsers.rst import convert_directive_function, \
    directives, roles

import sphinx
from sphinx import package_dir, locale
from sphinx.roles import XRefRole
from sphinx.config import Config
from sphinx.errors import SphinxError, SphinxWarning, ExtensionError, \
    VersionRequirementError, ConfigError
from import ObjType, BUILTIN_DOMAINS
from import GenericObject, Target, StandardDomain
from sphinx.environment import BuildEnvironment
from import SphinxStandaloneReader
from sphinx.util import pycompat  # noqa: F401
from sphinx.util import import_object
from sphinx.util.tags import Tags
from sphinx.util.osutil import ENOENT
from sphinx.util.logging import is_suppressed_warning
from sphinx.util.console import bold, lightgray, darkgray, darkgreen, \
from sphinx.util.i18n import find_catalog_source_files

if hasattr(sys, 'intern'):
    intern = sys.intern

# List of all known core events. Maps name to arguments description.
events = {
    'builder-inited': '',
    'env-get-outdated': 'env, added, changed, removed',
    'env-purge-doc': 'env, docname',
    'env-before-read-docs': 'env, docnames',
    'source-read': 'docname, source text',
    'doctree-read': 'the doctree before being pickled',
    'env-merge-info': 'env, read docnames, other env instance',
    'missing-reference': 'env, node, contnode',
    'doctree-resolved': 'doctree, docname',
    'env-updated': 'env',
    'html-collect-pages': 'builder',
    'html-page-context': 'pagename, context, doctree or None',
    'build-finished': 'exception',

ENV_PICKLE_FILENAME = 'environment.pickle'

# list of deprecated extensions. Keys are extension name.
# Values are Sphinx version that merge the extension.
EXTENSION_BLACKLIST = {"sphinxjp.themecore": "1.2"}

[docs]class Sphinx(object): def __init__(self, srcdir, confdir, outdir, doctreedir, buildername, confoverrides=None, status=sys.stdout, warning=sys.stderr, freshenv=False, warningiserror=False, tags=None, verbosity=0, parallel=0): self.verbosity = verbosity self.next_listener_id = 0 self._extensions = {} self._extension_metadata = {} self._additional_source_parsers = {} self._listeners = {} self._setting_up_extension = ['?'] = BUILTIN_DOMAINS.copy() self.buildername = buildername self.builderclasses = BUILTIN_BUILDERS.copy() self.builder = None self.env = None self.enumerable_nodes = {} self.srcdir = srcdir self.confdir = confdir self.outdir = outdir self.doctreedir = doctreedir self.parallel = parallel if status is None: self._status = cStringIO() self.quiet = True else: self._status = status self.quiet = False if warning is None: self._warning = cStringIO() else: self._warning = warning self._warncount = 0 self.warningiserror = warningiserror self._events = events.copy() self._translators = {} # keep last few messages for traceback self.messagelog = deque(maxlen=10) # say hello to the world'Running Sphinx v%s' % sphinx.__display_version__)) # status code for command-line application self.statuscode = 0 if not path.isdir(outdir):'making output directory...') os.makedirs(outdir) # read config self.tags = Tags(tags) self.config = Config(confdir, CONFIG_FILENAME, confoverrides or {}, self.tags) self.config.check_unicode(self.warn) # defer checking types until i18n has been initialized # initialize some limited config variables before loading extensions self.config.pre_init_values(self.warn) # check the Sphinx version if requested if self.config.needs_sphinx and self.config.needs_sphinx > sphinx.__display_version__: raise VersionRequirementError( 'This project needs at least Sphinx v%s and therefore cannot ' 'be built with this version.' % self.config.needs_sphinx) # set confdir to srcdir if -C given (!= no confdir); a few pieces # of code expect a confdir to be set if self.confdir is None: self.confdir = self.srcdir # extension loading support for alabaster theme # self.config.html_theme is not set from at here # for now, sphinx always load a 'alabaster' extension. if 'alabaster' not in self.config.extensions: self.config.extensions.append('alabaster') # load all user-given extension modules for extension in self.config.extensions: self.setup_extension(extension) # the config file itself can be an extension if self.config.setup: self._setting_up_extension = [''] # py31 doesn't have 'callable' function for below check if hasattr(self.config.setup, '__call__'): self.config.setup(self) else: raise ConfigError( "'setup' that is specified in the has not been " + "callable. Please provide a callable `setup` function " + "in order to behave as a sphinx extension itself." ) # now that we know all config values, collect them from self.config.init_values(self.warn) # check extension versions if requested if self.config.needs_extensions: for extname, needs_ver in self.config.needs_extensions.items(): if extname not in self._extensions: self.warn('needs_extensions config value specifies a ' 'version requirement for extension %s, but it is ' 'not loaded' % extname) continue has_ver = self._extension_metadata[extname]['version'] if has_ver == 'unknown version' or needs_ver > has_ver: raise VersionRequirementError( 'This project needs the extension %s at least in ' 'version %s and therefore cannot be built with the ' 'loaded version (%s).' % (extname, needs_ver, has_ver)) # set up translation infrastructure self._init_i18n() # check all configuration values for permissible types self.config.check_types(self.warn) # set up source_parsers self._init_source_parsers() # set up the build environment self._init_env(freshenv) # set up the builder self._init_builder(self.buildername) # set up the enumerable nodes self._init_enumerable_nodes() def _init_i18n(self): """Load translated strings from the configured localedirs if enabled in the configuration. """ if self.config.language is not None:'loading translations [%s]... ' % self.config.language), nonl=True) user_locale_dirs = [ path.join(self.srcdir, x) for x in self.config.locale_dirs] # compile mo files if sphinx.po file in user locale directories are updated for catinfo in find_catalog_source_files( user_locale_dirs, self.config.language, domains=['sphinx'], charset=self.config.source_encoding): catinfo.write_mo(self.config.language) locale_dirs = [None, path.join(package_dir, 'locale')] + user_locale_dirs else: locale_dirs = [] self.translator, has_translation = locale.init(locale_dirs, self.config.language) if self.config.language is not None: if has_translation or self.config.language == 'en': # "en" never needs to be translated'done') else:'not available for built-in messages') def _init_source_parsers(self): for suffix, parser in iteritems(self._additional_source_parsers): if suffix not in self.config.source_suffix: self.config.source_suffix.append(suffix) if suffix not in self.config.source_parsers: self.config.source_parsers[suffix] = parser def _init_env(self, freshenv): if freshenv: self.env = BuildEnvironment(self.srcdir, self.doctreedir, self.config) self.env.set_warnfunc(self.warn) self.env.find_files(self.config) for domain in[domain] =[domain](self.env) else: try:'loading pickled environment... '), nonl=True) self.env = BuildEnvironment.frompickle( self.srcdir, self.config, path.join(self.doctreedir, ENV_PICKLE_FILENAME)) self.env.set_warnfunc(self.warn) = {} for domain in # this can raise if the data version doesn't fit[domain] =[domain](self.env)'done') except Exception as err: if isinstance(err, IOError) and err.errno == ENOENT:'not yet created') else:'failed: %s' % err) return self._init_env(freshenv=True) def _init_builder(self, buildername): if buildername is None: print('No builder selected, using default: html', file=self._status) buildername = 'html' if buildername not in self.builderclasses: raise SphinxError('Builder name %s not registered' % buildername) builderclass = self.builderclasses[buildername] if isinstance(builderclass, tuple): # builtin builder mod, cls = builderclass builderclass = getattr( __import__('' + mod, None, None, [cls]), cls) self.builder = builderclass(self) self.emit('builder-inited') def _init_enumerable_nodes(self): for node, settings in iteritems(self.enumerable_nodes):['std'].enumerable_nodes[node] = settings # ---- main "build" method ------------------------------------------------- def build(self, force_all=False, filenames=None): try: if force_all: self.builder.compile_all_catalogs() self.builder.build_all() elif filenames: self.builder.compile_specific_catalogs(filenames) self.builder.build_specific(filenames) else: self.builder.compile_update_catalogs() self.builder.build_update() status = (self.statuscode == 0 and 'succeeded' or 'finished with problems') if self._warncount:'build %s, %s warning%s.' % (status, self._warncount, self._warncount != 1 and 's' or ''))) else:'build %s.' % status)) except Exception as err: # delete the saved env to force a fresh build next time envfile = path.join(self.doctreedir, ENV_PICKLE_FILENAME) if path.isfile(envfile): os.unlink(envfile) self.emit('build-finished', err) raise else: self.emit('build-finished', None) self.builder.cleanup() # ---- logging handling ---------------------------------------------------- def _log(self, message, wfile, nonl=False): try: wfile.write(message) except UnicodeEncodeError: encoding = getattr(wfile, 'encoding', 'ascii') or 'ascii' wfile.write(message.encode(encoding, 'replace')) if not nonl: wfile.write('\n') if hasattr(wfile, 'flush'): wfile.flush() self.messagelog.append(message)
[docs] def warn(self, message, location=None, prefix='WARNING: ', type=None, subtype=None): """Emit a warning. If *location* is given, it should either be a tuple of (docname, lineno) or a string describing the location of the warning as well as possible. *prefix* usually should not be changed. *type* and *subtype* are used to suppress warnings with :confval:`suppress_warnings`. .. note:: For warnings emitted during parsing, you should use :meth:`.BuildEnvironment.warn` since that will collect all warnings during parsing for later output. """ if is_suppressed_warning(type, subtype, self.config.suppress_warnings): return if isinstance(location, tuple): docname, lineno = location if docname: location = '%s:%s' % (self.env.doc2path(docname), lineno or '') else: location = None warntext = location and '%s: %s%s\n' % (location, prefix, message) or \ '%s%s\n' % (prefix, message) if self.warningiserror: raise SphinxWarning(warntext) self._warncount += 1 self._log(warntext, self._warning, True)
[docs] def info(self, message='', nonl=False): """Emit an informational message. If *nonl* is true, don't emit a newline at the end (which implies that more info output will follow soon.) """ self._log(message, self._status, nonl)
[docs] def verbose(self, message, *args, **kwargs): """Emit a verbose informational message. The message will only be emitted for verbosity levels >= 1 (i.e. at least one ``-v`` option was given). The message can contain %-style interpolation placeholders, which is formatted with either the ``*args`` or ``**kwargs`` when output. """ if self.verbosity < 1: return if args or kwargs: message = message % (args or kwargs) self._log(message, self._status)
[docs] def debug(self, message, *args, **kwargs): """Emit a debug-level informational message. The message will only be emitted for verbosity levels >= 2 (i.e. at least two ``-v`` options were given). The message can contain %-style interpolation placeholders, which is formatted with either the ``*args`` or ``**kwargs`` when output. """ if self.verbosity < 2: return if args or kwargs: message = message % (args or kwargs) self._log(darkgray(message), self._status)
[docs] def debug2(self, message, *args, **kwargs): """Emit a lowlevel debug-level informational message. The message will only be emitted for verbosity level 3 (i.e. three ``-v`` options were given). The message can contain %-style interpolation placeholders, which is formatted with either the ``*args`` or ``**kwargs`` when output. """ if self.verbosity < 3: return if args or kwargs: message = message % (args or kwargs) self._log(lightgray(message), self._status)
def _display_chunk(chunk): if isinstance(chunk, (list, tuple)): if len(chunk) == 1: return text_type(chunk[0]) return '%s .. %s' % (chunk[0], chunk[-1]) return text_type(chunk) def old_status_iterator(self, iterable, summary, colorfunc=darkgreen, stringify_func=_display_chunk): l = 0 for item in iterable: if l == 0:, nonl=1) l = 1 + ' ', nonl=1) yield item if l == 1: # new version with progress info def status_iterator(self, iterable, summary, colorfunc=darkgreen, length=0, stringify_func=_display_chunk): if length == 0: for item in self.old_status_iterator(iterable, summary, colorfunc, stringify_func): yield item return l = 0 summary = bold(summary) for item in iterable: l += 1 s = '%s[%3d%%] %s' % (summary, 100*l/length, colorfunc(stringify_func(item))) if self.verbosity: s += '\n' else: s = term_width_line(s), nonl=1) yield item if l > 0: # ---- general extensibility interface -------------------------------------
[docs] def setup_extension(self, extension): """Import and setup a Sphinx extension module. No-op if called twice.""" self.debug('[app] setting up extension: %r', extension) if extension in self._extensions: return if extension in EXTENSION_BLACKLIST: self.warn('the extension %r was already merged with Sphinx since version %s; ' 'this extension is ignored.' % ( extension, EXTENSION_BLACKLIST[extension])) return self._setting_up_extension.append(extension) try: mod = __import__(extension, None, None, ['setup']) except ImportError as err: self.verbose('Original exception:\n' + traceback.format_exc()) raise ExtensionError('Could not import extension %s' % extension, err) if not hasattr(mod, 'setup'): self.warn('extension %r has no setup() function; is it really ' 'a Sphinx extension module?' % extension) ext_meta = None else: try: ext_meta = mod.setup(self) except VersionRequirementError as err: # add the extension name to the version required raise VersionRequirementError( 'The %s extension used by this project needs at least ' 'Sphinx v%s; it therefore cannot be built with this ' 'version.' % (extension, err)) if ext_meta is None: ext_meta = {} # special-case for compatibility if extension == 'rst2pdf.pdfbuilder': ext_meta = {'parallel_read_safe': True} try: if not ext_meta.get('version'): ext_meta['version'] = 'unknown version' except Exception: self.warn('extension %r returned an unsupported object from ' 'its setup() function; it should return None or a ' 'metadata dictionary' % extension) ext_meta = {'version': 'unknown version'} self._extensions[extension] = mod self._extension_metadata[extension] = ext_meta self._setting_up_extension.pop()
[docs] def require_sphinx(self, version): # check the Sphinx version if requested if version > sphinx.__display_version__[:3]: raise VersionRequirementError(version)
def import_object(self, objname, source=None): """Import an object from a '' string.""" return import_object(objname, source=None) # event interface def _validate_event(self, event): event = intern(event) if event not in self._events: raise ExtensionError('Unknown event name: %s' % event)
[docs] def connect(self, event, callback): self._validate_event(event) listener_id = self.next_listener_id if event not in self._listeners: self._listeners[event] = {listener_id: callback} else: self._listeners[event][listener_id] = callback self.next_listener_id += 1 self.debug('[app] connecting event %r: %r [id=%s]', event, callback, listener_id) return listener_id
[docs] def disconnect(self, listener_id): self.debug('[app] disconnecting event: [id=%s]', listener_id) for event in itervalues(self._listeners): event.pop(listener_id, None)
[docs] def emit(self, event, *args): try: self.debug2('[app] emitting event: %r%s', event, repr(args)[:100]) except Exception: # not every object likes to be repr()'d (think # random stuff coming via autodoc) pass results = [] if event in self._listeners: for _, callback in iteritems(self._listeners[event]): results.append(callback(self, *args)) return results
[docs] def emit_firstresult(self, event, *args): for result in self.emit(event, *args): if result is not None: return result return None
# registering addon parts
[docs] def add_builder(self, builder): self.debug('[app] adding builder: %r', builder) if not hasattr(builder, 'name'): raise ExtensionError('Builder class %s has no "name" attribute' % builder) if in self.builderclasses: if isinstance(self.builderclasses[], tuple): raise ExtensionError('Builder %r is a builtin builder' % else: raise ExtensionError( 'Builder %r already exists (in module %s)' % (, self.builderclasses[].__module__)) self.builderclasses[] = builder
[docs] def add_config_value(self, name, default, rebuild, types=()): self.debug('[app] adding config value: %r', (name, default, rebuild) + ((types,) if types else ())) if name in self.config.values: raise ExtensionError('Config value %r already present' % name) if rebuild in (False, True): rebuild = rebuild and 'env' or '' self.config.values[name] = (default, rebuild, types)
[docs] def add_event(self, name): self.debug('[app] adding event: %r', name) if name in self._events: raise ExtensionError('Event %r already present' % name) self._events[name] = ''
[docs] def set_translator(self, name, translator_class):'A Translator for the %s builder is changed.' % name)) self._translators[name] = translator_class
[docs] def add_node(self, node, **kwds): self.debug('[app] adding node: %r', (node, kwds)) if not kwds.pop('override', False) and \ hasattr(nodes.GenericNodeVisitor, 'visit_' + node.__name__): self.warn('while setting up extension %s: node class %r is ' 'already registered, its visitors will be overridden' % (self._setting_up_extension, node.__name__), type='app', subtype='add_node') nodes._add_node_class_names([node.__name__]) for key, val in iteritems(kwds): try: visit, depart = val except ValueError: raise ExtensionError('Value for key %r must be a ' '(visit, depart) function tuple' % key) translator = self._translators.get(key) if translator is not None: pass elif key == 'html': from sphinx.writers.html import HTMLTranslator as translator elif key == 'latex': from sphinx.writers.latex import LaTeXTranslator as translator elif key == 'text': from sphinx.writers.text import TextTranslator as translator elif key == 'man': from sphinx.writers.manpage import ManualPageTranslator \ as translator elif key == 'texinfo': from sphinx.writers.texinfo import TexinfoTranslator \ as translator else: # ignore invalid keys for compatibility continue setattr(translator, 'visit_'+node.__name__, visit) if depart: setattr(translator, 'depart_'+node.__name__, depart)
[docs] def add_enumerable_node(self, node, figtype, title_getter=None, **kwds): self.enumerable_nodes[node] = (figtype, title_getter) self.add_node(node, **kwds)
def _directive_helper(self, obj, content=None, arguments=None, **options): if isinstance(obj, (types.FunctionType, types.MethodType)): obj.content = content obj.arguments = arguments or (0, 0, False) obj.options = options return convert_directive_function(obj) else: if content or arguments or options: raise ExtensionError('when adding directive classes, no ' 'additional arguments may be given') return obj
[docs] def add_directive(self, name, obj, content=None, arguments=None, **options): self.debug('[app] adding directive: %r', (name, obj, content, arguments, options)) if name in directives._directives: self.warn('while setting up extension %s: directive %r is ' 'already registered, it will be overridden' % (self._setting_up_extension[-1], name), type='app', subtype='add_directive') directives.register_directive( name, self._directive_helper(obj, content, arguments, **options))
[docs] def add_role(self, name, role): self.debug('[app] adding role: %r', (name, role)) if name in roles._roles: self.warn('while setting up extension %s: role %r is ' 'already registered, it will be overridden' % (self._setting_up_extension[-1], name), type='app', subtype='add_role') roles.register_local_role(name, role)
[docs] def add_generic_role(self, name, nodeclass): # don't use roles.register_generic_role because it uses # register_canonical_role self.debug('[app] adding generic role: %r', (name, nodeclass)) if name in roles._roles: self.warn('while setting up extension %s: role %r is ' 'already registered, it will be overridden' % (self._setting_up_extension[-1], name), type='app', subtype='add_generic_role') role = roles.GenericRole(name, nodeclass) roles.register_local_role(name, role)
[docs] def add_domain(self, domain): self.debug('[app] adding domain: %r', domain) if in raise ExtensionError('domain %s already registered' %[] = domain
[docs] def override_domain(self, domain): self.debug('[app] overriding domain: %r', domain) if not in raise ExtensionError('domain %s not yet registered' % if not issubclass(domain,[]): raise ExtensionError('new domain not a subclass of registered %s ' 'domain' %[] = domain
[docs] def add_directive_to_domain(self, domain, name, obj, content=None, arguments=None, **options): self.debug('[app] adding directive to domain: %r', (domain, name, obj, content, arguments, options)) if domain not in raise ExtensionError('domain %s not yet registered' % domain)[domain].directives[name] = \ self._directive_helper(obj, content, arguments, **options)
[docs] def add_role_to_domain(self, domain, name, role): self.debug('[app] adding role to domain: %r', (domain, name, role)) if domain not in raise ExtensionError('domain %s not yet registered' % domain)[domain].roles[name] = role
[docs] def add_index_to_domain(self, domain, index): self.debug('[app] adding index to domain: %r', (domain, index)) if domain not in raise ExtensionError('domain %s not yet registered' % domain)[domain].indices.append(index)
[docs] def add_object_type(self, directivename, rolename, indextemplate='', parse_node=None, ref_nodeclass=None, objname='', doc_field_types=[]): self.debug('[app] adding object type: %r', (directivename, rolename, indextemplate, parse_node, ref_nodeclass, objname, doc_field_types)) StandardDomain.object_types[directivename] = \ ObjType(objname or directivename, rolename) # create a subclass of GenericObject as the new directive new_directive = type(directivename, (GenericObject, object), {'indextemplate': indextemplate, 'parse_node': staticmethod(parse_node), 'doc_field_types': doc_field_types}) StandardDomain.directives[directivename] = new_directive # XXX support more options? StandardDomain.roles[rolename] = XRefRole(innernodeclass=ref_nodeclass)
# backwards compatible alias add_description_unit = add_object_type
[docs] def add_crossref_type(self, directivename, rolename, indextemplate='', ref_nodeclass=None, objname=''): self.debug('[app] adding crossref type: %r', (directivename, rolename, indextemplate, ref_nodeclass, objname)) StandardDomain.object_types[directivename] = \ ObjType(objname or directivename, rolename) # create a subclass of Target as the new directive new_directive = type(directivename, (Target, object), {'indextemplate': indextemplate}) StandardDomain.directives[directivename] = new_directive # XXX support more options? StandardDomain.roles[rolename] = XRefRole(innernodeclass=ref_nodeclass)
[docs] def add_transform(self, transform): self.debug('[app] adding transform: %r', transform) SphinxStandaloneReader.transforms.append(transform)
[docs] def add_javascript(self, filename): self.debug('[app] adding javascript: %r', filename) from import StandaloneHTMLBuilder if '://' in filename: StandaloneHTMLBuilder.script_files.append(filename) else: StandaloneHTMLBuilder.script_files.append( posixpath.join('_static', filename))
[docs] def add_stylesheet(self, filename): self.debug('[app] adding stylesheet: %r', filename) from import StandaloneHTMLBuilder if '://' in filename: StandaloneHTMLBuilder.css_files.append(filename) else: StandaloneHTMLBuilder.css_files.append( posixpath.join('_static', filename))
[docs] def add_latex_package(self, packagename, options=None): self.debug('[app] adding latex package: %r', packagename) from import LaTeXBuilder LaTeXBuilder.usepackages.append((packagename, options))
[docs] def add_lexer(self, alias, lexer): self.debug('[app] adding lexer: %r', (alias, lexer)) from sphinx.highlighting import lexers if lexers is None: return lexers[alias] = lexer
[docs] def add_autodocumenter(self, cls): self.debug('[app] adding autodocumenter: %r', cls) from sphinx.ext import autodoc autodoc.add_documenter(cls) self.add_directive('auto' + cls.objtype, autodoc.AutoDirective)
[docs] def add_autodoc_attrgetter(self, type, getter): self.debug('[app] adding autodoc attrgetter: %r', (type, getter)) from sphinx.ext import autodoc autodoc.AutoDirective._special_attrgetters[type] = getter
[docs] def add_search_language(self, cls): self.debug('[app] adding search language: %r', cls) from import languages, SearchLanguage assert issubclass(cls, SearchLanguage) languages[cls.lang] = cls
[docs] def add_source_parser(self, suffix, parser): self.debug('[app] adding search source_parser: %r, %r', (suffix, parser)) if suffix in self._additional_source_parsers: self.warn('while setting up extension %s: source_parser for %r is ' 'already registered, it will be overridden' % (self._setting_up_extension[-1], suffix), type='app', subtype='add_source_parser') self._additional_source_parsers[suffix] = parser
[docs]class TemplateBridge(object): """ This class defines the interface for a "template bridge", that is, a class that renders templates given a template name and a context. """
[docs] def init(self, builder, theme=None, dirs=None): """Called by the builder to initialize the template system. *builder* is the builder object; you'll probably want to look at the value of ``builder.config.templates_path``. *theme* is a :class:`sphinx.theming.Theme` object or None; in the latter case, *dirs* can be list of fixed directories to look for templates. """ raise NotImplementedError('must be implemented in subclasses')
[docs] def newest_template_mtime(self): """Called by the builder to determine if output files are outdated because of template changes. Return the mtime of the newest template file that was changed. The default implementation returns ``0``. """ return 0
[docs] def render(self, template, context): """Called by the builder to render a template given as a filename with a specified context (a Python dictionary). """ raise NotImplementedError('must be implemented in subclasses')
[docs] def render_string(self, template, context): """Called by the builder to render a template given as a string with a specified context (a Python dictionary). """ raise NotImplementedError('must be implemented in subclasses')