Merge branch '2.0' into 6785_attr_can_refer_props

This commit is contained in:
Takeshi KOMIYA 2020-02-06 22:30:50 +09:00 committed by GitHub
commit ecf7307023
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 604 additions and 261 deletions

20
CHANGES
View File

@ -23,6 +23,17 @@ Deprecated
* ``sphinx.util.get_module_source()`` * ``sphinx.util.get_module_source()``
* ``sphinx.util.inspect.Signature`` * ``sphinx.util.inspect.Signature``
* ``sphinx.util.inspect.safe_getmembers()`` * ``sphinx.util.inspect.safe_getmembers()``
* ``sphinx.writers.latex.LaTeXTranslator.settings.author``
* ``sphinx.writers.latex.LaTeXTranslator.settings.contentsname``
* ``sphinx.writers.latex.LaTeXTranslator.settings.docclass``
* ``sphinx.writers.latex.LaTeXTranslator.settings.docname``
* ``sphinx.writers.latex.LaTeXTranslator.settings.title``
* ``sphinx.writers.latex.ADDITIONAL_SETTINGS``
* ``sphinx.writers.latex.DEFAULT_SETTINGS``
* ``sphinx.writers.latex.LUALATEX_DEFAULT_FONTPKG``
* ``sphinx.writers.latex.PDFLATEX_DEFAULT_FONTPKG``
* ``sphinx.writers.latex.XELATEX_DEFAULT_FONTPKG``
* ``sphinx.writers.latex.XELATEX_GREEK_DEFAULT_FONTPKG``
Features added Features added
-------------- --------------
@ -36,6 +47,7 @@ Features added
images (imagesize-1.2.0 or above is required) images (imagesize-1.2.0 or above is required)
* #6994: imgconverter: Support illustrator file (.ai) to .png conversion * #6994: imgconverter: Support illustrator file (.ai) to .png conversion
* autodoc: Support Positional-Only Argument separator (PEP-570 compliant) * autodoc: Support Positional-Only Argument separator (PEP-570 compliant)
* autodoc: Support type annotations for variables
* #2755: autodoc: Add new event: :event:`autodoc-before-process-signature` * #2755: autodoc: Add new event: :event:`autodoc-before-process-signature`
* #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``) * #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``)
annotation (python3.8+ or `typed_ast <https://github.com/python/typed_ast>`_ annotation (python3.8+ or `typed_ast <https://github.com/python/typed_ast>`_
@ -48,7 +60,10 @@ Features added
* SphinxTranslator now calls visitor/departure method for super node class if * SphinxTranslator now calls visitor/departure method for super node class if
visitor/departure method for original node class not found visitor/departure method for original node class not found
* #6418: Add new event: :event:`object-description-transform` * #6418: Add new event: :event:`object-description-transform`
* py domain: :rst:dir:`py:data` and :rst:dir:`py:attribute` take new options
named ``:type:`` and ``:value:`` to describe its type and initial value
* #6785: py domain: ``:py:attr:`` is able to refer properties again * #6785: py domain: ``:py:attr:`` is able to refer properties again
* #6772: apidoc: Add ``-q`` option for quiet mode
Bugs fixed Bugs fixed
---------- ----------
@ -56,13 +71,18 @@ Bugs fixed
* #6925: html: Remove redundant type="text/javascript" from <script> elements * #6925: html: Remove redundant type="text/javascript" from <script> elements
* #6906, #6907: autodoc: failed to read the source codes encoeded in cp1251 * #6906, #6907: autodoc: failed to read the source codes encoeded in cp1251
* #6961: latex: warning for babel shown twice * #6961: latex: warning for babel shown twice
* #7059: latex: LaTeX compilation falls into infinite loop (wrapfig issue)
* #6559: Wrong node-ids are generated in glossary directive * #6559: Wrong node-ids are generated in glossary directive
* #6986: apidoc: misdetects module name for .so file inside module * #6986: apidoc: misdetects module name for .so file inside module
* #6899: apidoc: private members are not shown even if ``--private`` given
* #6999: napoleon: fails to parse tilde in :exc: role * #6999: napoleon: fails to parse tilde in :exc: role
* #7019: gettext: Absolute path used in message catalogs * #7019: gettext: Absolute path used in message catalogs
* #7023: autodoc: nested partial functions are not listed * #7023: autodoc: nested partial functions are not listed
* #7023: autodoc: partial functions imported from other modules are listed as * #7023: autodoc: partial functions imported from other modules are listed as
module members without :impoprted-members: option module members without :impoprted-members: option
* #6889: autodoc: Trailing comma in ``:members::`` option causes cryptic warning
* #7055: linkcheck: redirect is treated as an error
* #7090: std domain: Can't assign numfig-numbers for custom container nodes
Testing Testing
-------- --------

View File

@ -92,6 +92,61 @@ The following is a list of deprecated interfaces.
- 4.0 - 4.0
- ``inspect.getmembers()`` - ``inspect.getmembers()``
* - ``sphinx.writers.latex.LaTeXTranslator.settings.author``
- 2.4
- 4.0
- N/A
* - ``sphinx.writers.latex.LaTeXTranslator.settings.contentsname``
- 2.4
- 4.0
- ``document['contentsname']``
* - ``sphinx.writers.latex.LaTeXTranslator.settings.docclass``
- 2.4
- 4.0
- ``document['docclass']``
* - ``sphinx.writers.latex.LaTeXTranslator.settings.docname``
- 2.4
- 4.0
- N/A
* - ``sphinx.writers.latex.LaTeXTranslator.settings.title``
- 2.4
- 4.0
- N/A
* - ``sphinx.writers.latex.ADDITIONAL_SETTINGS``
- 2.4
- 4.0
- ``sphinx.builders.latex.constants.ADDITIONAL_SETTINGS``
* - ``sphinx.writers.latex.DEFAULT_SETTINGS``
- 2.4
- 4.0
- ``sphinx.builders.latex.constants.DEFAULT_SETTINGS``
* - ``sphinx.writers.latex.LUALATEX_DEFAULT_FONTPKG``
- 2.4
- 4.0
- ``sphinx.builders.latex.constants.LUALATEX_DEFAULT_FONTPKG``
* - ``sphinx.writers.latex.PDFLATEX_DEFAULT_FONTPKG``
- 2.4
- 4.0
- ``sphinx.builders.latex.constants.PDFLATEX_DEFAULT_FONTPKG``
* - ``sphinx.writers.latex.XELATEX_DEFAULT_FONTPKG``
- 2.4
- 4.0
- ``sphinx.builders.latex.constants.XELATEX_DEFAULT_FONTPKG``
* - ``sphinx.writers.latex.XELATEX_GREEK_DEFAULT_FONTPKG``
- 2.4
- 4.0
- ``sphinx.builders.latex.constants.XELATEX_GREEK_DEFAULT_FONTPKG``
* - ``sphinx.builders.gettext.POHEADER`` * - ``sphinx.builders.gettext.POHEADER``
- 2.3 - 2.3
- 4.0 - 4.0

View File

@ -39,6 +39,11 @@ Options
Directory to place the output files. If it does not exist, it is created. Directory to place the output files. If it does not exist, it is created.
.. option:: -q
Do not output anything on standard output, only write warnings and errors to
standard error.
.. option:: -f, --force .. option:: -f, --force
Force overwriting of any existing generated files. Force overwriting of any existing generated files.

View File

@ -195,6 +195,18 @@ The following directives are provided for module and class contents:
as "defined constants." Class and object attributes are not documented as "defined constants." Class and object attributes are not documented
using this environment. using this environment.
.. rubric:: options
.. rst:directive:option:: type: type of the variable
:type: text
.. versionadded:: 2.4
.. rst:directive:option:: value: initial value of the variable
:type: text
.. versionadded:: 2.4
.. rst:directive:: .. py:exception:: name .. rst:directive:: .. py:exception:: name
Describes an exception class. The signature can, but need not include Describes an exception class. The signature can, but need not include
@ -229,6 +241,18 @@ The following directives are provided for module and class contents:
information about the type of the data to be expected and whether it may be information about the type of the data to be expected and whether it may be
changed directly. changed directly.
.. rubric:: options
.. rst:directive:option:: type: type of the attribute
:type: text
.. versionadded:: 2.4
.. rst:directive:option:: value: initial value of the attribute
:type: text
.. versionadded:: 2.4
.. rst:directive:: .. py:method:: name(parameters) .. rst:directive:: .. py:method:: name(parameters)
Describes an object method. The parameters should not include the ``self`` Describes an object method. The parameters should not include the ``self``

View File

@ -20,6 +20,7 @@ import sphinx.builders.latex.nodes # NOQA # Workaround: import this before wri
from sphinx import package_dir, addnodes, highlighting from sphinx import package_dir, addnodes, highlighting
from sphinx.application import Sphinx from sphinx.application import Sphinx
from sphinx.builders import Builder from sphinx.builders import Builder
from sphinx.builders.latex.constants import ADDITIONAL_SETTINGS, DEFAULT_SETTINGS
from sphinx.builders.latex.util import ExtBabel from sphinx.builders.latex.util import ExtBabel
from sphinx.config import Config, ENUM from sphinx.config import Config, ENUM
from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.deprecation import RemovedInSphinx40Warning
@ -34,9 +35,7 @@ from sphinx.util.i18n import format_date
from sphinx.util.nodes import inline_all_toctrees from sphinx.util.nodes import inline_all_toctrees
from sphinx.util.osutil import SEP, make_filename_from_project from sphinx.util.osutil import SEP, make_filename_from_project
from sphinx.util.template import LaTeXRenderer from sphinx.util.template import LaTeXRenderer
from sphinx.writers.latex import ( from sphinx.writers.latex import LaTeXWriter, LaTeXTranslator
ADDITIONAL_SETTINGS, DEFAULT_SETTINGS, LaTeXWriter, LaTeXTranslator
)
# load docutils.nodes after loading sphinx.builders.latex.nodes # load docutils.nodes after loading sphinx.builders.latex.nodes
from docutils import nodes # NOQA from docutils import nodes # NOQA
@ -222,6 +221,7 @@ class LaTeXBuilder(Builder):
defaults=self.env.settings, defaults=self.env.settings,
components=(docwriter,), components=(docwriter,),
read_config_files=True).get_default_values() # type: Any read_config_files=True).get_default_values() # type: Any
patch_settings(docsettings)
self.init_document_data() self.init_document_data()
self.write_stylesheet() self.write_stylesheet()
@ -244,16 +244,18 @@ class LaTeXBuilder(Builder):
doctree = self.assemble_doctree( doctree = self.assemble_doctree(
docname, toctree_only, docname, toctree_only,
appendices=(self.config.latex_appendices if docclass != 'howto' else [])) appendices=(self.config.latex_appendices if docclass != 'howto' else []))
doctree['docclass'] = docclass
doctree['contentsname'] = self.get_contentsname(docname)
doctree['tocdepth'] = tocdepth doctree['tocdepth'] = tocdepth
self.post_process_images(doctree) self.post_process_images(doctree)
self.update_doc_context(title, author) self.update_doc_context(title, author)
with progress_message(__("writing")): with progress_message(__("writing")):
docsettings.author = author docsettings._author = author
docsettings.title = title docsettings._title = title
docsettings.contentsname = self.get_contentsname(docname) docsettings._contentsname = doctree['contentsname']
docsettings.docname = docname docsettings._docname = docname
docsettings.docclass = docclass docsettings._docclass = docclass
doctree.settings = docsettings doctree.settings = docsettings
docwriter.write(doctree, destination) docwriter.write(doctree, destination)
@ -401,6 +403,44 @@ class LaTeXBuilder(Builder):
copy_asset_file(filename, self.outdir, context=context, renderer=LaTeXRenderer()) copy_asset_file(filename, self.outdir, context=context, renderer=LaTeXRenderer())
def patch_settings(settings: Any) -> Any:
"""Make settings object to show deprecation messages."""
class Values(type(settings)): # type: ignore
@property
def author(self):
warnings.warn('settings.author is deprecated',
RemovedInSphinx40Warning, stacklevel=2)
return self._author
@property
def title(self):
warnings.warn('settings.title is deprecated',
RemovedInSphinx40Warning, stacklevel=2)
return self._title
@property
def contentsname(self):
warnings.warn('settings.contentsname is deprecated',
RemovedInSphinx40Warning, stacklevel=2)
return self._contentsname
@property
def docname(self):
warnings.warn('settings.docname is deprecated',
RemovedInSphinx40Warning, stacklevel=2)
return self._docname
@property
def docclass(self):
warnings.warn('settings.docclass is deprecated',
RemovedInSphinx40Warning, stacklevel=2)
return self._docclass
# dynamic subclassing
settings.__class__ = Values
def validate_config_values(app: Sphinx, config: Config) -> None: def validate_config_values(app: Sphinx, config: Config) -> None:
for key in list(config.latex_elements): for key in list(config.latex_elements):
if key not in DEFAULT_SETTINGS: if key not in DEFAULT_SETTINGS:

View File

@ -0,0 +1,192 @@
"""
sphinx.builders.latex.constants
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
consntants for LaTeX builder.
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from typing import Any, Dict
PDFLATEX_DEFAULT_FONTPKG = r'''
\usepackage{times}
\expandafter\ifx\csname T@LGR\endcsname\relax
\else
% LGR was declared as font encoding
\substitutefont{LGR}{\rmdefault}{cmr}
\substitutefont{LGR}{\sfdefault}{cmss}
\substitutefont{LGR}{\ttdefault}{cmtt}
\fi
\expandafter\ifx\csname T@X2\endcsname\relax
\expandafter\ifx\csname T@T2A\endcsname\relax
\else
% T2A was declared as font encoding
\substitutefont{T2A}{\rmdefault}{cmr}
\substitutefont{T2A}{\sfdefault}{cmss}
\substitutefont{T2A}{\ttdefault}{cmtt}
\fi
\else
% X2 was declared as font encoding
\substitutefont{X2}{\rmdefault}{cmr}
\substitutefont{X2}{\sfdefault}{cmss}
\substitutefont{X2}{\ttdefault}{cmtt}
\fi
'''
XELATEX_DEFAULT_FONTPKG = r'''
\setmainfont{FreeSerif}[
Extension = .otf,
UprightFont = *,
ItalicFont = *Italic,
BoldFont = *Bold,
BoldItalicFont = *BoldItalic
]
\setsansfont{FreeSans}[
Extension = .otf,
UprightFont = *,
ItalicFont = *Oblique,
BoldFont = *Bold,
BoldItalicFont = *BoldOblique,
]
\setmonofont{FreeMono}[
Extension = .otf,
UprightFont = *,
ItalicFont = *Oblique,
BoldFont = *Bold,
BoldItalicFont = *BoldOblique,
]
'''
XELATEX_GREEK_DEFAULT_FONTPKG = (XELATEX_DEFAULT_FONTPKG +
'\n\\newfontfamily\\greekfont{FreeSerif}' +
'\n\\newfontfamily\\greekfontsf{FreeSans}' +
'\n\\newfontfamily\\greekfonttt{FreeMono}')
LUALATEX_DEFAULT_FONTPKG = XELATEX_DEFAULT_FONTPKG
DEFAULT_SETTINGS = {
'latex_engine': 'pdflatex',
'papersize': 'letterpaper',
'pointsize': '10pt',
'pxunit': '.75bp',
'classoptions': '',
'extraclassoptions': '',
'maxlistdepth': '',
'sphinxpkgoptions': '',
'sphinxsetup': '',
'fvset': '\\fvset{fontsize=\\small}',
'passoptionstopackages': '',
'geometry': '\\usepackage{geometry}',
'inputenc': '',
'utf8extra': '',
'cmappkg': '\\usepackage{cmap}',
'fontenc': '\\usepackage[T1]{fontenc}',
'amsmath': '\\usepackage{amsmath,amssymb,amstext}',
'multilingual': '',
'babel': '\\usepackage{babel}',
'polyglossia': '',
'fontpkg': PDFLATEX_DEFAULT_FONTPKG,
'substitutefont': '',
'textcyrillic': '',
'textgreek': '\\usepackage{textalpha}',
'fncychap': '\\usepackage[Bjarne]{fncychap}',
'hyperref': ('% Include hyperref last.\n'
'\\usepackage{hyperref}\n'
'% Fix anchor placement for figures with captions.\n'
'\\usepackage{hypcap}% it must be loaded after hyperref.\n'
'% Set up styles of URL: it should be placed after hyperref.\n'
'\\urlstyle{same}'),
'contentsname': '',
'extrapackages': '',
'preamble': '',
'title': '',
'release': '',
'author': '',
'releasename': '',
'makeindex': '\\makeindex',
'shorthandoff': '',
'maketitle': '\\sphinxmaketitle',
'tableofcontents': '\\sphinxtableofcontents',
'atendofbody': '',
'printindex': '\\printindex',
'transition': '\n\n\\bigskip\\hrule\\bigskip\n\n',
'figure_align': 'htbp',
'tocdepth': '',
'secnumdepth': '',
} # type: Dict[str, Any]
ADDITIONAL_SETTINGS = {
'pdflatex': {
'inputenc': '\\usepackage[utf8]{inputenc}',
'utf8extra': ('\\ifdefined\\DeclareUnicodeCharacter\n'
'% support both utf8 and utf8x syntaxes\n'
' \\ifdefined\\DeclareUnicodeCharacterAsOptional\n'
' \\def\\sphinxDUC#1{\\DeclareUnicodeCharacter{"#1}}\n'
' \\else\n'
' \\let\\sphinxDUC\\DeclareUnicodeCharacter\n'
' \\fi\n'
' \\sphinxDUC{00A0}{\\nobreakspace}\n'
' \\sphinxDUC{2500}{\\sphinxunichar{2500}}\n'
' \\sphinxDUC{2502}{\\sphinxunichar{2502}}\n'
' \\sphinxDUC{2514}{\\sphinxunichar{2514}}\n'
' \\sphinxDUC{251C}{\\sphinxunichar{251C}}\n'
' \\sphinxDUC{2572}{\\textbackslash}\n'
'\\fi'),
},
'xelatex': {
'latex_engine': 'xelatex',
'polyglossia': '\\usepackage{polyglossia}',
'babel': '',
'fontenc': ('\\usepackage{fontspec}\n'
'\\defaultfontfeatures[\\rmfamily,\\sffamily,\\ttfamily]{}'),
'fontpkg': XELATEX_DEFAULT_FONTPKG,
'textgreek': '',
'utf8extra': ('\\catcode`^^^^00a0\\active\\protected\\def^^^^00a0'
'{\\leavevmode\\nobreak\\ }'),
},
'lualatex': {
'latex_engine': 'lualatex',
'polyglossia': '\\usepackage{polyglossia}',
'babel': '',
'fontenc': ('\\usepackage{fontspec}\n'
'\\defaultfontfeatures[\\rmfamily,\\sffamily,\\ttfamily]{}'),
'fontpkg': LUALATEX_DEFAULT_FONTPKG,
'textgreek': '',
'utf8extra': ('\\catcode`^^^^00a0\\active\\protected\\def^^^^00a0'
'{\\leavevmode\\nobreak\\ }'),
},
'platex': {
'latex_engine': 'platex',
'babel': '',
'classoptions': ',dvipdfmx',
'fontpkg': '\\usepackage{times}',
'textgreek': '',
'fncychap': '',
'geometry': '\\usepackage[dvipdfm]{geometry}',
},
'uplatex': {
'latex_engine': 'uplatex',
'babel': '',
'classoptions': ',dvipdfmx',
'fontpkg': '\\usepackage{times}',
'textgreek': '',
'fncychap': '',
'geometry': '\\usepackage[dvipdfm]{geometry}',
},
# special settings for latex_engine + language_code
('xelatex', 'fr'): {
# use babel instead of polyglossia by default
'polyglossia': '',
'babel': '\\usepackage{babel}',
},
('xelatex', 'zh'): {
'fontenc': '\\usepackage{xeCJK}',
},
('xelatex', 'el'): {
'fontpkg': XELATEX_GREEK_DEFAULT_FONTPKG,
},
} # type: Dict[Any, Dict[str, Any]]

View File

@ -26,7 +26,7 @@ from sphinx.builders import Builder
from sphinx.locale import __ from sphinx.locale import __
from sphinx.util import encode_uri, requests, logging from sphinx.util import encode_uri, requests, logging
from sphinx.util.console import ( # type: ignore from sphinx.util.console import ( # type: ignore
purple, red, darkgreen, darkgray, darkred, turquoise purple, red, darkgreen, darkgray, turquoise
) )
from sphinx.util.nodes import get_node_line from sphinx.util.nodes import get_node_line
from sphinx.util.requests import is_ssl_error from sphinx.util.requests import is_ssl_error
@ -251,11 +251,11 @@ class CheckExternalLinksBuilder(Builder):
elif status == 'redirected': elif status == 'redirected':
try: try:
text, color = { text, color = {
301: ('permanently', darkred), 301: ('permanently', purple),
302: ('with Found', purple), 302: ('with Found', purple),
303: ('with See Other', purple), 303: ('with See Other', purple),
307: ('temporarily', turquoise), 307: ('temporarily', turquoise),
308: ('permanently', darkred), 308: ('permanently', purple),
}[code] }[code]
except KeyError: except KeyError:
text, color = ('with unknown code', purple) text, color = ('with unknown code', purple)

View File

@ -446,6 +446,25 @@ class PyFunction(PyObject):
class PyVariable(PyObject): class PyVariable(PyObject):
"""Description of a variable.""" """Description of a variable."""
option_spec = PyObject.option_spec.copy()
option_spec.update({
'type': directives.unchanged,
'value': directives.unchanged,
})
def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
fullname, prefix = super().handle_signature(sig, signode)
typ = self.options.get('type')
if typ:
signode += addnodes.desc_annotation(typ, ': ' + typ)
value = self.options.get('value')
if value:
signode += addnodes.desc_annotation(value, ' = ' + value)
return fullname, prefix
def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
name, cls = name_cls name, cls = name_cls
if modname: if modname:
@ -638,6 +657,25 @@ class PyStaticMethod(PyMethod):
class PyAttribute(PyObject): class PyAttribute(PyObject):
"""Description of an attribute.""" """Description of an attribute."""
option_spec = PyObject.option_spec.copy()
option_spec.update({
'type': directives.unchanged,
'value': directives.unchanged,
})
def handle_signature(self, sig: str, signode: desc_signature) -> Tuple[str, str]:
fullname, prefix = super().handle_signature(sig, signode)
typ = self.options.get('type')
if typ:
signode += addnodes.desc_annotation(typ, ': ' + typ)
value = self.options.get('value')
if value:
signode += addnodes.desc_annotation(value, ' = ' + value)
return fullname, prefix
def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str: def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
name, cls = name_cls name, cls = name_cls
try: try:

View File

@ -925,11 +925,11 @@ class StandardDomain(Domain):
if isinstance(node, nodes.section): if isinstance(node, nodes.section):
return 'section' return 'section'
elif isinstance(node, nodes.container): elif (isinstance(node, nodes.container) and
if node.get('literal_block') and has_child(node, nodes.literal_block): 'literal_block' in node and
return 'code-block' has_child(node, nodes.literal_block)):
else: # given node is a code-block having caption
return None return 'code-block'
else: else:
figtype, _ = self.enumerable_nodes.get(node.__class__, (None, None)) figtype, _ = self.enumerable_nodes.get(node.__class__, (None, None))
return figtype return figtype

View File

@ -20,6 +20,7 @@ import locale
import os import os
import sys import sys
import warnings import warnings
from copy import copy
from fnmatch import fnmatch from fnmatch import fnmatch
from importlib.machinery import EXTENSION_SUFFIXES from importlib.machinery import EXTENSION_SUFFIXES
from os import path from os import path
@ -72,14 +73,19 @@ def module_join(*modnames: str) -> str:
def write_file(name: str, text: str, opts: Any) -> None: def write_file(name: str, text: str, opts: Any) -> None:
"""Write the output file for module/package <name>.""" """Write the output file for module/package <name>."""
quiet = getattr(opts, 'quiet', None)
fname = path.join(opts.destdir, '%s.%s' % (name, opts.suffix)) fname = path.join(opts.destdir, '%s.%s' % (name, opts.suffix))
if opts.dryrun: if opts.dryrun:
print(__('Would create file %s.') % fname) if not quiet:
print(__('Would create file %s.') % fname)
return return
if not opts.force and path.isfile(fname): if not opts.force and path.isfile(fname):
print(__('File %s already exists, skipping.') % fname) if not quiet:
print(__('File %s already exists, skipping.') % fname)
else: else:
print(__('Creating file %s.') % fname) if not quiet:
print(__('Creating file %s.') % fname)
with FileAvoidWrite(fname) as f: with FileAvoidWrite(fname) as f:
f.write(text) f.write(text)
@ -107,12 +113,16 @@ def format_directive(module: str, package: str = None) -> str:
def create_module_file(package: str, basename: str, opts: Any, def create_module_file(package: str, basename: str, opts: Any,
user_template_dir: str = None) -> None: user_template_dir: str = None) -> None:
"""Build the text of the file and write the file.""" """Build the text of the file and write the file."""
options = copy(OPTIONS)
if opts.includeprivate and 'private-members' not in options:
options.append('private-members')
qualname = module_join(package, basename) qualname = module_join(package, basename)
context = { context = {
'show_headings': not opts.noheadings, 'show_headings': not opts.noheadings,
'basename': basename, 'basename': basename,
'qualname': qualname, 'qualname': qualname,
'automodule_options': OPTIONS, 'automodule_options': options,
} }
text = ReSTRenderer([user_template_dir, template_dir]).render('module.rst_t', context) text = ReSTRenderer([user_template_dir, template_dir]).render('module.rst_t', context)
write_file(qualname, text, opts) write_file(qualname, text, opts)
@ -133,6 +143,9 @@ def create_package_file(root: str, master_package: str, subroot: str, py_files:
sub != INITPY] sub != INITPY]
submodules = [module_join(master_package, subroot, modname) submodules = [module_join(master_package, subroot, modname)
for modname in submodules] for modname in submodules]
options = copy(OPTIONS)
if opts.includeprivate and 'private-members' not in options:
options.append('private-members')
pkgname = module_join(master_package, subroot) pkgname = module_join(master_package, subroot)
context = { context = {
@ -142,7 +155,7 @@ def create_package_file(root: str, master_package: str, subroot: str, py_files:
'is_namespace': is_namespace, 'is_namespace': is_namespace,
'modulefirst': opts.modulefirst, 'modulefirst': opts.modulefirst,
'separatemodules': opts.separatemodules, 'separatemodules': opts.separatemodules,
'automodule_options': OPTIONS, 'automodule_options': options,
'show_headings': not opts.noheadings, 'show_headings': not opts.noheadings,
} }
text = ReSTRenderer([user_template_dir, template_dir]).render('package.rst_t', context) text = ReSTRenderer([user_template_dir, template_dir]).render('package.rst_t', context)
@ -316,6 +329,8 @@ Note: By default this script will not overwrite already created files."""))
parser.add_argument('-o', '--output-dir', action='store', dest='destdir', parser.add_argument('-o', '--output-dir', action='store', dest='destdir',
required=True, required=True,
help=__('directory to place all output')) help=__('directory to place all output'))
parser.add_argument('-q', action='store_true', dest='quiet',
help=__('no output on stdout, just warnings on stderr'))
parser.add_argument('-d', '--maxdepth', action='store', dest='maxdepth', parser.add_argument('-d', '--maxdepth', action='store', dest='maxdepth',
type=int, default=4, type=int, default=4,
help=__('maximum depth of submodules to show in the TOC ' help=__('maximum depth of submodules to show in the TOC '
@ -443,6 +458,8 @@ def main(argv: List[str] = sys.argv[1:]) -> int:
} }
if args.extensions: if args.extensions:
d['extensions'].extend(args.extensions) d['extensions'].extend(args.extensions)
if args.quiet:
d['quiet'] = True
for ext in d['extensions'][:]: for ext in d['extensions'][:]:
if ',' in ext: if ',' in ext:

View File

@ -10,6 +10,7 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import importlib
import re import re
import warnings import warnings
from types import ModuleType from types import ModuleType
@ -33,6 +34,7 @@ from sphinx.util import logging
from sphinx.util import rpartition from sphinx.util import rpartition
from sphinx.util.docstrings import prepare_docstring from sphinx.util.docstrings import prepare_docstring
from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature
from sphinx.util.typing import stringify as stringify_typehint
if False: if False:
# For type annotation # For type annotation
@ -72,14 +74,14 @@ def members_option(arg: Any) -> Union[object, List[str]]:
"""Used to convert the :members: option to auto directives.""" """Used to convert the :members: option to auto directives."""
if arg is None or arg is True: if arg is None or arg is True:
return ALL return ALL
return [x.strip() for x in arg.split(',')] return [x.strip() for x in arg.split(',') if x.strip()]
def members_set_option(arg: Any) -> Union[object, Set[str]]: def members_set_option(arg: Any) -> Union[object, Set[str]]:
"""Used to convert the :members: option to auto directives.""" """Used to convert the :members: option to auto directives."""
if arg is None: if arg is None:
return ALL return ALL
return {x.strip() for x in arg.split(',')} return {x.strip() for x in arg.split(',') if x.strip()}
SUPPRESS = object() SUPPRESS = object()
@ -1232,12 +1234,22 @@ class DataDocumenter(ModuleLevelDocumenter):
super().add_directive_header(sig) super().add_directive_header(sig)
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
if not self.options.annotation: if not self.options.annotation:
# obtain annotation for this data
annotations = getattr(self.parent, '__annotations__', {})
if self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
else:
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
if self.analyzer and key in self.analyzer.annotations:
self.add_line(' :type: ' + self.analyzer.annotations[key],
sourcename)
try: try:
objrepr = object_description(self.object) objrepr = object_description(self.object)
self.add_line(' :value: ' + objrepr, sourcename)
except ValueError: except ValueError:
pass pass
else:
self.add_line(' :annotation: = ' + objrepr, sourcename)
elif self.options.annotation is SUPPRESS: elif self.options.annotation is SUPPRESS:
pass pass
else: else:
@ -1276,6 +1288,12 @@ class DataDeclarationDocumenter(DataDocumenter):
"""Never import anything.""" """Never import anything."""
# disguise as a data # disguise as a data
self.objtype = 'data' self.objtype = 'data'
try:
# import module to obtain type annotation
self.parent = importlib.import_module(self.modname)
except ImportError:
pass
return True return True
def add_content(self, more_content: Any, no_docstring: bool = False) -> None: def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
@ -1404,12 +1422,22 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
if not self.options.annotation: if not self.options.annotation:
if not self._datadescriptor: if not self._datadescriptor:
# obtain annotation for this attribute
annotations = getattr(self.parent, '__annotations__', {})
if self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
else:
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
if self.analyzer and key in self.analyzer.annotations:
self.add_line(' :type: ' + self.analyzer.annotations[key],
sourcename)
try: try:
objrepr = object_description(self.object) objrepr = object_description(self.object)
self.add_line(' :value: ' + objrepr, sourcename)
except ValueError: except ValueError:
pass pass
else:
self.add_line(' :annotation: = ' + objrepr, sourcename)
elif self.options.annotation is SUPPRESS: elif self.options.annotation is SUPPRESS:
pass pass
else: else:

View File

@ -9,6 +9,7 @@
""" """
import re import re
from collections import OrderedDict
from typing import Any, Dict, Iterable from typing import Any, Dict, Iterable
from typing import cast from typing import cast
@ -37,13 +38,14 @@ def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
"""Record type hints to env object.""" """Record type hints to env object."""
try: try:
if callable(obj): if callable(obj):
annotations = app.env.temp_data.setdefault('annotations', {}).setdefault(name, {}) annotations = app.env.temp_data.setdefault('annotations', {})
annotation = annotations.setdefault(name, OrderedDict())
sig = inspect.signature(obj) sig = inspect.signature(obj)
for param in sig.parameters.values(): for param in sig.parameters.values():
if param.annotation is not param.empty: if param.annotation is not param.empty:
annotations[param.name] = typing.stringify(param.annotation) annotation[param.name] = typing.stringify(param.annotation)
if sig.return_annotation is not sig.empty: if sig.return_annotation is not sig.empty:
annotations['return'] = typing.stringify(sig.return_annotation) annotation['return'] = typing.stringify(sig.return_annotation)
except TypeError: except TypeError:
pass pass
@ -55,7 +57,10 @@ def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element
return return
signature = cast(addnodes.desc_signature, contentnode.parent[0]) signature = cast(addnodes.desc_signature, contentnode.parent[0])
fullname = '.'.join([signature['module'], signature['fullname']]) if signature['module']:
fullname = '.'.join([signature['module'], signature['fullname']])
else:
fullname = signature['fullname']
annotations = app.env.temp_data.get('annotations', {}) annotations = app.env.temp_data.get('annotations', {})
if annotations.get(fullname, {}): if annotations.get(fullname, {}):
field_lists = [n for n in contentnode if isinstance(n, nodes.field_list)] field_lists = [n for n in contentnode if isinstance(n, nodes.field_list)]

View File

@ -142,9 +142,10 @@ class ModuleAnalyzer:
self.code = source.read() self.code = source.read()
# will be filled by parse() # will be filled by parse()
self.attr_docs = None # type: Dict[Tuple[str, str], List[str]] self.annotations = None # type: Dict[Tuple[str, str], str]
self.tagorder = None # type: Dict[str, int] self.attr_docs = None # type: Dict[Tuple[str, str], List[str]]
self.tags = None # type: Dict[str, Tuple[str, int, int]] self.tagorder = None # type: Dict[str, int]
self.tags = None # type: Dict[str, Tuple[str, int, int]]
def parse(self) -> None: def parse(self) -> None:
"""Parse the source code.""" """Parse the source code."""
@ -159,6 +160,7 @@ class ModuleAnalyzer:
else: else:
self.attr_docs[scope] = [''] self.attr_docs[scope] = ['']
self.annotations = parser.annotations
self.tags = parser.definitions self.tags = parser.definitions
self.tagorder = parser.deforders self.tagorder = parser.deforders
except Exception as exc: except Exception as exc:

View File

@ -38,6 +38,8 @@ def unparse(node: ast.AST) -> str:
"""Unparse an AST to string.""" """Unparse an AST to string."""
if node is None: if node is None:
return None return None
elif isinstance(node, str):
return node
elif isinstance(node, ast.Attribute): elif isinstance(node, ast.Attribute):
return "%s.%s" % (unparse(node.value), node.attr) return "%s.%s" % (unparse(node.value), node.attr)
elif isinstance(node, ast.Bytes): elif isinstance(node, ast.Bytes):

View File

@ -7,7 +7,6 @@
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import ast
import inspect import inspect
import itertools import itertools
import re import re
@ -17,6 +16,9 @@ from token import NAME, NEWLINE, INDENT, DEDENT, NUMBER, OP, STRING
from tokenize import COMMENT, NL from tokenize import COMMENT, NL
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
from sphinx.pycode.ast import ast # for py37 or older
from sphinx.pycode.ast import parse, unparse
comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$') comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$')
indent_re = re.compile('^\\s*$') indent_re = re.compile('^\\s*$')
@ -226,6 +228,7 @@ class VariableCommentPicker(ast.NodeVisitor):
self.current_classes = [] # type: List[str] self.current_classes = [] # type: List[str]
self.current_function = None # type: ast.FunctionDef self.current_function = None # type: ast.FunctionDef
self.comments = {} # type: Dict[Tuple[str, str], str] self.comments = {} # type: Dict[Tuple[str, str], str]
self.annotations = {} # type: Dict[Tuple[str, str], str]
self.previous = None # type: ast.AST self.previous = None # type: ast.AST
self.deforders = {} # type: Dict[str, int] self.deforders = {} # type: Dict[str, int]
super().__init__() super().__init__()
@ -254,6 +257,18 @@ class VariableCommentPicker(ast.NodeVisitor):
self.comments[(context, name)] = comment self.comments[(context, name)] = comment
def add_variable_annotation(self, name: str, annotation: ast.AST) -> None:
if self.current_function:
if self.current_classes and self.context[-1] == "__init__":
# store variable comments inside __init__ method of classes
context = ".".join(self.context[:-1])
else:
return
else:
context = ".".join(self.context)
self.annotations[(context, name)] = unparse(annotation)
def get_self(self) -> ast.arg: def get_self(self) -> ast.arg:
"""Returns the name of first argument if in function.""" """Returns the name of first argument if in function."""
if self.current_function and self.current_function.args.args: if self.current_function and self.current_function.args.args:
@ -295,6 +310,14 @@ class VariableCommentPicker(ast.NodeVisitor):
except TypeError: except TypeError:
return # this assignment is not new definition! return # this assignment is not new definition!
# record annotation
if hasattr(node, 'annotation') and node.annotation: # type: ignore
for varname in varnames:
self.add_variable_annotation(varname, node.annotation) # type: ignore
elif hasattr(node, 'type_comment') and node.type_comment:
for varname in varnames:
self.add_variable_annotation(varname, node.type_comment) # type: ignore
# check comments after assignment # check comments after assignment
parser = AfterCommentParser([current_line[node.col_offset:]] + parser = AfterCommentParser([current_line[node.col_offset:]] +
self.buffers[node.lineno:]) self.buffers[node.lineno:])
@ -468,6 +491,7 @@ class Parser:
def __init__(self, code: str, encoding: str = 'utf-8') -> None: def __init__(self, code: str, encoding: str = 'utf-8') -> None:
self.code = filter_whitespace(code) self.code = filter_whitespace(code)
self.encoding = encoding self.encoding = encoding
self.annotations = {} # type: Dict[Tuple[str, str], str]
self.comments = {} # type: Dict[Tuple[str, str], str] self.comments = {} # type: Dict[Tuple[str, str], str]
self.deforders = {} # type: Dict[str, int] self.deforders = {} # type: Dict[str, int]
self.definitions = {} # type: Dict[str, Tuple[str, int, int]] self.definitions = {} # type: Dict[str, Tuple[str, int, int]]
@ -479,9 +503,10 @@ class Parser:
def parse_comments(self) -> None: def parse_comments(self) -> None:
"""Parse the code and pick up comments.""" """Parse the code and pick up comments."""
tree = ast.parse(self.code) tree = parse(self.code)
picker = VariableCommentPicker(self.code.splitlines(True), self.encoding) picker = VariableCommentPicker(self.code.splitlines(True), self.encoding)
picker.visit(tree) picker.visit(tree)
self.annotations = picker.annotations
self.comments = picker.comments self.comments = picker.comments
self.deforders = picker.deforders self.deforders = picker.deforders

View File

@ -68,182 +68,6 @@ ENUMERATE_LIST_STYLE = defaultdict(lambda: r'\arabic',
'lowerroman': r'\roman', 'lowerroman': r'\roman',
'upperroman': r'\Roman', 'upperroman': r'\Roman',
}) })
PDFLATEX_DEFAULT_FONTPKG = r'''
\usepackage{times}
\expandafter\ifx\csname T@LGR\endcsname\relax
\else
% LGR was declared as font encoding
\substitutefont{LGR}{\rmdefault}{cmr}
\substitutefont{LGR}{\sfdefault}{cmss}
\substitutefont{LGR}{\ttdefault}{cmtt}
\fi
\expandafter\ifx\csname T@X2\endcsname\relax
\expandafter\ifx\csname T@T2A\endcsname\relax
\else
% T2A was declared as font encoding
\substitutefont{T2A}{\rmdefault}{cmr}
\substitutefont{T2A}{\sfdefault}{cmss}
\substitutefont{T2A}{\ttdefault}{cmtt}
\fi
\else
% X2 was declared as font encoding
\substitutefont{X2}{\rmdefault}{cmr}
\substitutefont{X2}{\sfdefault}{cmss}
\substitutefont{X2}{\ttdefault}{cmtt}
\fi
'''
XELATEX_DEFAULT_FONTPKG = r'''
\setmainfont{FreeSerif}[
Extension = .otf,
UprightFont = *,
ItalicFont = *Italic,
BoldFont = *Bold,
BoldItalicFont = *BoldItalic
]
\setsansfont{FreeSans}[
Extension = .otf,
UprightFont = *,
ItalicFont = *Oblique,
BoldFont = *Bold,
BoldItalicFont = *BoldOblique,
]
\setmonofont{FreeMono}[
Extension = .otf,
UprightFont = *,
ItalicFont = *Oblique,
BoldFont = *Bold,
BoldItalicFont = *BoldOblique,
]
'''
XELATEX_GREEK_DEFAULT_FONTPKG = (XELATEX_DEFAULT_FONTPKG +
'\n\\newfontfamily\\greekfont{FreeSerif}' +
'\n\\newfontfamily\\greekfontsf{FreeSans}' +
'\n\\newfontfamily\\greekfonttt{FreeMono}')
LUALATEX_DEFAULT_FONTPKG = XELATEX_DEFAULT_FONTPKG
DEFAULT_SETTINGS = {
'latex_engine': 'pdflatex',
'papersize': 'letterpaper',
'pointsize': '10pt',
'pxunit': '.75bp',
'classoptions': '',
'extraclassoptions': '',
'maxlistdepth': '',
'sphinxpkgoptions': '',
'sphinxsetup': '',
'fvset': '\\fvset{fontsize=\\small}',
'passoptionstopackages': '',
'geometry': '\\usepackage{geometry}',
'inputenc': '',
'utf8extra': '',
'cmappkg': '\\usepackage{cmap}',
'fontenc': '\\usepackage[T1]{fontenc}',
'amsmath': '\\usepackage{amsmath,amssymb,amstext}',
'multilingual': '',
'babel': '\\usepackage{babel}',
'polyglossia': '',
'fontpkg': PDFLATEX_DEFAULT_FONTPKG,
'substitutefont': '',
'textcyrillic': '',
'textgreek': '\\usepackage{textalpha}',
'fncychap': '\\usepackage[Bjarne]{fncychap}',
'hyperref': ('% Include hyperref last.\n'
'\\usepackage{hyperref}\n'
'% Fix anchor placement for figures with captions.\n'
'\\usepackage{hypcap}% it must be loaded after hyperref.\n'
'% Set up styles of URL: it should be placed after hyperref.\n'
'\\urlstyle{same}'),
'contentsname': '',
'extrapackages': '',
'preamble': '',
'title': '',
'release': '',
'author': '',
'releasename': '',
'makeindex': '\\makeindex',
'shorthandoff': '',
'maketitle': '\\sphinxmaketitle',
'tableofcontents': '\\sphinxtableofcontents',
'atendofbody': '',
'printindex': '\\printindex',
'transition': '\n\n\\bigskip\\hrule\\bigskip\n\n',
'figure_align': 'htbp',
'tocdepth': '',
'secnumdepth': '',
} # type: Dict[str, Any]
ADDITIONAL_SETTINGS = {
'pdflatex': {
'inputenc': '\\usepackage[utf8]{inputenc}',
'utf8extra': ('\\ifdefined\\DeclareUnicodeCharacter\n'
'% support both utf8 and utf8x syntaxes\n'
' \\ifdefined\\DeclareUnicodeCharacterAsOptional\n'
' \\def\\sphinxDUC#1{\\DeclareUnicodeCharacter{"#1}}\n'
' \\else\n'
' \\let\\sphinxDUC\\DeclareUnicodeCharacter\n'
' \\fi\n'
' \\sphinxDUC{00A0}{\\nobreakspace}\n'
' \\sphinxDUC{2500}{\\sphinxunichar{2500}}\n'
' \\sphinxDUC{2502}{\\sphinxunichar{2502}}\n'
' \\sphinxDUC{2514}{\\sphinxunichar{2514}}\n'
' \\sphinxDUC{251C}{\\sphinxunichar{251C}}\n'
' \\sphinxDUC{2572}{\\textbackslash}\n'
'\\fi'),
},
'xelatex': {
'latex_engine': 'xelatex',
'polyglossia': '\\usepackage{polyglossia}',
'babel': '',
'fontenc': ('\\usepackage{fontspec}\n'
'\\defaultfontfeatures[\\rmfamily,\\sffamily,\\ttfamily]{}'),
'fontpkg': XELATEX_DEFAULT_FONTPKG,
'textgreek': '',
'utf8extra': ('\\catcode`^^^^00a0\\active\\protected\\def^^^^00a0'
'{\\leavevmode\\nobreak\\ }'),
},
'lualatex': {
'latex_engine': 'lualatex',
'polyglossia': '\\usepackage{polyglossia}',
'babel': '',
'fontenc': ('\\usepackage{fontspec}\n'
'\\defaultfontfeatures[\\rmfamily,\\sffamily,\\ttfamily]{}'),
'fontpkg': LUALATEX_DEFAULT_FONTPKG,
'textgreek': '',
'utf8extra': ('\\catcode`^^^^00a0\\active\\protected\\def^^^^00a0'
'{\\leavevmode\\nobreak\\ }'),
},
'platex': {
'latex_engine': 'platex',
'babel': '',
'classoptions': ',dvipdfmx',
'fontpkg': '\\usepackage{times}',
'textgreek': '',
'fncychap': '',
'geometry': '\\usepackage[dvipdfm]{geometry}',
},
'uplatex': {
'latex_engine': 'uplatex',
'babel': '',
'classoptions': ',dvipdfmx',
'fontpkg': '\\usepackage{times}',
'textgreek': '',
'fncychap': '',
'geometry': '\\usepackage[dvipdfm]{geometry}',
},
# special settings for latex_engine + language_code
('xelatex', 'fr'): {
# use babel instead of polyglossia by default
'polyglossia': '',
'babel': '\\usepackage{babel}',
},
('xelatex', 'zh'): {
'fontenc': '\\usepackage{xeCJK}',
},
('xelatex', 'el'): {
'fontpkg': XELATEX_GREEK_DEFAULT_FONTPKG,
},
} # type: Dict[Any, Dict[str, Any]]
EXTRA_RE = re.compile(r'^(.*\S)\s+\(([^()]*)\)\s*$') EXTRA_RE = re.compile(r'^(.*\S)\s+\(([^()]*)\)\s*$')
@ -492,16 +316,18 @@ class LaTeXTranslator(SphinxTranslator):
self.compact_list = 0 self.compact_list = 0
self.first_param = 0 self.first_param = 0
sphinxpkgoptions = []
# sort out some elements # sort out some elements
self.elements = self.builder.context.copy() self.elements = self.builder.context.copy()
# but some have other interface in config file # but some have other interface in config file
self.elements['wrapperclass'] = self.format_docclass(self.settings.docclass) self.elements['wrapperclass'] = self.format_docclass(document.get('docclass'))
# we assume LaTeX class provides \chapter command except in case # we assume LaTeX class provides \chapter command except in case
# of non-Japanese 'howto' case # of non-Japanese 'howto' case
self.sectionnames = LATEXSECTIONNAMES[:] self.sectionnames = LATEXSECTIONNAMES[:]
if self.settings.docclass == 'howto': if document.get('docclass') == 'howto':
docclass = self.config.latex_docclass.get('howto', 'article') docclass = self.config.latex_docclass.get('howto', 'article')
if docclass[0] == 'j': # Japanese class... if docclass[0] == 'j': # Japanese class...
pass pass
@ -535,15 +361,12 @@ class LaTeXTranslator(SphinxTranslator):
self.numfig_secnum_depth = min(self.numfig_secnum_depth, self.numfig_secnum_depth = min(self.numfig_secnum_depth,
len(LATEXSECTIONNAMES) - 1) len(LATEXSECTIONNAMES) - 1)
# if passed key value is < 1 LaTeX will act as if 0; see sphinx.sty # if passed key value is < 1 LaTeX will act as if 0; see sphinx.sty
self.elements['sphinxpkgoptions'] += \ sphinxpkgoptions.append('numfigreset=%s' % self.numfig_secnum_depth)
(',numfigreset=%s' % self.numfig_secnum_depth)
else: else:
self.elements['sphinxpkgoptions'] += ',nonumfigreset' sphinxpkgoptions.append('nonumfigreset')
try:
if self.config.math_numfig: if self.config.numfig and self.config.math_numfig:
self.elements['sphinxpkgoptions'] += ',mathnumfig' sphinxpkgoptions.append('mathnumfig')
except AttributeError:
pass
if (self.config.language not in {None, 'en', 'ja'} and if (self.config.language not in {None, 'en', 'ja'} and
'fncychap' not in self.config.latex_elements): 'fncychap' not in self.config.latex_elements):
@ -606,7 +429,7 @@ class LaTeXTranslator(SphinxTranslator):
# tocdepth = 1: show parts, chapters and sections # tocdepth = 1: show parts, chapters and sections
# tocdepth = 2: show parts, chapters, sections and subsections # tocdepth = 2: show parts, chapters, sections and subsections
# ... # ...
tocdepth = self.document['tocdepth'] + self.top_sectionlevel - 2 tocdepth = self.document.get('tocdepth', 999) + self.top_sectionlevel - 2
if len(self.sectionnames) < len(LATEXSECTIONNAMES) and \ if len(self.sectionnames) < len(LATEXSECTIONNAMES) and \
self.top_sectionlevel > 0: self.top_sectionlevel > 0:
tocdepth += 1 # because top_sectionlevel is shifted by -1 tocdepth += 1 # because top_sectionlevel is shifted by -1
@ -624,17 +447,15 @@ class LaTeXTranslator(SphinxTranslator):
self.elements['secnumdepth'] = '\\setcounter{secnumdepth}{%d}' %\ self.elements['secnumdepth'] = '\\setcounter{secnumdepth}{%d}' %\
minsecnumdepth minsecnumdepth
contentsname = self.settings.contentsname contentsname = document.get('contentsname')
if contentsname: if contentsname:
self.elements['contentsname'] = self.babel_renewcommand('\\contentsname', self.elements['contentsname'] = self.babel_renewcommand('\\contentsname',
contentsname) contentsname)
if self.elements['maxlistdepth']: if self.elements['maxlistdepth']:
self.elements['sphinxpkgoptions'] += (',maxlistdepth=%s' % sphinxpkgoptions.append('maxlistdepth=%s' % self.elements['maxlistdepth'])
self.elements['maxlistdepth']) if sphinxpkgoptions:
if self.elements['sphinxpkgoptions']: self.elements['sphinxpkgoptions'] = '[,%s]' % ','.join(sphinxpkgoptions)
self.elements['sphinxpkgoptions'] = ('[%s]' %
self.elements['sphinxpkgoptions'])
if self.elements['sphinxsetup']: if self.elements['sphinxsetup']:
self.elements['sphinxsetup'] = ('\\sphinxsetup{%s}' % self.elements['sphinxsetup'] = ('\\sphinxsetup{%s}' %
self.elements['sphinxsetup']) self.elements['sphinxsetup'])
@ -1542,6 +1363,8 @@ class LaTeXTranslator(SphinxTranslator):
length = self.latex_image_length(node['width']) length = self.latex_image_length(node['width'])
elif isinstance(node[0], nodes.image) and 'width' in node[0]: elif isinstance(node[0], nodes.image) and 'width' in node[0]:
length = self.latex_image_length(node[0]['width']) length = self.latex_image_length(node[0]['width'])
self.body.append('\n\n') # Insert a blank line to prevent infinite loop
# https://github.com/sphinx-doc/sphinx/issues/7059
self.body.append('\\begin{wrapfigure}{%s}{%s}\n\\centering' % self.body.append('\\begin{wrapfigure}{%s}{%s}\n\\centering' %
('r' if node['align'] == 'right' else 'l', length or '0pt')) ('r' if node['align'] == 'right' else 'l', length or '0pt'))
self.context.append('\\end{wrapfigure}\n') self.context.append('\\end{wrapfigure}\n')
@ -2415,6 +2238,7 @@ class LaTeXTranslator(SphinxTranslator):
# Import old modules here for compatibility # Import old modules here for compatibility
from sphinx.builders.latex import constants # NOQA
from sphinx.builders.latex.transforms import URI_SCHEMES, ShowUrlsTransform # NOQA from sphinx.builders.latex.transforms import URI_SCHEMES, ShowUrlsTransform # NOQA
from sphinx.builders.latex.util import ExtBabel # NOQA from sphinx.builders.latex.util import ExtBabel # NOQA
@ -2427,6 +2251,12 @@ deprecated_alias('sphinx.writers.latex',
RemovedInSphinx30Warning) RemovedInSphinx30Warning)
deprecated_alias('sphinx.writers.latex', deprecated_alias('sphinx.writers.latex',
{ {
'ADDITIONAL_SETTINGS': constants.ADDITIONAL_SETTINGS,
'DEFAULT_SETTINGS': constants.DEFAULT_SETTINGS,
'LUALATEX_DEFAULT_FONTPKG': constants.LUALATEX_DEFAULT_FONTPKG,
'PDFLATEX_DEFAULT_FONTPKG': constants.PDFLATEX_DEFAULT_FONTPKG,
'XELATEX_DEFAULT_FONTPKG': constants.XELATEX_DEFAULT_FONTPKG,
'XELATEX_GREEK_DEFAULT_FONTPKG': constants.XELATEX_GREEK_DEFAULT_FONTPKG,
'ExtBabel': ExtBabel, 'ExtBabel': ExtBabel,
}, },
RemovedInSphinx40Warning) RemovedInSphinx40Warning)

