diff --git a/.codecov.yml b/.codecov.yml index aa7a96c27..0e6fdf0c3 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,6 +1,6 @@ coverage: status: - patch: + project: default: # allowed to drop X% and still result in a "success" commit status threshold: 0.05 diff --git a/CHANGES b/CHANGES index b267905d6..7f841a5d0 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,11 @@ Incompatible changes Deprecated ---------- +* #4623: ``sphinx.build_main()`` is deprecated. Use + ``sphinx.cmd.build.build_main()`` instead. +* autosummary: The interface of ``sphinx.ext.autosummary.get_documenter()`` has + been changed (Since 1.7.0) + Features added -------------- @@ -22,6 +27,12 @@ Bugs fixed * #4622: epub: :confval:`epub_scheme` does not effect to content.opf * #4627: graphviz: Fit graphviz images to page * #4617: quickstart: PROJECT_DIR argument is required +* #4623: sphinx.build_main no longer exists in 1.7.0 +* #4615: The argument of ``sphinx.build`` has been changed in 1.7.0 +* autosummary: The interface of ``sphinx.ext.autosummary.get_documenter()`` has + been changed +* #4630: Have order on msgids in sphinx.pot deterministic +* #4563: autosummary: Incorrect end of line punctuation detection * #4577: Enumerated sublists with explicit start with wrong number Testing diff --git a/sphinx/__init__.py b/sphinx/__init__.py index c8ce7a7ea..5acaaa2ca 100644 --- a/sphinx/__init__.py +++ b/sphinx/__init__.py @@ -15,6 +15,7 @@ from __future__ import absolute_import import os +import sys import warnings from os import path @@ -66,9 +67,30 @@ def main(*args, **kwargs): RemovedInSphinx20Warning, stacklevel=2, ) + args = args[1:] # skip first argument to adjust arguments (refs: #4615) return build.main(*args, **kwargs) +def build_main(argv=sys.argv): + """Sphinx build "main" command-line entry.""" + warnings.warn( + '`sphinx.build_main()` has moved to `sphinx.cmd.build.build_main()`.', + RemovedInSphinx20Warning, + stacklevel=2, + ) + return build.build_main(argv[1:]) # skip first argument to adjust arguments (refs: #4615) + + +def make_main(argv=sys.argv): + """Sphinx build "make mode" entry.""" + warnings.warn( + '`sphinx.build_main()` has moved to `sphinx.cmd.build.make_main()`.', + RemovedInSphinx20Warning, + stacklevel=2, + ) + return build.make_main(argv[1:]) # skip first argument to adjust arguments (refs: #4615) + + if __name__ == '__main__': from .cmd import build warnings.warn( diff --git a/sphinx/apidoc.py b/sphinx/apidoc.py index 4e20fb7e4..be5e5c5ab 100644 --- a/sphinx/apidoc.py +++ b/sphinx/apidoc.py @@ -21,6 +21,7 @@ def main(*args, **kwargs): RemovedInSphinx20Warning, stacklevel=2, ) + args = args[1:] # skip first argument to adjust arguments (refs: #4615) _main(*args, **kwargs) diff --git a/sphinx/application.py b/sphinx/application.py index d118c2afe..d943de81d 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -88,6 +88,7 @@ builtin_extensions = ( 'sphinx.roles', 'sphinx.transforms.post_transforms', 'sphinx.transforms.post_transforms.images', + 'sphinx.util.compat', # collectors should be loaded by specific order 'sphinx.environment.collectors.dependencies', 'sphinx.environment.collectors.asset', diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index a5cca6a47..c33a0af40 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -12,7 +12,7 @@ from __future__ import unicode_literals from codecs import open -from collections import defaultdict +from collections import defaultdict, OrderedDict from datetime import datetime, tzinfo, timedelta from os import path, walk, getenv from time import time @@ -68,8 +68,8 @@ class Catalog(object): # type: () -> None self.messages = [] # type: List[unicode] # retain insertion order, a la OrderedDict - self.metadata = {} # type: Dict[unicode, List[Tuple[unicode, int, unicode]]] - # msgid -> file, line, uid + self.metadata = OrderedDict() # type: Dict[unicode, List[Tuple[unicode, int, unicode]]] # NOQA + # msgid -> file, line, uid def add(self, msg, origin): # type: (unicode, MsgOrigin) -> None @@ -236,7 +236,8 @@ class MessageCatalogBuilder(I18nBuilder): def _extract_from_template(self): # type: () -> None - files = self._collect_templates() + files = list(self._collect_templates()) + files.sort() logger.info(bold('building [%s]: ' % self.name), nonl=1) logger.info('targets for %d template files', len(files)) diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 4728281c3..1334084e2 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -58,23 +58,27 @@ import os import posixpath import re import sys +import warnings from types import ModuleType from typing import TYPE_CHECKING from docutils import nodes from docutils.parsers.rst import Directive, directives +from docutils.parsers.rst.states import RSTStateMachine, state_classes from docutils.statemachine import ViewList from six import string_types from six import text_type import sphinx from sphinx import addnodes +from sphinx.deprecation import RemovedInSphinx20Warning from sphinx.environment.adapters.toctree import TocTree from sphinx.ext.autodoc import get_documenters from sphinx.ext.autodoc.directive import DocumenterBridge, Options from sphinx.ext.autodoc.importer import import_module from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.util import import_object, rst, logging +from sphinx.util.docutils import new_document if TYPE_CHECKING: from typing import Any, Dict, List, Tuple, Type, Union # NOQA @@ -153,13 +157,17 @@ def autosummary_table_visit_html(self, node): # -- autodoc integration ------------------------------------------------------- +# current application object (used in `get_documenter()`). +_app = None # type: Sphinx + + class FakeDirective(DocumenterBridge): def __init__(self): super(FakeDirective, self).__init__({}, None, Options(), 0) # type: ignore -def get_documenter(app, obj, parent): - # type: (Sphinx, Any, Any) -> Type[Documenter] +def get_documenter(*args): + # type: (Any) -> Type[Documenter] """Get an autodoc.Documenter class suitable for documenting the given object. @@ -168,6 +176,16 @@ def get_documenter(app, obj, parent): belongs to. """ from sphinx.ext.autodoc import DataDocumenter, ModuleDocumenter + if len(args) == 3: + # new style arguments: (app, obj, parent) + app, obj, parent = args + else: + # old style arguments: (obj, parent) + app = _app + obj, parent = args + warnings.warn('the interface of get_documenter() has been changed. ' + 'Please give application object as first argument.', + RemovedInSphinx20Warning) if inspect.ismodule(obj): # ModuleDocumenter.can_document_member always returns False @@ -324,27 +342,7 @@ class Autosummary(Directive): # -- Grab the summary documenter.add_content(None) - doc = self.result.data - - while doc and not doc[0].strip(): - doc.pop(0) - - # If there's a blank line, then we can assume the first sentence / - # paragraph has ended, so anything after shouldn't be part of the - # summary - for i, piece in enumerate(doc): - if not piece.strip(): - doc = doc[:i] - break - - # Try to find the "first sentence", which may span multiple lines - m = re.search(r"^([A-Z].*?\.)(?:\s|$)", " ".join(doc).strip()) - if m: - summary = m.group(1).strip() - elif doc: - summary = doc[0].strip() - else: - summary = '' + summary = extract_summary(self.result.data[:], self.state.document) items.append((display_name, sig, summary, real_name)) @@ -451,6 +449,40 @@ def mangle_signature(sig, max_chars=30): return u"(%s)" % sig +def extract_summary(doc, document): + # type: (List[unicode], Any) -> unicode + """Extract summary from docstring.""" + + # Skip a blank lines at the top + while doc and not doc[0].strip(): + doc.pop(0) + + # If there's a blank line, then we can assume the first sentence / + # paragraph has ended, so anything after shouldn't be part of the + # summary + for i, piece in enumerate(doc): + if not piece.strip(): + doc = doc[:i] + break + + # Try to find the "first sentence", which may span multiple lines + sentences = " ".join(doc).split('.') + if len(sentences) == 1: + summary = sentences[0].strip() + else: + summary = '' + state_machine = RSTStateMachine(state_classes, 'Body') + while sentences: + summary += sentences.pop(0) + '.' + node = new_document('', document.settings) + state_machine.run([summary], node) + if not node.traverse(nodes.system_message): + # considered as that splitting by period does not break inline markups + break + + return summary + + def limited_join(sep, items, max_chars=30, overflow_marker="..."): # type: (unicode, List[unicode], int, unicode) -> unicode """Join a number of strings to one, limiting the length to *max_chars*. diff --git a/sphinx/quickstart.py b/sphinx/quickstart.py index 81676e8b1..ab4cf92da 100644 --- a/sphinx/quickstart.py +++ b/sphinx/quickstart.py @@ -22,6 +22,7 @@ def main(*args, **kwargs): RemovedInSphinx20Warning, stacklevel=2, ) + args = args[1:] # skip first argument to adjust arguments (refs: #4615) _main(*args, **kwargs) diff --git a/sphinx/util/compat.py b/sphinx/util/compat.py new file mode 100644 index 000000000..32ac11341 --- /dev/null +++ b/sphinx/util/compat.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +""" + sphinx.util.compat + ~~~~~~~~~~~~~~~~~~ + + modules for backward compatibility + + :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +from __future__ import absolute_import + +import sys +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from typing import Any, Dict # NOQA + from sphinx.application import Sphinx # NOQA + + +def register_application_for_autosummary(app): + # type: (Sphinx) -> None + """Register application object to autosummary module. + + Since Sphinx-1.7, documenters and attrgetters are registered into + applicaiton object. As a result, the arguments of + ``get_documenter()`` has been changed. To keep compatibility, + this handler registers application object to the module. + """ + if 'sphinx.ext.autosummary' in sys.modules: + from sphinx.ext import autosummary + autosummary._app = app + + +def setup(app): + # type: (Sphinx) -> Dict[unicode, Any] + app.connect('builder-inited', register_application_for_autosummary) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/tests/roots/test-gettext-template/_templates/template1.html b/tests/roots/test-gettext-template/_templates/template1.html new file mode 100644 index 000000000..f4b49f122 --- /dev/null +++ b/tests/roots/test-gettext-template/_templates/template1.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} +{% block body %} +

