mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Support the image directive.
This commit is contained in:
parent
649ce723c1
commit
ba47f283f6
3
CHANGES
3
CHANGES
@ -1,6 +1,9 @@
|
|||||||
Changes in trunk
|
Changes in trunk
|
||||||
================
|
================
|
||||||
|
|
||||||
|
* sphinx.htmlwriter, sphinx.latexwriter: Support the ``.. image::``
|
||||||
|
directive by copying image files to the output directory.
|
||||||
|
|
||||||
* sphinx.environment: Take dependent files into account when collecting
|
* sphinx.environment: Take dependent files into account when collecting
|
||||||
the set of outdated sources.
|
the set of outdated sources.
|
||||||
|
|
||||||
|
13
doc/rest.rst
13
doc/rest.rst
@ -205,6 +205,19 @@ The directive content follows after a blank line and is indented relative to the
|
|||||||
directive start.
|
directive start.
|
||||||
|
|
||||||
|
|
||||||
|
Images
|
||||||
|
------
|
||||||
|
|
||||||
|
reST supports an image directive, used like so::
|
||||||
|
|
||||||
|
.. image:: filename
|
||||||
|
(options)
|
||||||
|
|
||||||
|
When used within Sphinx, the ``filename`` given must be relative to the source
|
||||||
|
file, and Sphinx will automatically copy image files over to a subdirectory of
|
||||||
|
the output directory on building.
|
||||||
|
|
||||||
|
|
||||||
Footnotes
|
Footnotes
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ class Builder(object):
|
|||||||
|
|
||||||
def init_templates(self):
|
def init_templates(self):
|
||||||
"""Call if you need Jinja templates in the builder."""
|
"""Call if you need Jinja templates in the builder."""
|
||||||
# lazily import this, maybe other builders won't need it
|
# lazily import this, other builders won't need it
|
||||||
from sphinx._jinja import Environment, SphinxFileSystemLoader
|
from sphinx._jinja import Environment, SphinxFileSystemLoader
|
||||||
|
|
||||||
# load templates
|
# load templates
|
||||||
@ -103,14 +103,18 @@ class Builder(object):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_relative_uri(self, from_, to, typ=None):
|
def get_relative_uri(self, from_, to, typ=None):
|
||||||
"""Return a relative URI between two source filenames.
|
"""
|
||||||
May raise environment.NoUri if there's no way to return a
|
Return a relative URI between two source filenames. May raise environment.NoUri
|
||||||
sensible URI."""
|
if there's no way to return a sensible URI.
|
||||||
|
"""
|
||||||
return relative_uri(self.get_target_uri(from_),
|
return relative_uri(self.get_target_uri(from_),
|
||||||
self.get_target_uri(to, typ))
|
self.get_target_uri(to, typ))
|
||||||
|
|
||||||
def get_outdated_docs(self):
|
def get_outdated_docs(self):
|
||||||
"""Return a list of output files that are outdated."""
|
"""
|
||||||
|
Return an iterable of output files that are outdated, or a string describing
|
||||||
|
what an update build will build.
|
||||||
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def status_iterator(self, iterable, summary, colorfunc):
|
def status_iterator(self, iterable, summary, colorfunc):
|
||||||
@ -173,7 +177,7 @@ class Builder(object):
|
|||||||
"""Only rebuild files changed or added since last build."""
|
"""Only rebuild files changed or added since last build."""
|
||||||
to_build = self.get_outdated_docs()
|
to_build = self.get_outdated_docs()
|
||||||
if isinstance(to_build, str):
|
if isinstance(to_build, str):
|
||||||
self.build([], to_build)
|
self.build(['__all__'], to_build)
|
||||||
else:
|
else:
|
||||||
to_build = list(to_build)
|
to_build = list(to_build)
|
||||||
self.build(to_build,
|
self.build(to_build,
|
||||||
@ -211,7 +215,7 @@ class Builder(object):
|
|||||||
self.info(bold('checking consistency...'))
|
self.info(bold('checking consistency...'))
|
||||||
self.env.check_consistency()
|
self.env.check_consistency()
|
||||||
else:
|
else:
|
||||||
if not docnames:
|
if method == 'update' and not docnames:
|
||||||
self.info(bold('no targets are out of date.'))
|
self.info(bold('no targets are out of date.'))
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -341,6 +345,7 @@ class StandaloneHTMLBuilder(Builder):
|
|||||||
destination = StringOutput(encoding='utf-8')
|
destination = StringOutput(encoding='utf-8')
|
||||||
doctree.settings = self.docsettings
|
doctree.settings = self.docsettings
|
||||||
|
|
||||||
|
self.imgpath = relative_uri(self.get_target_uri(docname), '_images')
|
||||||
self.docwriter.write(doctree, destination)
|
self.docwriter.write(doctree, destination)
|
||||||
self.docwriter.assemble_parts()
|
self.docwriter.assemble_parts()
|
||||||
|
|
||||||
@ -474,8 +479,19 @@ class StandaloneHTMLBuilder(Builder):
|
|||||||
self.info(' index', nonl=1)
|
self.info(' index', nonl=1)
|
||||||
self.handle_page('index', {'indextemplate': indextemplate}, 'index.html')
|
self.handle_page('index', {'indextemplate': indextemplate}, 'index.html')
|
||||||
|
|
||||||
# copy static files
|
|
||||||
self.info()
|
self.info()
|
||||||
|
|
||||||
|
# copy image files
|
||||||
|
if self.env.images:
|
||||||
|
self.info(bold('copying images...'), nonl=1)
|
||||||
|
ensuredir(path.join(self.outdir, '_images'))
|
||||||
|
for src, dest in self.env.images.iteritems():
|
||||||
|
self.info(' '+src, nonl=1)
|
||||||
|
shutil.copyfile(path.join(self.srcdir, src),
|
||||||
|
path.join(self.outdir, '_images', dest))
|
||||||
|
self.info()
|
||||||
|
|
||||||
|
# copy static files
|
||||||
self.info(bold('copying static files...'))
|
self.info(bold('copying static files...'))
|
||||||
ensuredir(path.join(self.outdir, 'static'))
|
ensuredir(path.join(self.outdir, 'static'))
|
||||||
staticdirnames = [path.join(path.dirname(__file__), 'static')] + \
|
staticdirnames = [path.join(path.dirname(__file__), 'static')] + \
|
||||||
@ -796,6 +812,15 @@ class LaTeXBuilder(Builder):
|
|||||||
return largetree
|
return largetree
|
||||||
|
|
||||||
def finish(self):
|
def finish(self):
|
||||||
|
# copy image files
|
||||||
|
if self.env.images:
|
||||||
|
self.info(bold('copying images...'), nonl=1)
|
||||||
|
for src, dest in self.env.images.iteritems():
|
||||||
|
self.info(' '+src, nonl=1)
|
||||||
|
shutil.copyfile(path.join(self.srcdir, src),
|
||||||
|
path.join(self.outdir, dest))
|
||||||
|
self.info()
|
||||||
|
|
||||||
self.info(bold('copying TeX support files...'))
|
self.info(bold('copying TeX support files...'))
|
||||||
staticdirname = path.join(path.dirname(__file__), 'texinputs')
|
staticdirname = path.join(path.dirname(__file__), 'texinputs')
|
||||||
for filename in os.listdir(staticdirname):
|
for filename in os.listdir(staticdirname):
|
||||||
|
@ -352,7 +352,7 @@ def desc_directive(desctype, arguments, options, content, lineno,
|
|||||||
signode['ids'].append(fullname)
|
signode['ids'].append(fullname)
|
||||||
signode['first'] = (not names)
|
signode['first'] = (not names)
|
||||||
state.document.note_explicit_target(signode)
|
state.document.note_explicit_target(signode)
|
||||||
env.note_descref(fullname, desctype)
|
env.note_descref(fullname, desctype, lineno)
|
||||||
names.append(name)
|
names.append(name)
|
||||||
|
|
||||||
env.note_index_entry('single',
|
env.note_index_entry('single',
|
||||||
|
@ -57,7 +57,7 @@ default_settings = {
|
|||||||
|
|
||||||
# This is increased every time a new environment attribute is added
|
# This is increased every time a new environment attribute is added
|
||||||
# to properly invalidate pickle files.
|
# to properly invalidate pickle files.
|
||||||
ENV_VERSION = 19
|
ENV_VERSION = 20
|
||||||
|
|
||||||
|
|
||||||
def walk_depth(node, depth, maxdepth):
|
def walk_depth(node, depth, maxdepth):
|
||||||
@ -251,6 +251,7 @@ class BuildEnvironment:
|
|||||||
# (type, string, target, aliasname)
|
# (type, string, target, aliasname)
|
||||||
self.versionchanges = {} # version -> list of
|
self.versionchanges = {} # version -> list of
|
||||||
# (type, docname, lineno, module, descname, content)
|
# (type, docname, lineno, module, descname, content)
|
||||||
|
self.images = {} # absolute path -> unique filename
|
||||||
|
|
||||||
# These are set while parsing a file
|
# These are set while parsing a file
|
||||||
self.docname = None # current document name
|
self.docname = None # current document name
|
||||||
@ -269,9 +270,11 @@ class BuildEnvironment:
|
|||||||
self._warnfunc = func
|
self._warnfunc = func
|
||||||
self.settings['warning_stream'] = RedirStream(func)
|
self.settings['warning_stream'] = RedirStream(func)
|
||||||
|
|
||||||
def warn(self, docname, msg):
|
def warn(self, docname, msg, lineno=None):
|
||||||
if docname:
|
if docname:
|
||||||
self._warnfunc(self.doc2path(docname) + ':: ' + msg)
|
if lineno is None:
|
||||||
|
lineno = ''
|
||||||
|
self._warnfunc('%s:%s: %s' % (self.doc2path(docname), lineno, msg))
|
||||||
else:
|
else:
|
||||||
self._warnfunc('GLOBAL:: ' + msg)
|
self._warnfunc('GLOBAL:: ' + msg)
|
||||||
|
|
||||||
@ -420,6 +423,12 @@ class BuildEnvironment:
|
|||||||
self.warn(None, 'master file %s not found' %
|
self.warn(None, 'master file %s not found' %
|
||||||
self.doc2path(config.master_doc))
|
self.doc2path(config.master_doc))
|
||||||
|
|
||||||
|
# remove all non-existing images from inventory
|
||||||
|
for imgsrc in self.images.keys():
|
||||||
|
if not os.access(path.join(self.srcdir, imgsrc), os.R_OK):
|
||||||
|
del self.images[imgsrc]
|
||||||
|
|
||||||
|
|
||||||
# --------- SINGLE FILE BUILDING -------------------------------------------
|
# --------- SINGLE FILE BUILDING -------------------------------------------
|
||||||
|
|
||||||
def read_doc(self, docname, src_path=None, save_parsed=True, app=None):
|
def read_doc(self, docname, src_path=None, save_parsed=True, app=None):
|
||||||
@ -436,6 +445,7 @@ class BuildEnvironment:
|
|||||||
settings_overrides=self.settings,
|
settings_overrides=self.settings,
|
||||||
reader=MyStandaloneReader())
|
reader=MyStandaloneReader())
|
||||||
self.process_dependencies(docname, doctree)
|
self.process_dependencies(docname, doctree)
|
||||||
|
self.process_images(docname, doctree)
|
||||||
self.process_metadata(docname, doctree)
|
self.process_metadata(docname, doctree)
|
||||||
self.create_title_from(docname, doctree)
|
self.create_title_from(docname, doctree)
|
||||||
self.note_labels_from(docname, doctree)
|
self.note_labels_from(docname, doctree)
|
||||||
@ -482,11 +492,37 @@ class BuildEnvironment:
|
|||||||
deps = doctree.settings.record_dependencies
|
deps = doctree.settings.record_dependencies
|
||||||
if not deps:
|
if not deps:
|
||||||
return
|
return
|
||||||
basename = path.dirname(self.doc2path(docname, base=None))
|
docdir = path.dirname(self.doc2path(docname, base=None))
|
||||||
for dep in deps.list:
|
for dep in deps.list:
|
||||||
dep = path.join(basename, dep)
|
dep = path.join(docdir, dep)
|
||||||
self.dependencies.setdefault(docname, set()).add(dep)
|
self.dependencies.setdefault(docname, set()).add(dep)
|
||||||
|
|
||||||
|
def process_images(self, docname, doctree):
|
||||||
|
"""
|
||||||
|
Process and rewrite image URIs.
|
||||||
|
"""
|
||||||
|
docdir = path.dirname(self.doc2path(docname, base=None))
|
||||||
|
for node in doctree.traverse(nodes.image):
|
||||||
|
imguri = node['uri']
|
||||||
|
if imguri.find('://') != -1:
|
||||||
|
self.warn(docname, 'Nonlocal image URI found: %s' % imguri, node.line)
|
||||||
|
else:
|
||||||
|
imgpath = path.normpath(path.join(docdir, imguri))
|
||||||
|
node['uri'] = imgpath
|
||||||
|
self.dependencies.setdefault(docname, set()).add(imgpath)
|
||||||
|
if not os.access(path.join(self.srcdir, imgpath), os.R_OK):
|
||||||
|
self.warn(docname, 'Image file not readable: %s' % imguri, node.line)
|
||||||
|
if imgpath in self.images:
|
||||||
|
continue
|
||||||
|
names = set(self.images.values())
|
||||||
|
uniquename = path.basename(imgpath)
|
||||||
|
base, ext = path.splitext(uniquename)
|
||||||
|
i = 0
|
||||||
|
while uniquename in names:
|
||||||
|
i += 1
|
||||||
|
uniquename = '%s%s%s' % (base, i, ext)
|
||||||
|
self.images[imgpath] = uniquename
|
||||||
|
|
||||||
def process_metadata(self, docname, doctree):
|
def process_metadata(self, docname, doctree):
|
||||||
"""
|
"""
|
||||||
Process the docinfo part of the doctree as metadata.
|
Process the docinfo part of the doctree as metadata.
|
||||||
@ -527,6 +563,8 @@ class BuildEnvironment:
|
|||||||
if not explicit:
|
if not explicit:
|
||||||
continue
|
continue
|
||||||
labelid = document.nameids[name]
|
labelid = document.nameids[name]
|
||||||
|
if labelid is None:
|
||||||
|
continue
|
||||||
node = document.ids[labelid]
|
node = document.ids[labelid]
|
||||||
if name.isdigit() or node.has_key('refuri') or \
|
if name.isdigit() or node.has_key('refuri') or \
|
||||||
node.tagname.startswith('desc_'):
|
node.tagname.startswith('desc_'):
|
||||||
@ -535,7 +573,8 @@ class BuildEnvironment:
|
|||||||
continue
|
continue
|
||||||
if name in self.labels:
|
if name in self.labels:
|
||||||
self.warn(docname, 'duplicate label %s, ' % name +
|
self.warn(docname, 'duplicate label %s, ' % name +
|
||||||
'other instance in %s' % self.doc2path(self.labels[name][0]))
|
'other instance in %s' % self.doc2path(self.labels[name][0]),
|
||||||
|
node.line)
|
||||||
self.anonlabels[name] = docname, labelid
|
self.anonlabels[name] = docname, labelid
|
||||||
if not isinstance(node, nodes.section):
|
if not isinstance(node, nodes.section):
|
||||||
# anonymous-only labels
|
# anonymous-only labels
|
||||||
@ -616,11 +655,12 @@ class BuildEnvironment:
|
|||||||
# -------
|
# -------
|
||||||
# these are called from docutils directives and therefore use self.docname
|
# these are called from docutils directives and therefore use self.docname
|
||||||
#
|
#
|
||||||
def note_descref(self, fullname, desctype):
|
def note_descref(self, fullname, desctype, line):
|
||||||
if fullname in self.descrefs:
|
if fullname in self.descrefs:
|
||||||
self.warn(self.docname,
|
self.warn(self.docname,
|
||||||
'duplicate canonical description name %s, ' % fullname +
|
'duplicate canonical description name %s, ' % fullname +
|
||||||
'other instance in %s' % self.doc2path(self.descrefs[fullname][0]))
|
'other instance in %s' % self.doc2path(self.descrefs[fullname][0]),
|
||||||
|
line)
|
||||||
self.descrefs[fullname] = (self.docname, desctype)
|
self.descrefs[fullname] = (self.docname, desctype)
|
||||||
|
|
||||||
def note_module(self, modname, synopsis, platform, deprecated):
|
def note_module(self, modname, synopsis, platform, deprecated):
|
||||||
@ -780,7 +820,8 @@ class BuildEnvironment:
|
|||||||
docname, labelid = self.reftargets.get((typ, target), ('', ''))
|
docname, labelid = self.reftargets.get((typ, target), ('', ''))
|
||||||
if not docname:
|
if not docname:
|
||||||
if typ == 'term':
|
if typ == 'term':
|
||||||
self.warn(fromdocname, 'term not in glossary: %s' % target)
|
self.warn(fromdocname, 'term not in glossary: %s' % target,
|
||||||
|
node.line)
|
||||||
newnode = contnode
|
newnode = contnode
|
||||||
else:
|
else:
|
||||||
newnode = nodes.reference('', '')
|
newnode = nodes.reference('', '')
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
from os import path
|
||||||
|
|
||||||
from docutils import nodes
|
from docutils import nodes
|
||||||
from docutils.writers.html4css1 import Writer, HTMLTranslator as BaseTranslator
|
from docutils.writers.html4css1 import Writer, HTMLTranslator as BaseTranslator
|
||||||
@ -246,6 +247,15 @@ class HTMLTranslator(BaseTranslator):
|
|||||||
def depart_highlightlang(self, node):
|
def depart_highlightlang(self, node):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# overwritten
|
||||||
|
def visit_image(self, node):
|
||||||
|
olduri = node['uri']
|
||||||
|
# rewrite the URI if the environment knows about it
|
||||||
|
if olduri in self.builder.env.images:
|
||||||
|
node['uri'] = path.join(self.builder.imgpath,
|
||||||
|
self.builder.env.images[olduri])
|
||||||
|
BaseTranslator.visit_image(self, node)
|
||||||
|
|
||||||
def visit_toctree(self, node):
|
def visit_toctree(self, node):
|
||||||
# this only happens when formatting a toc from env.tocs -- in this
|
# this only happens when formatting a toc from env.tocs -- in this
|
||||||
# case we don't want to include the subtree
|
# case we don't want to include the subtree
|
||||||
|
@ -40,6 +40,15 @@ FOOTER = r'''
|
|||||||
\end{document}
|
\end{document}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
GRAPHICX = r'''
|
||||||
|
%% Check if we are compiling under latex or pdflatex.
|
||||||
|
\ifx\pdftexversion\undefined
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\else
|
||||||
|
\usepackage[pdftex]{graphicx}
|
||||||
|
\fi
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
class LaTeXWriter(writers.Writer):
|
class LaTeXWriter(writers.Writer):
|
||||||
|
|
||||||
@ -118,11 +127,14 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
|||||||
self.first_document = 1
|
self.first_document = 1
|
||||||
self.this_is_the_title = 1
|
self.this_is_the_title = 1
|
||||||
self.literal_whitespace = 0
|
self.literal_whitespace = 0
|
||||||
|
self.need_graphicx = 0
|
||||||
|
|
||||||
def astext(self):
|
def astext(self):
|
||||||
return (HEADER % self.options) + \
|
return (HEADER % self.options) + \
|
||||||
(self.options['modindex'] and '\\makemodindex\n' or '') + \
|
(self.options['modindex'] and '\\makemodindex\n' or '') + \
|
||||||
self.highlighter.get_stylesheet() + '\n\n' + \
|
self.highlighter.get_stylesheet() + \
|
||||||
|
(self.need_graphicx and GRAPHICX or '') + \
|
||||||
|
'\n\n' + \
|
||||||
u''.join(self.body) + \
|
u''.join(self.body) + \
|
||||||
(self.options['modindex'] and '\\printmodindex\n' or '') + \
|
(self.options['modindex'] and '\\printmodindex\n' or '') + \
|
||||||
(FOOTER % self.options)
|
(FOOTER % self.options)
|
||||||
@ -498,6 +510,49 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
|||||||
def depart_module(self, node):
|
def depart_module(self, node):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def visit_image(self, node):
|
||||||
|
self.need_graphicx = 1
|
||||||
|
attrs = node.attributes
|
||||||
|
pre = [] # in reverse order
|
||||||
|
post = []
|
||||||
|
include_graphics_options = ""
|
||||||
|
inline = isinstance(node.parent, nodes.TextElement)
|
||||||
|
if attrs.has_key('scale'):
|
||||||
|
# Could also be done with ``scale`` option to
|
||||||
|
# ``\includegraphics``; doing it this way for consistency.
|
||||||
|
pre.append('\\scalebox{%f}{' % (attrs['scale'] / 100.0,))
|
||||||
|
post.append('}')
|
||||||
|
if attrs.has_key('width'):
|
||||||
|
include_graphics_options = '[width=%s]' % attrs['width']
|
||||||
|
if attrs.has_key('align'):
|
||||||
|
align_prepost = {
|
||||||
|
# By default latex aligns the top of an image.
|
||||||
|
(1, 'top'): ('', ''),
|
||||||
|
(1, 'middle'): ('\\raisebox{-0.5\\height}{', '}'),
|
||||||
|
(1, 'bottom'): ('\\raisebox{-\\height}{', '}'),
|
||||||
|
(0, 'center'): ('{\\hfill', '\\hfill}'),
|
||||||
|
# These 2 don't exactly do the right thing. The image should
|
||||||
|
# be floated alongside the paragraph. See
|
||||||
|
# http://www.w3.org/TR/html4/struct/objects.html#adef-align-IMG
|
||||||
|
(0, 'left'): ('{', '\\hfill}'),
|
||||||
|
(0, 'right'): ('{\\hfill', '}'),}
|
||||||
|
try:
|
||||||
|
pre.append(align_prepost[inline, attrs['align']][0])
|
||||||
|
post.append(align_prepost[inline, attrs['align']][1])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
if not inline:
|
||||||
|
pre.append('\n')
|
||||||
|
post.append('\n')
|
||||||
|
pre.reverse()
|
||||||
|
self.body.extend(pre)
|
||||||
|
# XXX: for now, don't fiddle around with graphics formats
|
||||||
|
uri = self.builder.env.images.get(node['uri'], node['uri'])
|
||||||
|
self.body.append('\\includegraphics%s{%s}' % (include_graphics_options, uri))
|
||||||
|
self.body.extend(post)
|
||||||
|
def depart_image(self, node):
|
||||||
|
pass
|
||||||
|
|
||||||
def visit_note(self, node):
|
def visit_note(self, node):
|
||||||
self.body.append('\n\\begin{notice}[note]')
|
self.body.append('\n\\begin{notice}[note]')
|
||||||
def depart_note(self, node):
|
def depart_note(self, node):
|
||||||
|
@ -120,6 +120,8 @@ def xfileref_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
|
|||||||
# we want a cross-reference, create the reference node
|
# we want a cross-reference, create the reference node
|
||||||
pnode = addnodes.pending_xref(rawtext, reftype=typ, refcaption=False,
|
pnode = addnodes.pending_xref(rawtext, reftype=typ, refcaption=False,
|
||||||
modname=env.currmodule, classname=env.currclass)
|
modname=env.currmodule, classname=env.currclass)
|
||||||
|
# we may need the line number for warnings
|
||||||
|
pnode.line = lineno
|
||||||
innertext = text
|
innertext = text
|
||||||
# special actions for Python object cross-references
|
# special actions for Python object cross-references
|
||||||
if typ in ('data', 'exc', 'func', 'class', 'const', 'attr', 'meth', 'mod'):
|
if typ in ('data', 'exc', 'func', 'class', 'const', 'attr', 'meth', 'mod'):
|
||||||
|
Loading…
Reference in New Issue
Block a user