View File

@ -2,12 +2,17 @@
attr1: str = '' attr1: str = ''
#: attr2 #: attr2
attr2: str attr2: str
#: attr3
attr3 = '' # type: str
class Class: class Class:
attr1: int = 0 attr1: int = 0
attr2: int attr2: int
attr3 = 0 # type: int
def __init__(self): def __init__(self):
self.attr3: int = 0 #: attr3 self.attr4: int = 0 #: attr4
self.attr4: int #: attr4 self.attr5: int #: attr5
self.attr6 = 0 # type: int
"""attr6"""

View File

@ -906,7 +906,7 @@ def test_autodoc_module_scope(app):
'', '',
'.. py:attribute:: Class.mdocattr', '.. py:attribute:: Class.mdocattr',
' :module: target', ' :module: target',
' :annotation: = <_io.StringIO object>', ' :value: <_io.StringIO object>',
'', '',
' should be documented as well - süß', ' should be documented as well - süß',
' ' ' '
@ -922,7 +922,7 @@ def test_autodoc_class_scope(app):
'', '',
'.. py:attribute:: Class.mdocattr', '.. py:attribute:: Class.mdocattr',
' :module: target', ' :module: target',
' :annotation: = <_io.StringIO object>', ' :value: <_io.StringIO object>',
'', '',
' should be documented as well - süß', ' should be documented as well - süß',
' ' ' '
@ -942,12 +942,12 @@ def test_class_attributes(app):
' ', ' ',
' .. py:attribute:: AttCls.a1', ' .. py:attribute:: AttCls.a1',
' :module: target', ' :module: target',
' :annotation: = hello world', ' :value: hello world',
' ', ' ',
' ', ' ',
' .. py:attribute:: AttCls.a2', ' .. py:attribute:: AttCls.a2',
' :module: target', ' :module: target',
' :annotation: = None', ' :value: None',
' ' ' '
] ]
@ -966,7 +966,7 @@ def test_instance_attributes(app):
' ', ' ',
' .. py:attribute:: InstAttCls.ca1', ' .. py:attribute:: InstAttCls.ca1',
' :module: target', ' :module: target',
" :annotation: = 'a'", " :value: 'a'",
' ', ' ',
' Doc comment for class attribute InstAttCls.ca1.', ' Doc comment for class attribute InstAttCls.ca1.',
' It can have multiple lines.', ' It can have multiple lines.',
@ -974,28 +974,28 @@ def test_instance_attributes(app):
' ', ' ',
' .. py:attribute:: InstAttCls.ca2', ' .. py:attribute:: InstAttCls.ca2',
' :module: target', ' :module: target',
" :annotation: = 'b'", " :value: 'b'",
' ', ' ',
' Doc comment for InstAttCls.ca2. One line only.', ' Doc comment for InstAttCls.ca2. One line only.',
' ', ' ',
' ', ' ',
' .. py:attribute:: InstAttCls.ca3', ' .. py:attribute:: InstAttCls.ca3',
' :module: target', ' :module: target',
" :annotation: = 'c'", " :value: 'c'",
' ', ' ',
' Docstring for class attribute InstAttCls.ca3.', ' Docstring for class attribute InstAttCls.ca3.',
' ', ' ',
' ', ' ',
' .. py:attribute:: InstAttCls.ia1', ' .. py:attribute:: InstAttCls.ia1',
' :module: target', ' :module: target',
' :annotation: = None', ' :value: None',
' ', ' ',
' Doc comment for instance attribute InstAttCls.ia1', ' Doc comment for instance attribute InstAttCls.ia1',
' ', ' ',
' ', ' ',
' .. py:attribute:: InstAttCls.ia2', ' .. py:attribute:: InstAttCls.ia2',
' :module: target', ' :module: target',
' :annotation: = None', ' :value: None',
' ', ' ',
' Docstring for instance attribute InstAttCls.ia2.', ' Docstring for instance attribute InstAttCls.ia2.',
' ' ' '
@ -1014,7 +1014,7 @@ def test_instance_attributes(app):
' ', ' ',
' .. py:attribute:: InstAttCls.ca1', ' .. py:attribute:: InstAttCls.ca1',
' :module: target', ' :module: target',
" :annotation: = 'a'", " :value: 'a'",
' ', ' ',
' Doc comment for class attribute InstAttCls.ca1.', ' Doc comment for class attribute InstAttCls.ca1.',
' It can have multiple lines.', ' It can have multiple lines.',
@ -1022,7 +1022,7 @@ def test_instance_attributes(app):
' ', ' ',
' .. py:attribute:: InstAttCls.ia1', ' .. py:attribute:: InstAttCls.ia1',
' :module: target', ' :module: target',
' :annotation: = None', ' :value: None',
' ', ' ',
' Doc comment for instance attribute InstAttCls.ia1', ' Doc comment for instance attribute InstAttCls.ia1',
' ' ' '
@ -1090,28 +1090,28 @@ def test_enum_class(app):
' ', ' ',
' .. py:attribute:: EnumCls.val1', ' .. py:attribute:: EnumCls.val1',
' :module: target.enum', ' :module: target.enum',
' :annotation: = 12', ' :value: 12',
' ', ' ',
' doc for val1', ' doc for val1',
' ', ' ',
' ', ' ',
' .. py:attribute:: EnumCls.val2', ' .. py:attribute:: EnumCls.val2',
' :module: target.enum', ' :module: target.enum',
' :annotation: = 23', ' :value: 23',
' ', ' ',
' doc for val2', ' doc for val2',
' ', ' ',
' ', ' ',
' .. py:attribute:: EnumCls.val3', ' .. py:attribute:: EnumCls.val3',
' :module: target.enum', ' :module: target.enum',
' :annotation: = 34', ' :value: 34',
' ', ' ',
' doc for val3', ' doc for val3',
' ', ' ',
' ', ' ',
' .. py:attribute:: EnumCls.val4', ' .. py:attribute:: EnumCls.val4',
' :module: target.enum', ' :module: target.enum',
' :annotation: = 34', ' :value: 34',
' ' ' '
] ]
@ -1121,7 +1121,7 @@ def test_enum_class(app):
'', '',
'.. py:attribute:: EnumCls.val1', '.. py:attribute:: EnumCls.val1',
' :module: target.enum', ' :module: target.enum',
' :annotation: = 12', ' :value: 12',
'', '',
' doc for val1', ' doc for val1',
' ' ' '
@ -1405,40 +1405,68 @@ def test_autodoc_typed_instance_variables(app):
' ', ' ',
' .. py:attribute:: Class.attr1', ' .. py:attribute:: Class.attr1',
' :module: target.typed_vars', ' :module: target.typed_vars',
' :annotation: = 0', ' :type: int',
' :value: 0',
' ', ' ',
' ', ' ',
' .. py:attribute:: Class.attr2', ' .. py:attribute:: Class.attr2',
' :module: target.typed_vars', ' :module: target.typed_vars',
' :annotation: = None', ' :type: int',
' :value: None',
' ', ' ',
' ', ' ',
' .. py:attribute:: Class.attr3', ' .. py:attribute:: Class.attr3',
' :module: target.typed_vars', ' :module: target.typed_vars',
' :annotation: = None', ' :type: int',
' :value: 0',
' ', ' ',
' attr3',
' ',
' ', ' ',
' .. py:attribute:: Class.attr4', ' .. py:attribute:: Class.attr4',
' :module: target.typed_vars', ' :module: target.typed_vars',
' :annotation: = None', ' :type: int',
' :value: None',
' ', ' ',
' attr4', ' attr4',
' ', ' ',
' ',
' .. py:attribute:: Class.attr5',
' :module: target.typed_vars',
' :type: int',
' :value: None',
' ',
' attr5',
' ',
' ',
' .. py:attribute:: Class.attr6',
' :module: target.typed_vars',
' :type: int',
' :value: None',
' ',
' attr6',
' ',
'', '',
'.. py:data:: attr1', '.. py:data:: attr1',
' :module: target.typed_vars', ' :module: target.typed_vars',
" :annotation: = ''", ' :type: str',
" :value: ''",
'', '',
' attr1', ' attr1',
' ', ' ',
'', '',
'.. py:data:: attr2', '.. py:data:: attr2',
' :module: target.typed_vars', ' :module: target.typed_vars',
" :annotation: = None", ' :type: str',
' :value: None',
'', '',
' attr2', ' attr2',
' ',
'',
'.. py:data:: attr3',
' :module: target.typed_vars',
' :type: str',
" :value: ''",
'',
' attr3',
' ' ' '
] ]
@ -1455,7 +1483,7 @@ def test_autodoc_for_egged_code(app):
'', '',
'.. py:data:: CONSTANT', '.. py:data:: CONSTANT',
' :module: sample', ' :module: sample',
' :annotation: = 1', ' :value: 1',
'', '',
' constant on sample.py', ' constant on sample.py',
' ', ' ',