{{ _('Template 1') }}

+

{%trans%}This is Template 1.{%endtrans%}

+{% endblock %} diff --git a/tests/roots/test-gettext-template/_templates/template2.html b/tests/roots/test-gettext-template/_templates/template2.html new file mode 100644 index 000000000..2a21069a6 --- /dev/null +++ b/tests/roots/test-gettext-template/_templates/template2.html @@ -0,0 +1,5 @@ +{% extends "layout.html" %} +{% block body %} +

{{ _('Template 2') }}

+

{%trans%}This is Template 2.{%endtrans%}

+{% endblock %} diff --git a/tests/roots/test-gettext-template/conf.py b/tests/roots/test-gettext-template/conf.py new file mode 100644 index 000000000..1f4d84d07 --- /dev/null +++ b/tests/roots/test-gettext-template/conf.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +templates_path = ['_templates'] diff --git a/tests/roots/test-gettext-template/contents.rst b/tests/roots/test-gettext-template/contents.rst new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_build_gettext.py b/tests/test_build_gettext.py index c14013f9a..58ac7409c 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_build_gettext.py @@ -165,3 +165,18 @@ def test_gettext_template(app): result = (app.outdir / 'sphinx.pot').text(encoding='utf-8') assert "Welcome" in result assert "Sphinx %(version)s" in result + + +@pytest.mark.sphinx('gettext', testroot='gettext-template') +def test_gettext_template_msgid_order_in_sphinxpot(app): + app.builder.build_all() + assert (app.outdir / 'sphinx.pot').isfile() + + result = (app.outdir / 'sphinx.pot').text(encoding='utf-8') + assert re.search( + ('msgid "Template 1".*' + 'msgid "This is Template 1\.".*' + 'msgid "Template 2".*' + 'msgid "This is Template 2\.".*'), + result, + flags=re.S) diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index a77785954..f9e217801 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -9,14 +9,12 @@ :license: BSD, see LICENSE for details. """ +import pytest from six import iteritems, StringIO -from sphinx.ext.autosummary import mangle_signature, import_by_name - +from sphinx.ext.autosummary import mangle_signature, import_by_name, extract_summary from sphinx.testing.util import etree_parse -import pytest - html_warnfile = StringIO() @@ -57,6 +55,29 @@ def test_mangle_signature(): assert res == outp, (u"'%s' -> '%s' != '%s'" % (inp, res, outp)) +def test_extract_summary(): + from sphinx.util.docutils import new_document + from mock import Mock + settings = Mock(language_code='', + id_prefix='', + auto_id_prefix='', + pep_reference=False, + rfc_reference=False) + document = new_document('', settings) + + # normal case + doc = ['', + 'This is a first sentence. And second one.', + '', + 'Second block is here'] + assert extract_summary(doc, document) == 'This is a first sentence.' + + # inliner case + doc = ['This sentence contains *emphasis text having dots.*,', + 'it does not break sentence.'] + assert extract_summary(doc, document) == ' '.join(doc) + + @pytest.mark.sphinx('dummy', **default_kw) def test_get_items_summary(make_app, app_params): import sphinx.ext.autosummary @@ -95,7 +116,7 @@ def test_get_items_summary(make_app, app_params): expected_values = { 'withSentence': 'I have a sentence which spans multiple lines.', - 'noSentence': "this doesn't start with a", + 'noSentence': "this doesn't start with a capital.", 'emptyLine': "This is the real summary", 'module_attr': 'This is a module attribute', 'C.class_attr': 'This is a class attribute',