' +
common_content) in content
+
+
+@pytest.mark.sphinx('html', testroot='root',
+ confoverrides={'option_emphasise_placeholders': True})
+def test_option_emphasise_placeholders(app, status, warning):
+ app.build()
+ content = (app.outdir / 'objects.html').read_text()
+ assert 'TYPE' in content
+ assert '{TYPE}' not in content
+ assert ('WHERE'
+ '-'
+ 'COUNT' in content)
+ assert '{{value}}' in content
+
+
+@pytest.mark.sphinx('html', testroot='root')
+def test_option_emphasise_placeholders_default(app, status, warning):
+ app.build()
+ content = (app.outdir / 'objects.html').read_text()
+ assert '={TYPE}' in content
+ assert '={WHERE}-{COUNT}' in content
+ assert '{client_name}' in content
diff --git a/tests/test_config.py b/tests/test_config.py
index 3d72a6b0f04..4dabafc8313 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -381,3 +381,49 @@ def test_nitpick_ignore_regex_fullmatch(app, status, warning):
assert len(warning) == len(nitpick_warnings)
for actual, expected in zip(warning, nitpick_warnings):
assert expected in actual
+
+
+def test_conf_py_language_none(tempdir):
+ """Regression test for #10474."""
+
+ # Given a conf.py file with language = None
+ (tempdir / 'conf.py').write_text("language = None", encoding='utf-8')
+
+ # When we load conf.py into a Config object
+ cfg = Config.read(tempdir, {}, None)
+ cfg.init_values()
+
+ # Then the language is coerced to English
+ assert cfg.language == "en"
+
+
+@mock.patch("sphinx.config.logger")
+def test_conf_py_language_none_warning(logger, tempdir):
+ """Regression test for #10474."""
+
+ # Given a conf.py file with language = None
+ (tempdir / 'conf.py').write_text("language = None", encoding='utf-8')
+
+ # When we load conf.py into a Config object
+ Config.read(tempdir, {}, None)
+
+ # Then a warning is raised
+ assert logger.warning.called
+ assert logger.warning.call_args[0][0] == (
+ "Invalid configuration value found: 'language = None'. "
+ "Update your configuration to a valid language code. "
+ "Falling back to 'en' (English).")
+
+
+def test_conf_py_no_language(tempdir):
+ """Regression test for #10474."""
+
+ # Given a conf.py file with no language attribute
+ (tempdir / 'conf.py').write_text("", encoding='utf-8')
+
+ # When we load conf.py into a Config object
+ cfg = Config.read(tempdir, {}, None)
+ cfg.init_values()
+
+ # Then the language is coerced to English
+ assert cfg.language == "en"
diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py
index 70876728df1..765f9fd650b 100644
--- a/tests/test_domain_cpp.py
+++ b/tests/test_domain_cpp.py
@@ -1113,11 +1113,11 @@ def test_domain_cpp_build_misuse_of_roles(app, status, warning):
if targetType == 'templateParam':
warn.append("WARNING: cpp:{} targets a {} (".format(r, txtTargetType))
warn.append("WARNING: cpp:{} targets a {} (".format(r, txtTargetType))
- warn = list(sorted(warn))
+ warn = sorted(warn)
for w in ws:
assert "targets a" in w
ws = [w[w.index("WARNING:"):] for w in ws]
- ws = list(sorted(ws))
+ ws = sorted(ws)
print("Expected warnings:")
for w in warn:
print(w)
diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py
index 014067e8459..baad0c2daa2 100644
--- a/tests/test_domain_py.py
+++ b/tests/test_domain_py.py
@@ -452,6 +452,33 @@ def test_pyfunction_signature_full(app):
[desc_sig_name, pending_xref, "str"])])])
+def test_pyfunction_with_unary_operators(app):
+ text = ".. py:function:: menu(egg=+1, bacon=-1, sausage=~1, spam=not spam)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "egg"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "+1"])],
+ [desc_parameter, ([desc_sig_name, "bacon"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "-1"])],
+ [desc_parameter, ([desc_sig_name, "sausage"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "~1"])],
+ [desc_parameter, ([desc_sig_name, "spam"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "not spam"])])])
+
+
+def test_pyfunction_with_binary_operators(app):
+ text = ".. py:function:: menu(spam=2**64)"
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree[1][0][1],
+ [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "spam"],
+ [desc_sig_operator, "="],
+ [nodes.inline, "2**64"])])])
+
+
@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
def test_pyfunction_signature_full_py38(app):
# case: separator at head
diff --git a/tests/test_environment.py b/tests/test_environment.py
index 7ffca7898e0..c6f6b5aba52 100644
--- a/tests/test_environment.py
+++ b/tests/test_environment.py
@@ -49,8 +49,7 @@ def test_images(app):
app.build()
tree = app.env.get_doctree('images')
- htmlbuilder = StandaloneHTMLBuilder(app)
- htmlbuilder.set_environment(app.env)
+ htmlbuilder = StandaloneHTMLBuilder(app, app.env)
htmlbuilder.init()
htmlbuilder.imgpath = 'dummy'
htmlbuilder.post_process_images(tree)
@@ -59,8 +58,7 @@ def test_images(app):
assert set(htmlbuilder.images.values()) == \
{'img.png', 'img1.png', 'simg.png', 'svgimg.svg', 'img.foo.png'}
- latexbuilder = LaTeXBuilder(app)
- latexbuilder.set_environment(app.env)
+ latexbuilder = LaTeXBuilder(app, app.env)
latexbuilder.init()
latexbuilder.post_process_images(tree)
assert set(latexbuilder.images.keys()) == \
diff --git a/tests/test_environment_toctree.py b/tests/test_environment_toctree.py
index 588bcac18be..60a9826fda6 100644
--- a/tests/test_environment_toctree.py
+++ b/tests/test_environment_toctree.py
@@ -156,7 +156,7 @@ def test_get_toc_for(app):
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
def test_get_toc_for_only(app):
app.build()
- builder = StandaloneHTMLBuilder(app)
+ builder = StandaloneHTMLBuilder(app, app.env)
toctree = TocTree(app.env).get_toc_for('index', builder)
assert_node(toctree,
diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py
index ecd2bbce6fc..a35c6f640e5 100644
--- a/tests/test_ext_autodoc.py
+++ b/tests/test_ext_autodoc.py
@@ -549,6 +549,7 @@ def test_autodoc_members(app):
actual = do_autodoc(app, 'class', 'target.inheritance.Base', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Base()',
+ ' .. py:attribute:: Base.inheritedattr',
' .. py:method:: Base.inheritedclassmeth()',
' .. py:method:: Base.inheritedmeth()',
' .. py:method:: Base.inheritedstaticmeth(cls)'
@@ -569,6 +570,7 @@ def test_autodoc_members(app):
actual = do_autodoc(app, 'class', 'target.inheritance.Base', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Base()',
+ ' .. py:attribute:: Base.inheritedattr',
' .. py:method:: Base.inheritedclassmeth()',
' .. py:method:: Base.inheritedmeth()',
' .. py:method:: Base.inheritedstaticmeth(cls)'
@@ -601,6 +603,7 @@ def test_autodoc_exclude_members(app):
actual = do_autodoc(app, 'class', 'target.inheritance.Base', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Base()',
+ ' .. py:attribute:: Base.inheritedattr',
' .. py:method:: Base.inheritedclassmeth()'
]
@@ -618,6 +621,7 @@ def test_autodoc_exclude_members(app):
actual = do_autodoc(app, 'class', 'target.inheritance.Base', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Base()',
+ ' .. py:attribute:: Base.inheritedattr',
' .. py:method:: Base.inheritedclassmeth()'
]
@@ -628,6 +632,7 @@ def test_autodoc_exclude_members(app):
actual = do_autodoc(app, 'class', 'target.inheritance.Base', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Base()',
+ ' .. py:attribute:: Base.inheritedattr',
' .. py:method:: Base.inheritedclassmeth()',
' .. py:method:: Base.inheritedstaticmeth(cls)'
]
@@ -639,6 +644,7 @@ def test_autodoc_exclude_members(app):
actual = do_autodoc(app, 'class', 'target.inheritance.Base', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Base()',
+ ' .. py:attribute:: Base.inheritedattr',
' .. py:method:: Base.inheritedclassmeth()',
]
@@ -648,6 +654,7 @@ def test_autodoc_exclude_members(app):
actual = do_autodoc(app, 'class', 'target.inheritance.Base', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Base()',
+ ' .. py:attribute:: Base.inheritedattr',
' .. py:method:: Base.inheritedclassmeth()',
]
@@ -658,6 +665,7 @@ def test_autodoc_exclude_members(app):
actual = do_autodoc(app, 'class', 'target.inheritance.Base', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Base()',
+ ' .. py:attribute:: Base.inheritedattr',
' .. py:method:: Base.inheritedclassmeth()',
' .. py:method:: Base.inheritedmeth()',
' .. py:method:: Base.inheritedstaticmeth(cls)'
diff --git a/tests/test_ext_autodoc_automodule.py b/tests/test_ext_autodoc_automodule.py
index 8208d86f661..71b23679d96 100644
--- a/tests/test_ext_autodoc_automodule.py
+++ b/tests/test_ext_autodoc_automodule.py
@@ -133,6 +133,13 @@ def test_automodule_inherited_members(app):
' :module: target.inheritance',
'',
'',
+ ' .. py:attribute:: Base.inheritedattr',
+ ' :module: target.inheritance',
+ ' :value: None',
+ '',
+ ' docstring',
+ '',
+ '',
' .. py:method:: Base.inheritedclassmeth()',
' :module: target.inheritance',
' :classmethod:',
diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py
index 0011f450b5a..f40f3354e11 100644
--- a/tests/test_ext_autodoc_configs.py
+++ b/tests/test_ext_autodoc_configs.py
@@ -277,6 +277,72 @@ def test_autodoc_inherit_docstrings(app):
]
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_autodoc_inherit_docstrings_for_inherited_members(app):
+ options = {"members": None,
+ "inherited-members": None}
+
+ assert app.config.autodoc_inherit_docstrings is True # default
+ actual = do_autodoc(app, 'class', 'target.inheritance.Derived', options)
+ assert list(actual) == [
+ '',
+ '.. py:class:: Derived()',
+ ' :module: target.inheritance',
+ '',
+ '',
+ ' .. py:attribute:: Derived.inheritedattr',
+ ' :module: target.inheritance',
+ ' :value: None',
+ '',
+ ' docstring',
+ '',
+ '',
+ ' .. py:method:: Derived.inheritedclassmeth()',
+ ' :module: target.inheritance',
+ ' :classmethod:',
+ '',
+ ' Inherited class method.',
+ '',
+ '',
+ ' .. py:method:: Derived.inheritedmeth()',
+ ' :module: target.inheritance',
+ '',
+ ' Inherited function.',
+ '',
+ '',
+ ' .. py:method:: Derived.inheritedstaticmeth(cls)',
+ ' :module: target.inheritance',
+ ' :staticmethod:',
+ '',
+ ' Inherited static method.',
+ '',
+ ]
+
+ # disable autodoc_inherit_docstrings
+ app.config.autodoc_inherit_docstrings = False
+ actual = do_autodoc(app, 'class', 'target.inheritance.Derived', options)
+ assert list(actual) == [
+ '',
+ '.. py:class:: Derived()',
+ ' :module: target.inheritance',
+ '',
+ '',
+ ' .. py:method:: Derived.inheritedclassmeth()',
+ ' :module: target.inheritance',
+ ' :classmethod:',
+ '',
+ ' Inherited class method.',
+ '',
+ '',
+ ' .. py:method:: Derived.inheritedstaticmeth(cls)',
+ ' :module: target.inheritance',
+ ' :staticmethod:',
+ '',
+ ' Inherited static method.',
+ '',
+ ]
+
+
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_docstring_signature(app):
options = {"members": None, "special-members": "__init__, __new__"}
diff --git a/tests/test_ext_napoleon_iterators.py b/tests/test_ext_napoleon_iterators.py
index 6b80af6bf68..f5fe541b678 100644
--- a/tests/test_ext_napoleon_iterators.py
+++ b/tests/test_ext_napoleon_iterators.py
@@ -55,19 +55,19 @@ def test_iter(self):
self.assertTrue(it is it.__iter__())
a = []
- b = [i for i in peek_iter(a)]
+ b = list(peek_iter(a))
self.assertEqual([], b)
a = ['1']
- b = [i for i in peek_iter(a)]
+ b = list(peek_iter(a))
self.assertEqual(['1'], b)
a = ['1', '2']
- b = [i for i in peek_iter(a)]
+ b = list(peek_iter(a))
self.assertEqual(['1', '2'], b)
a = ['1', '2', '3']
- b = [i for i in peek_iter(a)]
+ b = list(peek_iter(a))
self.assertEqual(['1', '2', '3'], b)
def test_next_with_multi(self):
@@ -303,7 +303,7 @@ def get_next():
return next(a)
it = modify_iter(get_next, sentinel, int)
expected = [1, 2, 3]
- self.assertEqual(expected, [i for i in it])
+ self.assertEqual(expected, list(it))
def test_init_with_sentinel_kwargs(self):
a = iter([1, 2, 3, 4])
@@ -313,13 +313,13 @@ def get_next():
return next(a)
it = modify_iter(get_next, sentinel, modifier=str)
expected = ['1', '2', '3']
- self.assertEqual(expected, [i for i in it])
+ self.assertEqual(expected, list(it))
def test_modifier_default(self):
a = ['', ' ', ' a ', 'b ', ' c', ' ', '']
it = modify_iter(a)
expected = ['', ' ', ' a ', 'b ', ' c', ' ', '']
- self.assertEqual(expected, [i for i in it])
+ self.assertEqual(expected, list(it))
def test_modifier_not_callable(self):
self.assertRaises(TypeError, modify_iter, [1], modifier='not_callable')
@@ -328,10 +328,10 @@ def test_modifier_rstrip(self):
a = ['', ' ', ' a ', 'b ', ' c', ' ', '']
it = modify_iter(a, modifier=lambda s: s.rstrip())
expected = ['', '', ' a', 'b', ' c', '', '']
- self.assertEqual(expected, [i for i in it])
+ self.assertEqual(expected, list(it))
def test_modifier_rstrip_unicode(self):
a = ['', ' ', ' a ', 'b ', ' c', ' ', '']
it = modify_iter(a, modifier=lambda s: s.rstrip())
expected = ['', '', ' a', 'b', ' c', '', '']
- self.assertEqual(expected, [i for i in it])
+ self.assertEqual(expected, list(it))
diff --git a/tests/test_markup.py b/tests/test_markup.py
index 9e6165a5f84..f15761c5e5d 100644
--- a/tests/test_markup.py
+++ b/tests/test_markup.py
@@ -106,8 +106,7 @@ def verify(rst, html_expected):
def verify_re_latex(app, parse):
def verify(rst, latex_expected):
document = parse(rst)
- app.builder = LaTeXBuilder(app)
- app.builder.set_environment(app.env)
+ app.builder = LaTeXBuilder(app, app.env)
app.builder.init()
theme = app.builder.themes.get('manual')
latex_translator = ForgivingLaTeXTranslator(document, app.builder, theme)
diff --git a/tests/test_pycode_ast.py b/tests/test_pycode_ast.py
index 6143105eb71..31018bacaa3 100644
--- a/tests/test_pycode_ast.py
+++ b/tests/test_pycode_ast.py
@@ -25,7 +25,7 @@
("...", "..."), # Ellipsis
("a // b", "a // b"), # FloorDiv
("Tuple[int, int]", "Tuple[int, int]"), # Index, Subscript
- ("~ 1", "~ 1"), # Invert
+ ("~1", "~1"), # Invert
("lambda x, y: x + y",
"lambda x, y: ..."), # Lambda
("[1, 2, 3]", "[1, 2, 3]"), # List
@@ -37,14 +37,14 @@
("1234", "1234"), # Num
("not a", "not a"), # Not
("a or b", "a or b"), # Or
- ("a ** b", "a ** b"), # Pow
+ ("a**b", "a**b"), # Pow
("a >> b", "a >> b"), # RShift
("{1, 2, 3}", "{1, 2, 3}"), # Set
("a - b", "a - b"), # Sub
("'str'", "'str'"), # Str
- ("+ a", "+ a"), # UAdd
- ("- 1", "- 1"), # UnaryOp
- ("- a", "- a"), # USub
+ ("+a", "+a"), # UAdd
+ ("-1", "-1"), # UnaryOp
+ ("-a", "-a"), # USub
("(1, 2, 3)", "(1, 2, 3)"), # Tuple
("()", "()"), # Tuple (empty)
("(1,)", "(1,)"), # Tuple (single item)
diff --git a/tests/test_transforms_post_transforms.py b/tests/test_transforms_post_transforms.py
index 272d83e3a02..215e6d14fe2 100644
--- a/tests/test_transforms_post_transforms.py
+++ b/tests/test_transforms_post_transforms.py
@@ -48,3 +48,12 @@ def missing_reference(app, env, node, contnode):
content = (app.outdir / 'index.html').read_text(encoding='utf8')
assert 'Age' in content
+
+
+@pytest.mark.sphinx('html', testroot='transforms-post_transforms-keyboard',
+ freshenv=True)
+def test_keyboard_hyphen_spaces(app):
+ """Regression test for issue 10495, we want no crash."""
+ app.build()
+ assert "spanish" in (app.outdir / 'index.html').read_text(encoding='utf8')
+ assert "inquisition" in (app.outdir / 'index.html').read_text(encoding='utf8')
diff --git a/tests/test_util_logging.py b/tests/test_util_logging.py
index 49cd2c11e0c..b9756f9470c 100644
--- a/tests/test_util_logging.py
+++ b/tests/test_util_logging.py
@@ -2,13 +2,14 @@
import codecs
import os
+import os.path
import pytest
from docutils import nodes
from sphinx.errors import SphinxWarning
from sphinx.testing.util import strip_escseq
-from sphinx.util import logging
+from sphinx.util import logging, osutil
from sphinx.util.console import colorize
from sphinx.util.logging import is_suppressed_warning, prefixed_warnings
from sphinx.util.parallel import ParallelTasks
@@ -379,3 +380,18 @@ def test_prefixed_warnings(app, status, warning):
assert 'WARNING: Another PREFIX: message3' in warning.getvalue()
assert 'WARNING: PREFIX: message4' in warning.getvalue()
assert 'WARNING: message5' in warning.getvalue()
+
+
+def test_get_node_location_abspath():
+ # Ensure that node locations are reported as an absolute path,
+ # even if the source attribute is a relative path.
+
+ relative_filename = os.path.join('relative', 'path.txt')
+ absolute_filename = osutil.abspath(relative_filename)
+
+ n = nodes.Node()
+ n.source = relative_filename
+
+ location = logging.get_node_location(n)
+
+ assert location == absolute_filename + ':'
diff --git a/tox.ini b/tox.ini
index 105a02597ec..99ea3f9dcdd 100644
--- a/tox.ini
+++ b/tox.ini
@@ -81,9 +81,9 @@ basepython = python3
description =
Lint documentation.
extras =
- docs
+ lint
commands =
- python utils/doclinter.py CHANGES CONTRIBUTING.rst README.rst doc/
+ sphinx-lint --disable missing-space-after-literal --enable line-too-long --max-line-length 85 CHANGES CONTRIBUTING.rst README.rst doc/
[testenv:twine]
basepython = python3
diff --git a/utils/doclinter.py b/utils/doclinter.py
deleted file mode 100644
index d67a49b0510..00000000000
--- a/utils/doclinter.py
+++ /dev/null
@@ -1,77 +0,0 @@
-"""A linter for Sphinx docs"""
-
-import os
-import re
-import sys
-from typing import List
-
-MAX_LINE_LENGTH = 85
-LONG_INTERPRETED_TEXT = re.compile(r'^\s*\W*(:(\w+:)+)?`.*`\W*$')
-CODE_BLOCK_DIRECTIVE = re.compile(r'^(\s*)\.\. code-block::')
-LEADING_SPACES = re.compile(r'^(\s*)')
-
-
-def lint(path: str) -> int:
- with open(path, encoding='utf-8') as f:
- document = f.readlines()
-
- errors = 0
- in_code_block = False
- code_block_depth = 0
- for i, line in enumerate(document):
- if line.endswith(' '):
- print('%s:%d: the line ends with whitespace.' %
- (path, i + 1))
- errors += 1
-
- matched = CODE_BLOCK_DIRECTIVE.match(line)
- if matched:
- in_code_block = True
- code_block_depth = len(matched.group(1))
- elif in_code_block:
- if line.strip() == '':
- pass
- else:
- spaces = LEADING_SPACES.match(line).group(1)
- if len(spaces) <= code_block_depth:
- in_code_block = False
- elif LONG_INTERPRETED_TEXT.match(line):
- pass
- elif len(line) > MAX_LINE_LENGTH:
- if re.match(r'^\s*\.\. ', line):
- # ignore directives and hyperlink targets
- pass
- elif re.match(r'^\s*__ ', line):
- # ignore anonymous hyperlink targets
- pass
- elif re.match(r'^\s*``[^`]+``$', line):
- # ignore a very long literal string
- pass
- else:
- print('%s:%d: the line is too long (%d > %d).' %
- (path, i + 1, len(line), MAX_LINE_LENGTH))
- errors += 1
-
- return errors
-
-
-def main(args: List[str]) -> int:
- errors = 0
- for path in args:
- if os.path.isfile(path):
- errors += lint(path)
- elif os.path.isdir(path):
- for root, _dirs, files in os.walk(path):
- for filename in files:
- if filename.endswith('.rst'):
- path = os.path.join(root, filename)
- errors += lint(path)
-
- if errors:
- return 1
- else:
- return 0
-
-
-if __name__ == '__main__':
- sys.exit(main(sys.argv[1:]))