View File

@ -285,6 +285,20 @@ def test_exceptions_module_is_ignored(app):
def test_pydata_signature(app): def test_pydata_signature(app):
text = (".. py:data:: version\n"
" :type: int\n"
" :value: 1\n")
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index,
[desc, ([desc_signature, ([desc_name, "version"],
[desc_annotation, ": int"],
[desc_annotation, " = 1"])],
desc_content)]))
assert_node(doctree[1], addnodes.desc, desctype="data",
domain="py", objtype="data", noindex=False)
def test_pydata_signature_old(app):
text = (".. py:data:: version\n" text = (".. py:data:: version\n"
" :annotation: = 1\n") " :annotation: = 1\n")
doctree = restructuredtext.parse(app, text) doctree = restructuredtext.parse(app, text)
@ -481,7 +495,9 @@ def test_pystaticmethod(app):
def test_pyattribute(app): def test_pyattribute(app):
text = (".. py:class:: Class\n" text = (".. py:class:: Class\n"
"\n" "\n"
" .. py:attribute:: attr\n") " .. py:attribute:: attr\n"
" :type: str\n"
" :value: ''\n")
domain = app.env.get_domain('py') domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text) doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index, assert_node(doctree, (addnodes.index,
@ -491,7 +507,9 @@ def test_pyattribute(app):
desc)])])) desc)])]))
assert_node(doctree[1][1][0], addnodes.index, assert_node(doctree[1][1][0], addnodes.index,
entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)]) entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)])
assert_node(doctree[1][1][1], ([desc_signature, desc_name, "attr"], assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"],
[desc_annotation, ": str"],
[desc_annotation, " = ''"])],
[desc_content, ()])) [desc_content, ()]))
assert 'Class.attr' in domain.objects assert 'Class.attr' in domain.objects
assert domain.objects['Class.attr'] == ('index', 'attribute') assert domain.objects['Class.attr'] == ('index', 'attribute')

View File

@ -408,11 +408,13 @@ def test_private(tempdir):
# without --private option # without --private option
apidoc_main(['-o', tempdir, tempdir]) apidoc_main(['-o', tempdir, tempdir])
assert (tempdir / 'hello.rst').exists() assert (tempdir / 'hello.rst').exists()
assert ':private-members:' not in (tempdir / 'hello.rst').text()
assert not (tempdir / '_world.rst').exists() assert not (tempdir / '_world.rst').exists()
# with --private option # with --private option
apidoc_main(['--private', '-o', tempdir, tempdir]) apidoc_main(['--private', '-f', '-o', tempdir, tempdir])
assert (tempdir / 'hello.rst').exists() assert (tempdir / 'hello.rst').exists()
assert ':private-members:' in (tempdir / 'hello.rst').text()
assert (tempdir / '_world.rst').exists() assert (tempdir / '_world.rst').exists()

View File

@ -99,12 +99,19 @@ def test_annotated_assignment_py36():
source = ('a: str = "Sphinx" #: comment\n' source = ('a: str = "Sphinx" #: comment\n'
'b: int = 1\n' 'b: int = 1\n'
'"""string on next line"""\n' '"""string on next line"""\n'
'c: int #: comment') 'c: int #: comment\n'
'd = 1 # type: int\n'
'"""string on next line"""\n')
parser = Parser(source) parser = Parser(source)
parser.parse() parser.parse()
assert parser.comments == {('', 'a'): 'comment', assert parser.comments == {('', 'a'): 'comment',
('', 'b'): 'string on next line', ('', 'b'): 'string on next line',
('', 'c'): 'comment'} ('', 'c'): 'comment',
('', 'd'): 'string on next line'}
assert parser.annotations == {('', 'a'): 'str',
('', 'b'): 'int',
('', 'c'): 'int',
('', 'd'): 'int'}
assert parser.definitions == {} assert parser.definitions == {}