diff options
Diffstat (limited to '')
134 files changed, 2156 insertions, 510 deletions
diff --git a/tests/js/fixtures/cpp/searchindex.js b/tests/js/fixtures/cpp/searchindex.js new file mode 100644 index 0000000..46f4824 --- /dev/null +++ b/tests/js/fixtures/cpp/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {}, "docnames": ["index"], "envversion": {"sphinx": 62, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {"sphinx (c++ class)": [[0, "_CPPv46Sphinx", false]]}, "objects": {"": [[0, 0, 1, "_CPPv46Sphinx", "Sphinx"]]}, "objnames": {"0": ["cpp", "class", "C++ class"]}, "objtypes": {"0": "cpp:class"}, "terms": {"The": 0, "becaus": 0, "c": 0, "can": 0, "cardin": 0, "challeng": 0, "charact": 0, "class": 0, "descript": 0, "drop": 0, "engin": 0, "fixtur": 0, "frequent": 0, "gener": 0, "i": 0, "index": 0, "inflat": 0, "mathemat": 0, "occur": 0, "often": 0, "project": 0, "punctuat": 0, "queri": 0, "relat": 0, "sampl": 0, "search": 0, "size": 0, "sphinx": 0, "term": 0, "thei": 0, "thi": 0, "token": 0, "us": 0, "web": 0, "would": 0}, "titles": ["<no title>"], "titleterms": {}})
\ No newline at end of file diff --git a/tests/js/fixtures/multiterm/searchindex.js b/tests/js/fixtures/multiterm/searchindex.js new file mode 100644 index 0000000..a868eb6 --- /dev/null +++ b/tests/js/fixtures/multiterm/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"Main Page": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 62, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"At": 0, "adjac": 0, "all": 0, "an": 0, "appear": 0, "applic": 0, "ar": 0, "built": 0, "can": 0, "check": 0, "contain": 0, "do": 0, "document": 0, "doesn": 0, "each": 0, "fixtur": 0, "format": 0, "function": 0, "futur": 0, "html": 0, "i": 0, "includ": 0, "match": 0, "messag": 0, "multipl": 0, "multiterm": 0, "order": 0, "other": 0, "output": 0, "perform": 0, "perhap": 0, "phrase": 0, "project": 0, "queri": 0, "requir": 0, "same": 0, "search": 0, "successfulli": 0, "support": 0, "t": 0, "term": 0, "test": 0, "thi": 0, "time": 0, "us": 0, "when": 0, "write": 0}, "titles": ["Main Page"], "titleterms": {"main": 0, "page": 0}})
\ No newline at end of file diff --git a/tests/js/fixtures/partial/searchindex.js b/tests/js/fixtures/partial/searchindex.js new file mode 100644 index 0000000..356386a --- /dev/null +++ b/tests/js/fixtures/partial/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"sphinx_utils module": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 62, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"also": 0, "ar": 0, "built": 0, "confirm": 0, "document": 0, "function": 0, "html": 0, "i": 0, "includ": 0, "input": 0, "javascript": 0, "known": 0, "match": 0, "partial": 0, "possibl": 0, "prefix": 0, "project": 0, "provid": 0, "restructuredtext": 0, "sampl": 0, "search": 0, "should": 0, "thi": 0, "titl": 0, "us": 0, "when": 0}, "titles": ["sphinx_utils module"], "titleterms": {"modul": 0, "sphinx_util": 0}})
\ No newline at end of file diff --git a/tests/js/fixtures/titles/searchindex.js b/tests/js/fixtures/titles/searchindex.js new file mode 100644 index 0000000..9a229d0 --- /dev/null +++ b/tests/js/fixtures/titles/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"Main Page": [[0, null]], "Relevance": [[0, "relevance"], [1, null]]}, "docnames": ["index", "relevance"], "envversion": {"sphinx": 62, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst", "relevance.rst"], "indexentries": {"example (class in relevance)": [[0, "relevance.Example", false]], "module": [[0, "module-relevance", false]], "relevance": [[0, "module-relevance", false]], "relevance (relevance.example attribute)": [[0, "relevance.Example.relevance", false]]}, "objects": {"": [[0, 0, 0, "-", "relevance"]], "relevance": [[0, 1, 1, "", "Example"]], "relevance.Example": [[0, 2, 1, "", "relevance"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute"}, "terms": {"": [0, 1], "A": 1, "For": 1, "In": [0, 1], "against": 0, "also": 1, "an": 0, "answer": 0, "appear": 1, "ar": 1, "area": 0, "ask": 0, "attribut": 0, "built": 1, "can": [0, 1], "class": 0, "code": [0, 1], "consid": 1, "contain": 0, "context": 0, "corpu": 1, "could": 1, "demonstr": 0, "describ": 1, "detail": 1, "determin": 1, "docstr": 0, "document": [0, 1], "domain": 1, "engin": 0, "exampl": [0, 1], "extract": 0, "find": 0, "found": 0, "from": 0, "function": 1, "ha": 1, "handl": 0, "happen": 1, "head": 0, "help": 0, "highli": 1, "how": 0, "i": [0, 1], "improv": 0, "inform": 0, "intend": 0, "issu": 1, "itself": 1, "knowledg": 0, "languag": 1, "less": 1, "like": [0, 1], "match": 0, "mention": 1, "name": [0, 1], "object": 0, "one": 1, "onli": 1, "other": 0, "page": 1, "part": 1, "particular": 0, "printf": 1, "program": 1, "project": 0, "queri": [0, 1], "question": 0, "re": 0, "rel": 0, "research": 0, "result": 1, "sai": 0, "same": 1, "score": 0, "search": [0, 1], "seem": 0, "softwar": 1, "some": 1, "sphinx": 0, "straightforward": 1, "subject": 0, "subsect": 0, "term": [0, 1], "test": 0, "text": 0, "than": 1, "thei": 0, "them": 0, "thi": 0, "titl": 0, "user": [0, 1], "we": [0, 1], "when": 0, "whether": 1, "within": 0, "would": 1}, "titles": ["Main Page", "Relevance"], "titleterms": {"main": 0, "page": 0, "relev": [0, 1]}})
\ No newline at end of file diff --git a/tests/js/language_data.js b/tests/js/language_data.js new file mode 100644 index 0000000..89083d9 --- /dev/null +++ b/tests/js/language_data.js @@ -0,0 +1,26 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = []; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Dummy stemmer for languages without stemming rules. + */ +var Stemmer = function() { + this.stemWord = function(w) { + return w; + } +} + diff --git a/tests/js/roots/cpp/conf.py b/tests/js/roots/cpp/conf.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/js/roots/cpp/conf.py diff --git a/tests/js/roots/cpp/index.rst b/tests/js/roots/cpp/index.rst new file mode 100644 index 0000000..d731343 --- /dev/null +++ b/tests/js/roots/cpp/index.rst @@ -0,0 +1,10 @@ +This is a sample C++ project used to generate a search engine index fixture. + +.. cpp:class:: public Sphinx + + The description of Sphinx class. + +Indexing and querying the term C++ can be challenging, because search-related +tokenization often drops punctuation and mathematical characters (they occur +frequently on the web and would inflate the cardinality and size of web search +indexes). diff --git a/tests/js/roots/multiterm/conf.py b/tests/js/roots/multiterm/conf.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/js/roots/multiterm/conf.py diff --git a/tests/js/roots/multiterm/index.rst b/tests/js/roots/multiterm/index.rst new file mode 100644 index 0000000..495e5ce --- /dev/null +++ b/tests/js/roots/multiterm/index.rst @@ -0,0 +1,13 @@ +Main Page +========= + +This is the main page of the ``multiterm`` test project. + +This document is used as a test fixture to check that the search functionality +included when projects are built into an HTML output format can successfully +match this document when a search query containing multiple terms is performed. + +At the time-of-writing this message, the application doesn't support "phrase +queries" -- queries that require all of the contained terms to appear adjacent +to each other and in the same order in the document as in the query; perhaps it +will do in future? diff --git a/tests/js/roots/partial/conf.py b/tests/js/roots/partial/conf.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/js/roots/partial/conf.py diff --git a/tests/js/roots/partial/index.rst b/tests/js/roots/partial/index.rst new file mode 100644 index 0000000..6a9561b --- /dev/null +++ b/tests/js/roots/partial/index.rst @@ -0,0 +1,9 @@ +sphinx_utils module +=================== + +Partial (also known as "prefix") matches on document titles should be possible +using the JavaScript search functionality included when HTML documentation +projects are built. + +This document provides a sample reStructuredText input to confirm that partial +title matching is possible. diff --git a/tests/js/roots/titles/conf.py b/tests/js/roots/titles/conf.py new file mode 100644 index 0000000..e5f6bb9 --- /dev/null +++ b/tests/js/roots/titles/conf.py @@ -0,0 +1,6 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) + +extensions = ['sphinx.ext.autodoc'] diff --git a/tests/js/roots/titles/index.rst b/tests/js/roots/titles/index.rst new file mode 100644 index 0000000..464cd95 --- /dev/null +++ b/tests/js/roots/titles/index.rst @@ -0,0 +1,20 @@ +Main Page +========= + +This is the main page of the ``titles`` test project. + +In particular, this test project is intended to demonstrate how Sphinx +can handle scoring of query matches against document titles and subsection +heading titles relative to other document matches such as terms found within +document text and object names extracted from code. + +Relevance +--------- + +In the context of search engines, we can say that a document is **relevant** +to a user's query when it contains information that seems likely to help them +find an answer to a question they're asking, or to improve their knowledge of +the subject area they're researching. + +.. automodule:: relevance + :members: diff --git a/tests/js/roots/titles/relevance.py b/tests/js/roots/titles/relevance.py new file mode 100644 index 0000000..c4d0eec --- /dev/null +++ b/tests/js/roots/titles/relevance.py @@ -0,0 +1,7 @@ +class Example: + """Example class""" + num_attribute = 5 + text_attribute = "string" + + relevance = "testing" + """attribute docstring""" diff --git a/tests/js/roots/titles/relevance.rst b/tests/js/roots/titles/relevance.rst new file mode 100644 index 0000000..18f494f --- /dev/null +++ b/tests/js/roots/titles/relevance.rst @@ -0,0 +1,13 @@ +Relevance +========= + +In some domains, it can be straightforward to determine whether a search result +is relevant to the user's query. + +For example, if we are in a software programming language domain, and a user +has issued a query for the term ``printf``, then we could consider a document +in the corpus that describes a built-in language function with the same name +as (highly) relevant. A document that only happens to mention the ``printf`` +function name as part of some example code that appears on the page would +also be relevant, but likely less relevant than the one that describes the +function itself in detail. diff --git a/tests/js/searchtools.js b/tests/js/searchtools.js index 4f9984d..ebf37e5 100644 --- a/tests/js/searchtools.js +++ b/tests/js/searchtools.js @@ -1,20 +1,38 @@ describe('Basic html theme search', function() { + function loadFixture(name) { + req = new XMLHttpRequest(); + req.open("GET", `base/tests/js/fixtures/${name}`, false); + req.send(null); + return req.responseText; + } + + function checkRanking(expectedRanking, results) { + let [nextExpected, ...remainingItems] = expectedRanking; + + for (result of results.reverse()) { + if (!nextExpected) break; + + let [expectedPage, expectedTitle, expectedTarget] = nextExpected; + let [page, title, target] = result; + + if (page == expectedPage && title == expectedTitle && target == expectedTarget) { + [nextExpected, ...remainingItems] = remainingItems; + } + } + + expect(remainingItems.length).toEqual(0); + expect(nextExpected).toEqual(undefined); + } + describe('terms search', function() { it('should find "C++" when in index', function() { - index = { - docnames:["index"], - filenames:["index.rst"], - terms:{'c++':0}, - titles:["<no title>"], - titleterms:{} - } - Search.setIndex(index); - searchterms = ['c++']; - excluded = []; - terms = index.terms; - titleterms = index.titleterms; + eval(loadFixture("cpp/searchindex.js")); + + [_searchQuery, searchterms, excluded, ..._remainingItems] = Search._parseQuery('C++'); + terms = Search._index.terms; + titleterms = Search._index.titleterms; hits = [[ "index", @@ -28,22 +46,11 @@ describe('Basic html theme search', function() { }); it('should be able to search for multiple terms', function() { - index = { - alltitles: { - 'Main Page': [[0, 'main-page']], - }, - docnames:["index"], - filenames:["index.rst"], - terms:{main:0, page:0}, - titles:["Main Page"], - titleterms:{ main:0, page:0 } - } - Search.setIndex(index); + eval(loadFixture("multiterm/searchindex.js")); - searchterms = ['main', 'page']; - excluded = []; - terms = index.terms; - titleterms = index.titleterms; + [_searchQuery, searchterms, excluded, ..._remainingItems] = Search._parseQuery('main page'); + terms = Search._index.terms; + titleterms = Search._index.titleterms; hits = [[ 'index', 'Main Page', @@ -55,18 +62,11 @@ describe('Basic html theme search', function() { }); it('should partially-match "sphinx" when in title index', function() { - index = { - docnames:["index"], - filenames:["index.rst"], - terms:{'useful': 0, 'utilities': 0}, - titles:["sphinx_utils module"], - titleterms:{'sphinx_utils': 0} - } - Search.setIndex(index); - searchterms = ['sphinx']; - excluded = []; - terms = index.terms; - titleterms = index.titleterms; + eval(loadFixture("partial/searchindex.js")); + + [_searchQuery, searchterms, excluded, ..._remainingItems] = Search._parseQuery('sphinx'); + terms = Search._index.terms; + titleterms = Search._index.titleterms; hits = [[ "index", @@ -81,6 +81,88 @@ describe('Basic html theme search', function() { }); + describe('aggregation of search results', function() { + + it('should combine document title and document term matches', function() { + eval(loadFixture("multiterm/searchindex.js")); + + searchParameters = Search._parseQuery('main page'); + + hits = [ + [ + 'index', + 'Main Page', + '', + null, + 16, + 'index.rst' + ] + ]; + expect(Search._performSearch(...searchParameters)).toEqual(hits); + }); + + }); + + describe('search result ranking', function() { + + /* + * These tests should not proscribe precise expected ordering of search + * results; instead each test case should describe a single relevance rule + * that helps users to locate relevant information efficiently. + * + * If you think that one of the rules seems to be poorly-defined or is + * limiting the potential for search algorithm improvements, please check + * for existing discussion/bugreports related to it on GitHub[1] before + * creating one yourself. Suggestions for possible improvements are also + * welcome. + * + * [1] - https://github.com/sphinx-doc/sphinx.git/ + */ + + it('should score a code module match above a page-title match', function() { + eval(loadFixture("titles/searchindex.js")); + + expectedRanking = [ + ['index', 'relevance', '#module-relevance'], /* py:module documentation */ + ['relevance', 'Relevance', ''], /* main title */ + ]; + + searchParameters = Search._parseQuery('relevance'); + results = Search._performSearch(...searchParameters); + + checkRanking(expectedRanking, results); + }); + + it('should score a main-title match above an object member match', function() { + eval(loadFixture("titles/searchindex.js")); + + expectedRanking = [ + ['relevance', 'Relevance', ''], /* main title */ + ['index', 'relevance.Example.relevance', '#relevance.Example.relevance'], /* py:class attribute */ + ]; + + searchParameters = Search._parseQuery('relevance'); + results = Search._performSearch(...searchParameters); + + checkRanking(expectedRanking, results); + }); + + it('should score a main-title match above a subheading-title match', function() { + eval(loadFixture("titles/searchindex.js")); + + expectedRanking = [ + ['relevance', 'Relevance', ''], /* main title */ + ['index', 'Main Page > Relevance', '#relevance'], /* subsection heading title */ + ]; + + searchParameters = Search._parseQuery('relevance'); + results = Search._performSearch(...searchParameters); + + checkRanking(expectedRanking, results); + }); + + }); + }); describe("htmlToText", function() { @@ -100,15 +182,15 @@ describe("htmlToText", function() { </style> <!-- main content --> <section id="getting-started"> - <h1>Getting Started</h1> + <h1>Getting Started <a class="headerlink" href="#getting-started" title="Link to this heading">¶</a></h1> <p>Some text</p> </section> <section id="other-section"> - <h1>Other Section</h1> + <h1>Other Section <a class="headerlink" href="#other-section" title="Link to this heading">¶</a></h1> <p>Other text</p> </section> <section id="yet-another-section"> - <h1>Yet Another Section</h1> + <h1>Yet Another Section <a class="headerlink" href="#yet-another-section" title="Link to this heading">¶</a></h1> <p>More text</p> </section> </div> diff --git a/tests/roots/test-add_source_parser-conflicts-with-users-setting/conf.py b/tests/roots/test-add_source_parser-conflicts-with-users-setting/conf.py index 3ad5491..5e57901 100644 --- a/tests/roots/test-add_source_parser-conflicts-with-users-setting/conf.py +++ b/tests/roots/test-add_source_parser-conflicts-with-users-setting/conf.py @@ -11,7 +11,10 @@ class DummyTestParser(Parser): extensions = ['source_parser'] -source_suffix = ['.rst', '.test'] +source_suffix = { + '.rst': 'restructuredtext', + '.test': 'restructuredtext', +} source_parsers = { '.test': DummyTestParser } diff --git a/tests/roots/test-add_source_parser/conf.py b/tests/roots/test-add_source_parser/conf.py index 2acd4d2..ef85560 100644 --- a/tests/roots/test-add_source_parser/conf.py +++ b/tests/roots/test-add_source_parser/conf.py @@ -5,4 +5,3 @@ sys.path.insert(0, os.path.abspath('.')) extensions = ['source_parser'] -source_suffix = ['.rst'] diff --git a/tests/roots/test-autosummary/conf.py b/tests/roots/test-autosummary/conf.py index 46cf4fa..f459017 100644 --- a/tests/roots/test-autosummary/conf.py +++ b/tests/roots/test-autosummary/conf.py @@ -5,8 +5,6 @@ sys.path.insert(0, os.path.abspath('.')) extensions = ['sphinx.ext.autosummary'] -# The suffix of source filenames. -source_suffix = '.rst' autosummary_generate = True exclude_patterns = ['_build'] diff --git a/tests/roots/test-build-text/conf.py b/tests/roots/test-build-text/conf.py index fd9eefb..b0fdaf8 100644 --- a/tests/roots/test-build-text/conf.py +++ b/tests/roots/test-build-text/conf.py @@ -1,2 +1,4 @@ -source_suffix = '.txt' +source_suffix = { + '.txt': 'restructuredtext' +} exclude_patterns = ['_build'] diff --git a/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst b/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst index 75e4683..9715500 100644 --- a/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst +++ b/tests/roots/test-domain-py-python_maximum_signature_line_length/index.rst @@ -4,3 +4,16 @@ domain-py-maximum_signature_line_length .. py:function:: hello(name: str) -> str .. py:function:: foo([a, [b, ]]c, d[, e, f]) + +.. py:function:: generic_arg[T] + +.. py:function:: generic_foo[T]() + +.. py:function:: generic_bar[T](x: list[T]) + +.. py:function:: generic_ret[R]() -> R + +.. py:class:: MyGenericClass[X] + +.. py:class:: MyList[T](list[T]) + diff --git a/tests/roots/test-domain-py/index.rst b/tests/roots/test-domain-py/index.rst index b24bbea..71e45f7 100644 --- a/tests/roots/test-domain-py/index.rst +++ b/tests/roots/test-domain-py/index.rst @@ -8,3 +8,4 @@ test-domain-py module_option abbr canonical + type_alias diff --git a/tests/roots/test-domain-py/module.rst b/tests/roots/test-domain-py/module.rst index 70098f6..307e786 100644 --- a/tests/roots/test-domain-py/module.rst +++ b/tests/roots/test-domain-py/module.rst @@ -64,3 +64,6 @@ module .. py:data:: test2 :type: typing.Literal[-2] + +.. py:type:: MyType1 + :canonical: list[int | str] diff --git a/tests/roots/test-domain-py/roles.rst b/tests/roots/test-domain-py/roles.rst index 6bff2d2..d3492ce 100644 --- a/tests/roots/test-domain-py/roles.rst +++ b/tests/roots/test-domain-py/roles.rst @@ -5,14 +5,19 @@ roles .. py:method:: top_level +.. py:type:: TopLevelType + * :py:class:`TopLevel` * :py:meth:`top_level` +* :py:type:`TopLevelType` .. py:class:: NestedParentA * Link to :py:meth:`child_1` + .. py:type:: NestedTypeA + .. py:method:: child_1() * Link to :py:meth:`NestedChildA.subchild_2` @@ -46,3 +51,4 @@ roles * Link to :py:class:`NestedParentB` * :py:class:`NestedParentA.NestedChildA` +* :py:type:`NestedParentA.NestedTypeA` diff --git a/tests/roots/test-domain-py/type_alias.rst b/tests/roots/test-domain-py/type_alias.rst new file mode 100644 index 0000000..6a3df44 --- /dev/null +++ b/tests/roots/test-domain-py/type_alias.rst @@ -0,0 +1,15 @@ +Type Alias +========== + +.. py:module:: module_two + + .. py:class:: SomeClass + +:py:type:`.MyAlias` +:any:`MyAlias` +:any:`module_one.MyAlias` + +.. py:module:: module_one + + .. py:type:: MyAlias + :canonical: list[int | module_two.SomeClass] diff --git a/tests/roots/test-ext-autodoc/conf.py b/tests/roots/test-ext-autodoc/conf.py index 979a709..9d1cdc7 100644 --- a/tests/roots/test-ext-autodoc/conf.py +++ b/tests/roots/test-ext-autodoc/conf.py @@ -5,9 +5,6 @@ sys.path.insert(0, os.path.abspath('.')) extensions = ['sphinx.ext.autodoc'] -# The suffix of source filenames. -source_suffix = '.rst' - autodoc_mock_imports = [ 'dummy' ] diff --git a/tests/roots/test-ext-autodoc/target/annotated.py b/tests/roots/test-ext-autodoc/target/annotated.py index 5b87518..7adc3e0 100644 --- a/tests/roots/test-ext-autodoc/target/annotated.py +++ b/tests/roots/test-ext-autodoc/target/annotated.py @@ -1,8 +1,42 @@ -from __future__ import annotations +# from __future__ import annotations +import dataclasses +import types from typing import Annotated +@dataclasses.dataclass(frozen=True) +class FuncValidator: + func: types.FunctionType + + +@dataclasses.dataclass(frozen=True) +class MaxLen: + max_length: int + whitelisted_words: list[str] + + +def validate(value: str) -> str: + return value + + +#: Type alias for a validated string. +ValidatedString = Annotated[str, FuncValidator(validate)] + + def hello(name: Annotated[str, "attribute"]) -> None: """docstring""" pass + + +class AnnotatedAttributes: + """docstring""" + + #: Docstring about the ``name`` attribute. + name: Annotated[str, "attribute"] + + #: Docstring about the ``max_len`` attribute. + max_len: list[Annotated[str, MaxLen(10, ['word_one', 'word_two'])]] + + #: Docstring about the ``validated`` attribute. + validated: ValidatedString diff --git a/tests/roots/test-ext-autosummary-import_cycle/conf.py b/tests/roots/test-ext-autosummary-import_cycle/conf.py new file mode 100644 index 0000000..5e889f9 --- /dev/null +++ b/tests/roots/test-ext-autosummary-import_cycle/conf.py @@ -0,0 +1,7 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) + +extensions = ['sphinx.ext.autosummary'] +autosummary_generate = False diff --git a/tests/roots/test-ext-autosummary-import_cycle/index.rst b/tests/roots/test-ext-autosummary-import_cycle/index.rst new file mode 100644 index 0000000..14e7266 --- /dev/null +++ b/tests/roots/test-ext-autosummary-import_cycle/index.rst @@ -0,0 +1,6 @@ +.. automodule:: spam.eggs + :members: + + .. autosummary:: + + spam.eggs.Ham diff --git a/tests/roots/test-ext-autosummary-import_cycle/spam/__init__.py b/tests/roots/test-ext-autosummary-import_cycle/spam/__init__.py new file mode 100644 index 0000000..e94cf4b --- /dev/null +++ b/tests/roots/test-ext-autosummary-import_cycle/spam/__init__.py @@ -0,0 +1 @@ +"""``spam`` module docstring.""" diff --git a/tests/roots/test-ext-autosummary-import_cycle/spam/eggs.py b/tests/roots/test-ext-autosummary-import_cycle/spam/eggs.py new file mode 100644 index 0000000..12122e8 --- /dev/null +++ b/tests/roots/test-ext-autosummary-import_cycle/spam/eggs.py @@ -0,0 +1,10 @@ +"""``spam.eggs`` module docstring.""" + +import spam # Required for test. + + +class Ham: + """``spam.eggs.Ham`` class docstring.""" + a = 1 + b = 2 + c = 3 diff --git a/tests/roots/test-ext-autosummary-module_prefix/conf.py b/tests/roots/test-ext-autosummary-module_prefix/conf.py new file mode 100644 index 0000000..1065b91 --- /dev/null +++ b/tests/roots/test-ext-autosummary-module_prefix/conf.py @@ -0,0 +1,8 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) + +extensions = [ + 'sphinx.ext.autosummary', +] diff --git a/tests/roots/test-ext-autosummary-module_prefix/index.rst b/tests/roots/test-ext-autosummary-module_prefix/index.rst new file mode 100644 index 0000000..fe0b13c --- /dev/null +++ b/tests/roots/test-ext-autosummary-module_prefix/index.rst @@ -0,0 +1,5 @@ +.. autosummary:: + :toctree: docs/pkg + :recursive: + + pkg diff --git a/tests/roots/test-ext-autosummary-module_prefix/pkg/__init__.py b/tests/roots/test-ext-autosummary-module_prefix/pkg/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/roots/test-ext-autosummary-module_prefix/pkg/__init__.py diff --git a/tests/roots/test-ext-autosummary-module_prefix/pkg/mod0/__init__.py b/tests/roots/test-ext-autosummary-module_prefix/pkg/mod0/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/roots/test-ext-autosummary-module_prefix/pkg/mod0/__init__.py diff --git a/tests/roots/test-ext-autosummary-module_prefix/pkg/mod1/__init__.py b/tests/roots/test-ext-autosummary-module_prefix/pkg/mod1/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/roots/test-ext-autosummary-module_prefix/pkg/mod1/__init__.py diff --git a/tests/roots/test-ext-autosummary/conf.py b/tests/roots/test-ext-autosummary/conf.py index 55c769c..1c0d022 100644 --- a/tests/roots/test-ext-autosummary/conf.py +++ b/tests/roots/test-ext-autosummary/conf.py @@ -5,6 +5,3 @@ sys.path.insert(0, os.path.abspath('.')) extensions = ['sphinx.ext.autosummary'] autosummary_generate = True - -# The suffix of source filenames. -source_suffix = '.rst' diff --git a/tests/roots/test-ext-coverage/conf.py b/tests/roots/test-ext-coverage/conf.py index d3ec6e8..70fd03e 100644 --- a/tests/roots/test-ext-coverage/conf.py +++ b/tests/roots/test-ext-coverage/conf.py @@ -5,8 +5,11 @@ sys.path.insert(0, os.path.abspath('.')) extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage'] +coverage_modules = [ + 'grog', +] coverage_ignore_pyobjects = [ - r'^coverage_ignored(\..*)?$', + r'^grog\.coverage_ignored(\..*)?$', r'\.Ignored$', r'\.Documented\.ignored\d$', ] diff --git a/tests/roots/test-ext-coverage/grog/__init__.py b/tests/roots/test-ext-coverage/grog/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/roots/test-ext-coverage/grog/__init__.py diff --git a/tests/roots/test-ext-coverage/coverage_ignored.py b/tests/roots/test-ext-coverage/grog/coverage_ignored.py index b762955..b762955 100644 --- a/tests/roots/test-ext-coverage/coverage_ignored.py +++ b/tests/roots/test-ext-coverage/grog/coverage_ignored.py diff --git a/tests/roots/test-ext-coverage/grog/coverage_missing.py b/tests/roots/test-ext-coverage/grog/coverage_missing.py new file mode 100644 index 0000000..2fe4433 --- /dev/null +++ b/tests/roots/test-ext-coverage/grog/coverage_missing.py @@ -0,0 +1,7 @@ +"""This module is intentionally not documented.""" + +class Missing: + """An undocumented class.""" + + def missing_a(self): + """An undocumented method.""" diff --git a/tests/roots/test-ext-coverage/coverage_not_ignored.py b/tests/roots/test-ext-coverage/grog/coverage_not_ignored.py index b762955..b762955 100644 --- a/tests/roots/test-ext-coverage/coverage_not_ignored.py +++ b/tests/roots/test-ext-coverage/grog/coverage_not_ignored.py diff --git a/tests/roots/test-ext-coverage/index.rst b/tests/roots/test-ext-coverage/index.rst index b846898..85dccf9 100644 --- a/tests/roots/test-ext-coverage/index.rst +++ b/tests/roots/test-ext-coverage/index.rst @@ -1,6 +1,6 @@ -.. automodule:: coverage_ignored +.. automodule:: grog.coverage_ignored :members: -.. automodule:: coverage_not_ignored +.. automodule:: grog.coverage_not_ignored :members: diff --git a/tests/roots/test-ext-doctest-skipif/conf.py b/tests/roots/test-ext-doctest-skipif/conf.py index cd8f3eb..ae00e35 100644 --- a/tests/roots/test-ext-doctest-skipif/conf.py +++ b/tests/roots/test-ext-doctest-skipif/conf.py @@ -2,7 +2,9 @@ extensions = ['sphinx.ext.doctest'] project = 'test project for the doctest :skipif: directive' root_doc = 'skipif' -source_suffix = '.txt' +source_suffix = { + '.txt': 'restructuredtext' +} exclude_patterns = ['_build'] doctest_global_setup = ''' diff --git a/tests/roots/test-ext-doctest/conf.py b/tests/roots/test-ext-doctest/conf.py index d0e8b10..57fc406 100644 --- a/tests/roots/test-ext-doctest/conf.py +++ b/tests/roots/test-ext-doctest/conf.py @@ -2,5 +2,7 @@ extensions = ['sphinx.ext.doctest'] project = 'test project for doctest' root_doc = 'doctest' -source_suffix = '.txt' +source_suffix = { + '.txt': 'restructuredtext' +} exclude_patterns = ['_build'] diff --git a/tests/roots/test-html_assets/extra/API.html_t b/tests/roots/test-html_assets/extra/API.html.jinja index 34ecd9d..34ecd9d 100644 --- a/tests/roots/test-html_assets/extra/API.html_t +++ b/tests/roots/test-html_assets/extra/API.html.jinja diff --git a/tests/roots/test-html_assets/static/API.html_t b/tests/roots/test-html_assets/static/API.html.jinja index 34ecd9d..34ecd9d 100644 --- a/tests/roots/test-html_assets/static/API.html_t +++ b/tests/roots/test-html_assets/static/API.html.jinja diff --git a/tests/roots/test-images/index.rst b/tests/roots/test-images/index.rst index 9b9aac1..f6d7160 100644 --- a/tests/roots/test-images/index.rst +++ b/tests/roots/test-images/index.rst @@ -27,3 +27,8 @@ test-image .. non-exist remote image .. image:: http://localhost:7777/NOT_EXIST.PNG + +.. a self-contained image within a data URI + This image was generated using ImageMagick 6.9 with the command ``convert -pointsize 32 -font Noto-Sans-Egyptian-Hieroglyphs-Regular caption:$(printf '\U13080') -trim -border 2 -monochrome eoh.png`` +.. image:: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAAAjAQAAAADKt6U+AAAAAmJLR0QAAd2KE6QAAAAHdElNRQfoBQIVBgOBlOMTAAAAEGNhTnYAAAAtAAAAOwAAAAEAAAATst46RgAAAJtJREFUCNdNz70KwkAMAOA8iOhjuGh9HB9BCtoTHHwMH0Mc7KWTmx0dHDpovUk6HCil3sUmATHLR/4IAeJA+LEWPmbEeHJMWbTMZDA0CNFn8x1COFPaIHQ55R7hlZGdIjwj2aovRjJbhPvMLNN+r0g2vB7ByIWbHqqVh3LR3lhZWM0qYV8qjU6+lc4J7ZVx4SjEINBKOSinv/+YL1xvsJE6ztdqAAAADHRFWHRjYXB0aW9uAPCTgoD4hdKUAAAAD3RFWHRjYXB0aW9uOmxpbmVzADGoBz2RAAAAAElFTkSuQmCC + :alt: The Eye of Horus in a black font on a white background. diff --git a/tests/roots/test-inheritance/conf.py b/tests/roots/test-inheritance/conf.py index 26cadca..9953494 100644 --- a/tests/roots/test-inheritance/conf.py +++ b/tests/roots/test-inheritance/conf.py @@ -4,4 +4,3 @@ import sys sys.path.insert(0, os.path.abspath('.')) extensions = ['sphinx.ext.inheritance_diagram'] -source_suffix = '.rst' diff --git a/tests/roots/test-intl/conf.py b/tests/roots/test-intl/conf.py index 96ac664..09c47bb 100644 --- a/tests/roots/test-intl/conf.py +++ b/tests/roots/test-intl/conf.py @@ -1,5 +1,7 @@ project = 'Sphinx intl <Tests>' -source_suffix = '.txt' +source_suffix = { + '.txt': 'restructuredtext' +} keep_warnings = True templates_path = ['_templates'] html_additional_pages = {'contents': 'contents.html'} diff --git a/tests/roots/test-intl/glossary_terms_inconsistency.txt b/tests/roots/test-intl/glossary_terms_inconsistency.txt index 837411b..0de1e7e 100644 --- a/tests/roots/test-intl/glossary_terms_inconsistency.txt +++ b/tests/roots/test-intl/glossary_terms_inconsistency.txt @@ -4,3 +4,4 @@ i18n with glossary terms inconsistency ====================================== 1. link to :term:`Some term` and :term:`Some other term`. +2. link to :term:`Some term`. diff --git a/tests/roots/test-intl/index.txt b/tests/roots/test-intl/index.txt index 9de15d5..ac68314 100644 --- a/tests/roots/test-intl/index.txt +++ b/tests/roots/test-intl/index.txt @@ -31,6 +31,7 @@ CONTENTS section translation_progress topic + markup .. toctree:: :maxdepth: 2 diff --git a/tests/roots/test-intl/markup.txt b/tests/roots/test-intl/markup.txt new file mode 100644 index 0000000..d167a04 --- /dev/null +++ b/tests/roots/test-intl/markup.txt @@ -0,0 +1,6 @@ +i18n with strange markup +======================== + +1. title starting with 1. +------------------------- + diff --git a/tests/roots/test-intl/role_xref.txt b/tests/roots/test-intl/role_xref.txt index 2919b5c..f39e752 100644 --- a/tests/roots/test-intl/role_xref.txt +++ b/tests/roots/test-intl/role_xref.txt @@ -7,6 +7,9 @@ i18n role xref link to :term:`Some term`, :ref:`i18n-role-xref`, :doc:`index`. +link to :term:`Some term`, :ref:`i18n-role-xref`, :doc:`index`. +--------------------------------------------------------------- + .. _same-type-links: same type links diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms_inconsistency.po b/tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms_inconsistency.po index ef2bf30..048b81f 100644 --- a/tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms_inconsistency.po +++ b/tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms_inconsistency.po @@ -21,3 +21,6 @@ msgstr "I18N WITH GLOSSARY TERMS INCONSISTENCY" msgid "link to :term:`Some term` and :term:`Some other term`."
msgstr "LINK TO :term:`SOME NEW TERM`."
+
+msgid "link to :term:`Some term`."
+msgstr "LINK TO :term:`TERM NOT IN GLOSSARY`."
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/literalblock.po b/tests/roots/test-intl/xx/LC_MESSAGES/literalblock.po index 8d3e5d8..d320d95 100644 --- a/tests/roots/test-intl/xx/LC_MESSAGES/literalblock.po +++ b/tests/roots/test-intl/xx/LC_MESSAGES/literalblock.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: sphinx 1.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-11-22 08:28+0000\n" +"POT-Creation-Date: 2024-04-14 15:05+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -22,6 +22,11 @@ msgstr "I18N WITH LITERAL BLOCK" msgid "Correct literal block::" msgstr "CORRECT LITERAL BLOCK::" +msgid "" +"this is\n" +"literal block" +msgstr "THIS IS\nLITERAL BLOCK" + msgid "Missing literal block::" msgstr "MISSING LITERAL BLOCK::" @@ -31,6 +36,25 @@ msgstr "THAT'S ALL." msgid "included raw.txt" msgstr "INCLUDED RAW.TXT" +msgid "" +"===\n" +"Raw\n" +"===\n" +"\n" +".. raw:: html\n" +"\n" +" <iframe src=\"https://sphinx-doc.org\"></iframe>\n" +"\n" +msgstr "" +"===\n" +"RAW\n" +"===\n" +"\n" +".. raw:: html\n" +"\n" +" <iframe src=\"HTTPS://SPHINX-DOC.ORG\"></iframe>\n" +"\n" + msgid "code blocks" msgstr "CODE-BLOCKS" @@ -43,9 +67,6 @@ msgstr "" " 'RESULT'\n" "end" -msgid "example of C language" -msgstr "EXAMPLE OF C LANGUAGE" - msgid "" "#include <stdlib.h>\n" "int main(int argc, char** argv)\n" @@ -59,6 +80,9 @@ msgstr "" " return 0;\n" "}" +msgid "example of C language" +msgstr "EXAMPLE OF C LANGUAGE" + msgid "" "#include <stdio.h>\n" "int main(int argc, char** argv)\n" diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/markup.po b/tests/roots/test-intl/xx/LC_MESSAGES/markup.po new file mode 100644 index 0000000..ad6de9b --- /dev/null +++ b/tests/roots/test-intl/xx/LC_MESSAGES/markup.po @@ -0,0 +1,25 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2018, dev +# This file is distributed under the same license as the sphinx package. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2018. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: sphinx 1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-05-06 16:44+0900\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.4.0\n" + +msgid "i18n with strange markup" +msgstr "I18N WITH STRANGE MARKUP" + +msgid "1. title starting with 1." +msgstr "1. TITLE STARTING WITH 1." + diff --git a/tests/roots/test-latex-figure-in-admonition/conf.py b/tests/roots/test-latex-figure-in-admonition/conf.py index a45d22e..3d8b7b5 100644 --- a/tests/roots/test-latex-figure-in-admonition/conf.py +++ b/tests/roots/test-latex-figure-in-admonition/conf.py @@ -1 +1,3 @@ +extensions = ['sphinx.ext.todo'] +todo_include_todos = True exclude_patterns = ['_build'] diff --git a/tests/roots/test-latex-figure-in-admonition/index.rst b/tests/roots/test-latex-figure-in-admonition/index.rst index e3d39d3..c3fcaab 100644 --- a/tests/roots/test-latex-figure-in-admonition/index.rst +++ b/tests/roots/test-latex-figure-in-admonition/index.rst @@ -3,7 +3,24 @@ Test Figure in Admonition .. caution:: - This uses a figure in an admonition. + This uses a figure in a caution directive. .. figure:: img.png +.. note:: + + This uses a figure in a note directive. + + .. figure:: img.png + +.. seealso:: + + This uses a figure in a seealso directive. + + .. figure:: img.png + +.. todo:: + + This uses a figure in a todo directive. + + .. figure:: img.png diff --git a/tests/roots/test-latex-table/_mytemplates/latex/longtable.tex_t b/tests/roots/test-latex-table/_mytemplates/latex/longtable.tex.jinja index e2cb1db..e2cb1db 100644 --- a/tests/roots/test-latex-table/_mytemplates/latex/longtable.tex_t +++ b/tests/roots/test-latex-table/_mytemplates/latex/longtable.tex.jinja diff --git a/tests/roots/test-latex-table/_mytemplates/latex/tabulary.tex_t b/tests/roots/test-latex-table/_mytemplates/latex/tabulary.tex_t new file mode 100644 index 0000000..7e6d425 --- /dev/null +++ b/tests/roots/test-latex-table/_mytemplates/latex/tabulary.tex_t @@ -0,0 +1 @@ +AU REVOIR, KANIGGETS diff --git a/tests/roots/test-linkcheck-anchors-ignore-for-url/index.rst b/tests/roots/test-linkcheck-anchors-ignore-for-url/index.rst index df287b4..02969b6 100644 --- a/tests/roots/test-linkcheck-anchors-ignore-for-url/index.rst +++ b/tests/roots/test-linkcheck-anchors-ignore-for-url/index.rst @@ -1,5 +1,6 @@ * `Example valid url, no anchor <http://localhost:7777/valid>`_ * `Example valid url, valid anchor <http://localhost:7777/valid#valid-anchor>`_ +* `Example valid url, valid quotable anchor <http://localhost:7777/valid#py:module::urllib.parse>`_ * `Example valid url, invalid anchor <http://localhost:7777/valid#invalid-anchor>`_ * `Example ignored url, no anchor <http://localhost:7777/ignored>`_ * `Example ignored url, invalid anchor <http://localhost:7777/ignored#invalid-anchor>`_ diff --git a/tests/roots/test-markup-rubric/conf.py b/tests/roots/test-markup-rubric/conf.py index e274bde..eccdbf7 100644 --- a/tests/roots/test-markup-rubric/conf.py +++ b/tests/roots/test-markup-rubric/conf.py @@ -1,3 +1,4 @@ latex_documents = [ ('index', 'test.tex', 'The basic Sphinx documentation for testing', 'Sphinx', 'report') ] +latex_toplevel_sectioning = 'section' diff --git a/tests/roots/test-markup-rubric/index.rst b/tests/roots/test-markup-rubric/index.rst index c2ae68a..f91b0f7 100644 --- a/tests/roots/test-markup-rubric/index.rst +++ b/tests/roots/test-markup-rubric/index.rst @@ -5,3 +5,35 @@ test-markup-rubric .. rubric:: This is a multiline rubric + +.. rubric:: A rubric with a class + :class: myclass + +.. rubric:: A rubric with a heading level 1 + :heading-level: 1 + :class: myclass + +.. rubric:: A rubric with a heading level 2 + :heading-level: 2 + :class: myclass + +.. rubric:: A rubric with a heading level 3 + :heading-level: 3 + :class: myclass + +.. rubric:: A rubric with a heading level 4 + :heading-level: 4 + :class: myclass + +.. rubric:: A rubric with a heading level 5 + :heading-level: 5 + :class: myclass + +.. rubric:: A rubric with a heading level 6 + :heading-level: 6 + :class: myclass + +.. rubric:: A rubric with a heading level 7 + :heading-level: 7 + :class: myclass + diff --git a/tests/roots/test-root/conf.py b/tests/roots/test-root/conf.py index a14ffaf..25c723b 100644 --- a/tests/roots/test-root/conf.py +++ b/tests/roots/test-root/conf.py @@ -17,7 +17,10 @@ jsmath_path = 'dummy.js' templates_path = ['_templates'] -source_suffix = ['.txt', '.add', '.foo'] +source_suffix = { + '.txt': 'restructuredtext', + '.foo': 'foo', +} project = 'Sphinx <Tests>' copyright = '1234-6789, copyright text credits' @@ -68,7 +71,7 @@ latex_elements = { shadowrule=1pt, shadowsep=10pt, shadowsize=10pt, - div.topic_border-width=2pt,% alias to shadowrule + div.topic_border-width=2pt,% alias to shadowrule div.topic_padding=6pt,% alias to shadowsep div.topic_box-shadow=5pt,% overrides/alias shadowsize % diff --git a/tests/roots/test-root/images.txt b/tests/roots/test-root/images.txt index 5a096dc..a07429a 100644 --- a/tests/roots/test-root/images.txt +++ b/tests/roots/test-root/images.txt @@ -18,5 +18,13 @@ Sphinx image handling .. an SVG image (for HTML at least) .. image:: svgimg.* +.. an SVG image using width with units +.. image:: svgimg.* + :width: 2cm + +.. an SVG image using height with units +.. image:: svgimg.* + :height: 2cm + .. an image with more than 1 dot in its file name .. image:: img.foo.png diff --git a/tests/roots/test-root/markup.txt b/tests/roots/test-root/markup.txt index ff677eb..91f4194 100644 --- a/tests/roots/test-root/markup.txt +++ b/tests/roots/test-root/markup.txt @@ -230,6 +230,19 @@ Tables with multirow and multicol: figure in table + * - .. warning:: + + warning in table + + * - .. seealso:: + + figure in a seealso in a table + + .. figure:: img.png + + with a caption + + and a legend Figures ------- diff --git a/tests/roots/test-templating/conf.py b/tests/roots/test-templating/conf.py index e03eaf1..7a2baed 100644 --- a/tests/roots/test-templating/conf.py +++ b/tests/roots/test-templating/conf.py @@ -1,5 +1,7 @@ project = 'Sphinx templating <Tests>' -source_suffix = '.txt' +source_suffix = { + '.txt': 'restructuredtext' +} keep_warnings = True templates_path = ['_templates'] release = version = '2013.120' diff --git a/tests/roots/test-theming/test_theme/staticfiles/static/legacytmpl.html_t b/tests/roots/test-theming/test_theme/staticfiles/static/legacytmpl.html_t new file mode 100644 index 0000000..8b505e2 --- /dev/null +++ b/tests/roots/test-theming/test_theme/staticfiles/static/legacytmpl.html_t @@ -0,0 +1,2 @@ +<!-- testing legacy _t static templates --> +<html><project>{{ project | lower | escape }}</project></html> diff --git a/tests/roots/test-theming/test_theme/staticfiles/static/statictmpl.html_t b/tests/roots/test-theming/test_theme/staticfiles/static/statictmpl.html.jinja index 4ab292b..4ab292b 100644 --- a/tests/roots/test-theming/test_theme/staticfiles/static/statictmpl.html_t +++ b/tests/roots/test-theming/test_theme/staticfiles/static/statictmpl.html.jinja diff --git a/tests/roots/test-toctree-domain-objects/document_scoping.rst b/tests/roots/test-toctree-domain-objects/document_scoping.rst new file mode 100644 index 0000000..49aba9e --- /dev/null +++ b/tests/roots/test-toctree-domain-objects/document_scoping.rst @@ -0,0 +1,23 @@ +Level 1 +======= + +.. py:class:: ClassLevel1a + ClassLevel1b + + .. py:method:: f() + +.. py:method:: ClassLevel1a.g() + +.. py:method:: ClassLevel1b.g() + +Level 2 +------- + +.. py:class:: ClassLevel2a + ClassLevel2b + + .. py:method:: f() + +.. py:method:: ClassLevel2a.g() + +.. py:method:: ClassLevel2b.g() diff --git a/tests/roots/test-toctree-domain-objects/index.rst b/tests/roots/test-toctree-domain-objects/index.rst index 77ee010..5f04172 100644 --- a/tests/roots/test-toctree-domain-objects/index.rst +++ b/tests/roots/test-toctree-domain-objects/index.rst @@ -4,3 +4,4 @@ :name: mastertoc domains + document_scoping diff --git a/tests/roots/test-util-copyasset_overwrite/conf.py b/tests/roots/test-util-copyasset_overwrite/conf.py new file mode 100644 index 0000000..bb91f31 --- /dev/null +++ b/tests/roots/test-util-copyasset_overwrite/conf.py @@ -0,0 +1,7 @@ +import os +import sys +sys.path.insert(0, os.path.abspath('.')) + +extensions = ['myext'] +html_static_path = ['user_static'] +html_theme = 'basic' diff --git a/tests/roots/test-util-copyasset_overwrite/index.rst b/tests/roots/test-util-copyasset_overwrite/index.rst new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/roots/test-util-copyasset_overwrite/index.rst diff --git a/tests/roots/test-util-copyasset_overwrite/myext.py b/tests/roots/test-util-copyasset_overwrite/myext.py new file mode 100644 index 0000000..544057c --- /dev/null +++ b/tests/roots/test-util-copyasset_overwrite/myext.py @@ -0,0 +1,22 @@ +from pathlib import Path + +from sphinx.util.fileutil import copy_asset + + +def _copy_asset_overwrite_hook(app): + css = app.outdir / '_static' / 'custom-styles.css' + # html_static_path is copied by default + assert css.read_text() == '/* html_static_path */\n', 'invalid default text' + # warning generated by here + copy_asset( + Path(__file__).parent.joinpath('myext_static', 'custom-styles.css'), + app.outdir / '_static', + ) + # This demonstrates the overwriting + assert css.read_text() == '/* extension styles */\n', 'overwriting failed' + return [] + + +def setup(app): + app.connect('html-collect-pages', _copy_asset_overwrite_hook) + app.add_css_file('custom-styles.css') diff --git a/tests/roots/test-util-copyasset_overwrite/myext_static/custom-styles.css b/tests/roots/test-util-copyasset_overwrite/myext_static/custom-styles.css new file mode 100644 index 0000000..9509354 --- /dev/null +++ b/tests/roots/test-util-copyasset_overwrite/myext_static/custom-styles.css @@ -0,0 +1 @@ +/* extension styles */ diff --git a/tests/roots/test-util-copyasset_overwrite/user_static/custom-styles.css b/tests/roots/test-util-copyasset_overwrite/user_static/custom-styles.css new file mode 100644 index 0000000..1b892b9 --- /dev/null +++ b/tests/roots/test-util-copyasset_overwrite/user_static/custom-styles.css @@ -0,0 +1 @@ +/* html_static_path */ diff --git a/tests/roots/test-versioning/conf.py b/tests/roots/test-versioning/conf.py index 6344cb0..d52d1f2 100644 --- a/tests/roots/test-versioning/conf.py +++ b/tests/roots/test-versioning/conf.py @@ -1,3 +1,5 @@ project = 'versioning test root' -source_suffix = '.txt' +source_suffix = { + '.txt': 'restructuredtext' +} exclude_patterns = ['_build'] diff --git a/tests/test_addnodes.py b/tests/test_addnodes.py index aa99343..b3f77ad 100644 --- a/tests/test_addnodes.py +++ b/tests/test_addnodes.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from collections.abc import Iterator -@pytest.fixture() +@pytest.fixture def sig_elements() -> Iterator[set[type[addnodes.desc_sig_element]]]: """Fixture returning the current ``addnodes.SIG_ELEMENTS`` set.""" original = addnodes.SIG_ELEMENTS.copy() # safe copy of the current nodes diff --git a/tests/test_application.py b/tests/test_application.py index 1fc49d6..9326ba5 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -96,7 +96,7 @@ def test_add_source_parser(app, status, warning): # .rst; only in :confval:`source_suffix` assert '.rst' not in app.registry.get_source_parsers() - assert app.registry.source_suffix['.rst'] is None + assert app.registry.source_suffix['.rst'] == 'restructuredtext' # .test; configured by API assert app.registry.source_suffix['.test'] == 'test' diff --git a/tests/test_builders/test_build.py b/tests/test_builders/test_build.py index 3f6d12c..0e649f7 100644 --- a/tests/test_builders/test_build.py +++ b/tests/test_builders/test_build.py @@ -21,7 +21,7 @@ def request_session_head(url, **kwargs): return response -@pytest.fixture() +@pytest.fixture def nonascii_srcdir(request, rootdir, sphinx_test_tempdir): # Build in a non-ASCII source dir test_name = '\u65e5\u672c\u8a9e' diff --git a/tests/test_builders/test_build_dirhtml.py b/tests/test_builders/test_build_dirhtml.py index dc5ab86..93609e3 100644 --- a/tests/test_builders/test_build_dirhtml.py +++ b/tests/test_builders/test_build_dirhtml.py @@ -28,13 +28,13 @@ def test_dirhtml(app, status, warning): invdata = InventoryFile.load(f, 'path/to', posixpath.join) assert 'index' in invdata.get('std:doc') - assert invdata['std:doc']['index'] == ('Python', '', 'path/to/', '-') + assert invdata['std:doc']['index'] == ('Project name not set', '', 'path/to/', '-') assert 'foo/index' in invdata.get('std:doc') - assert invdata['std:doc']['foo/index'] == ('Python', '', 'path/to/foo/', '-') + assert invdata['std:doc']['foo/index'] == ('Project name not set', '', 'path/to/foo/', '-') assert 'index' in invdata.get('std:label') - assert invdata['std:label']['index'] == ('Python', '', 'path/to/#index', '-') + assert invdata['std:label']['index'] == ('Project name not set', '', 'path/to/#index', '-') assert 'foo' in invdata.get('std:label') - assert invdata['std:label']['foo'] == ('Python', '', 'path/to/foo/#foo', 'foo/index') + assert invdata['std:label']['foo'] == ('Project name not set', '', 'path/to/foo/#foo', 'foo/index') diff --git a/tests/test_builders/test_build_epub.py b/tests/test_builders/test_build_epub.py index 6829f22..691ffcc 100644 --- a/tests/test_builders/test_build_epub.py +++ b/tests/test_builders/test_build_epub.py @@ -67,7 +67,7 @@ def test_build_epub(app): # toc.ncx toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').read_text(encoding='utf8')) - assert toc.find("./ncx:docTitle/ncx:text").text == 'Python' + assert toc.find("./ncx:docTitle/ncx:text").text == 'Project name not set' # toc.ncx / head meta = list(toc.find("./ncx:head")) @@ -91,11 +91,11 @@ def test_build_epub(app): # content.opf / metadata metadata = opf.find("./idpf:metadata") assert metadata.find("./dc:language").text == 'en' - assert metadata.find("./dc:title").text == 'Python' + assert metadata.find("./dc:title").text == 'Project name not set' assert metadata.find("./dc:description").text == 'unknown' - assert metadata.find("./dc:creator").text == 'unknown' + assert metadata.find("./dc:creator").text == 'Author name not set' assert metadata.find("./dc:contributor").text == 'unknown' - assert metadata.find("./dc:publisher").text == 'unknown' + assert metadata.find("./dc:publisher").text == 'Author name not set' assert metadata.find("./dc:rights").text is None assert metadata.find("./idpf:meta[@property='ibooks:version']").text is None assert metadata.find("./idpf:meta[@property='ibooks:specified-fonts']").text == 'true' @@ -171,7 +171,7 @@ def test_nested_toc(app): # toc.ncx toc = EPUBElementTree.fromstring((app.outdir / 'toc.ncx').read_bytes()) - assert toc.find("./ncx:docTitle/ncx:text").text == 'Python' + assert toc.find("./ncx:docTitle/ncx:text").text == 'Project name not set' # toc.ncx / navPoint def navinfo(elem): @@ -409,6 +409,7 @@ def test_copy_images(app, status, warning): images = {image.name for image in images_dir.rglob('*')} images.discard('python-logo.png') assert images == { + # 'ba30773957c3fe046897111afd65a80b81cad089.png', # epub: image from data:image/png URI in source 'img.png', 'rimg.png', 'rimg1.png', diff --git a/tests/test_builders/test_build_gettext.py b/tests/test_builders/test_build_gettext.py index ddc6d30..dc8f4c9 100644 --- a/tests/test_builders/test_build_gettext.py +++ b/tests/test_builders/test_build_gettext.py @@ -16,13 +16,12 @@ if sys.version_info[:2] >= (3, 11): else: from sphinx.util.osutil import _chdir as chdir -_MSGID_PATTERN = re.compile(r'msgid "(.*)"') +_MSGID_PATTERN = re.compile(r'msgid "((?:\n|.)*?)"\nmsgstr', re.MULTILINE) -def msgid_getter(msgid): - if m := _MSGID_PATTERN.search(msgid): - return m[1] - return None +def get_msgids(pot): + matches = _MSGID_PATTERN.findall(pot) + return [m.replace('"\n"', '') for m in matches[1:]] def test_Catalog_duplicated_message(): @@ -105,7 +104,7 @@ def test_gettext_index_entries(app): app.build(filenames=[app.srcdir / 'index_entries.txt']) pot = (app.outdir / 'index_entries.pot').read_text(encoding='utf8') - msg_ids = list(filter(None, map(msgid_getter, pot.splitlines()))) + msg_ids = get_msgids(pot) assert msg_ids == [ "i18n with index entries", @@ -134,7 +133,7 @@ def test_gettext_disable_index_entries(app): app.build(filenames=[app.srcdir / 'index_entries.txt']) pot = (app.outdir / 'index_entries.pot').read_text(encoding='utf8') - msg_ids = list(filter(None, map(msgid_getter, pot.splitlines()))) + msg_ids = get_msgids(pot) assert msg_ids == [ "i18n with index entries", @@ -200,7 +199,7 @@ def test_gettext_prolog_epilog_substitution(app): assert (app.outdir / 'prolog_epilog_substitution.pot').is_file() pot = (app.outdir / 'prolog_epilog_substitution.pot').read_text(encoding='utf8') - msg_ids = list(filter(None, map(msgid_getter, pot.splitlines()))) + msg_ids = get_msgids(pot) assert msg_ids == [ "i18n with prologue and epilogue substitutions", @@ -227,9 +226,43 @@ def test_gettext_prolog_epilog_substitution_excluded(app): assert (app.outdir / 'prolog_epilog_substitution_excluded.pot').is_file() pot = (app.outdir / 'prolog_epilog_substitution_excluded.pot').read_text(encoding='utf8') - msg_ids = list(filter(None, map(msgid_getter, pot.splitlines()))) + msg_ids = get_msgids(pot) assert msg_ids == [ "i18n without prologue and epilogue substitutions", "This is content that does not include prologue and epilogue substitutions.", ] + + +@pytest.mark.sphinx( + 'gettext', srcdir='gettext', + confoverrides={'gettext_compact': False, + 'gettext_additional_targets': ['literal-block', 'doctest-block']}) +def test_gettext_literalblock_additional(app): + app.build(force_all=True) + + assert (app.outdir / 'literalblock.pot').is_file() + pot = (app.outdir / 'literalblock.pot').read_text(encoding='utf8') + msg_ids = get_msgids(pot) + + assert msg_ids == [ + 'i18n with literal block', + 'Correct literal block::', + 'this is\\nliteral block', + 'Missing literal block::', + "That's all.", + 'included raw.txt', + '===\\nRaw\\n===\\n\\n.. raw:: html\\n\\n <iframe src=\\"https://sphinx-doc.org\\"></iframe>\\n\\n', + 'code blocks', + "def main\\n 'result'\\nend", + '#include <stdlib.h>\\nint main(int argc, char** argv)\\n{\\n return 0;\\n}', + 'example of C language', + '#include <stdio.h>\\nint main(int argc, char** argv)\\n{\\n return 0;\\n}', + 'literal-block\\nin list', + 'test_code_for_noqa()\\ncontinued()', + 'doctest blocks', + '>>> import sys # sys importing\\n>>> def main(): # define main ' + "function\\n... sys.stdout.write('hello') # call write method of " + "stdout object\\n>>>\\n>>> if __name__ == '__main__': # if run this py " + 'file as python script\\n... main() # call main', + ] diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py index 1fa3ba4..8db0790 100644 --- a/tests/test_builders/test_build_html.py +++ b/tests/test_builders/test_build_html.py @@ -1,5 +1,6 @@ """Test the HTML builder and check output against XPath.""" +import contextlib import os import posixpath import re @@ -8,7 +9,7 @@ import pytest from sphinx.builders.html import validate_html_extra_path, validate_html_static_path from sphinx.deprecation import RemovedInSphinx80Warning -from sphinx.errors import ConfigError +from sphinx.errors import ConfigError, ThemeError from sphinx.util.console import strip_colors from sphinx.util.inventory import InventoryFile @@ -16,6 +17,31 @@ from tests.test_builders.xpath_data import FIGURE_CAPTION from tests.test_builders.xpath_util import check_xpath +def test_html_sidebars_error(make_app, tmp_path): + (tmp_path / 'conf.py').touch() + (tmp_path / 'index.rst').touch() + app = make_app( + buildername='html', + srcdir=tmp_path, + confoverrides={'html_sidebars': {'index': 'searchbox.html'}}, + ) + + # Test that the error is logged + warnings = app.warning.getvalue() + assert ("ERROR: Values in 'html_sidebars' must be a list of strings. " + "At least one pattern has a string value: 'index'. " + "Change to `html_sidebars = {'index': ['searchbox.html']}`.") in warnings + + # But that the value is unchanged. + # (Remove this bit of the test in Sphinx 8) + def _html_context_hook(app, pagename, templatename, context, doctree): + assert context["sidebars"] == 'searchbox.html' + app.connect('html-page-context', _html_context_hook) + with contextlib.suppress(ThemeError): + # ignore template rendering issues (ThemeError). + app.build() + + def test_html4_error(make_app, tmp_path): (tmp_path / 'conf.py').write_text('', encoding='utf-8') with pytest.raises( @@ -131,24 +157,24 @@ def test_html_inventory(app): 'py-modindex', 'genindex', 'search'} - assert invdata['std:label']['modindex'] == ('Python', + assert invdata['std:label']['modindex'] == ('Project name not set', '', 'https://www.google.com/py-modindex.html', 'Module Index') - assert invdata['std:label']['py-modindex'] == ('Python', + assert invdata['std:label']['py-modindex'] == ('Project name not set', '', 'https://www.google.com/py-modindex.html', 'Python Module Index') - assert invdata['std:label']['genindex'] == ('Python', + assert invdata['std:label']['genindex'] == ('Project name not set', '', 'https://www.google.com/genindex.html', 'Index') - assert invdata['std:label']['search'] == ('Python', + assert invdata['std:label']['search'] == ('Project name not set', '', 'https://www.google.com/search.html', 'Search Page') assert set(invdata['std:doc'].keys()) == {'index'} - assert invdata['std:doc']['index'] == ('Python', + assert invdata['std:doc']['index'] == ('Project name not set', '', 'https://www.google.com/index.html', 'The basic Sphinx documentation for testing') @@ -222,8 +248,8 @@ def test_html_sidebar(app, status, warning): app.build(force_all=True) result = (app.outdir / 'index.html').read_text(encoding='utf8') assert ('<div class="sphinxsidebar" role="navigation" ' - 'aria-label="main navigation">' in result) - assert '<h1 class="logo"><a href="#">Python</a></h1>' in result + 'aria-label="Main">' in result) + assert '<h1 class="logo"><a href="#">Project name not set</a></h1>' in result assert '<h3>Navigation</h3>' in result assert '<h3>Related Topics</h3>' in result assert '<h3 id="searchlabel">Quick search</h3>' in result @@ -237,7 +263,7 @@ def test_html_sidebar(app, status, warning): app.build(force_all=True) result = (app.outdir / 'index.html').read_text(encoding='utf8') assert ('<div class="sphinxsidebar" role="navigation" ' - 'aria-label="main navigation">' in result) + 'aria-label="Main">' in result) assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result assert '<h3>Navigation</h3>' not in result assert '<h3>Related Topics</h3>' in result @@ -251,7 +277,7 @@ def test_html_sidebar(app, status, warning): app.build(force_all=True) result = (app.outdir / 'index.html').read_text(encoding='utf8') assert ('<div class="sphinxsidebar" role="navigation" ' - 'aria-label="main navigation">' not in result) + 'aria-label="Main">' not in result) assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result assert '<h3>Navigation</h3>' not in result assert '<h3>Related Topics</h3>' not in result @@ -376,3 +402,34 @@ def test_html_remove_sources_before_write_gh_issue_10786(app, warning): file = os.fsdecode(target) assert f'WARNING: cannot copy image file {file!r}: {file!s} does not exist' == ws[-1] + + +@pytest.mark.sphinx('html', testroot='domain-py-python_maximum_signature_line_length', + confoverrides={'python_maximum_signature_line_length': 1}) +def test_html_pep_695_one_type_per_line(app, cached_etree_parse): + app.build() + fname = app.outdir / 'index.html' + etree = cached_etree_parse(fname) + + class chk: + def __init__(self, expect): + self.expect = expect + + def __call__(self, nodes): + assert len(nodes) == 1, nodes + objnode = ''.join(nodes[0].itertext()).replace('\n\n', '') + objnode = objnode.rstrip(chr(182)) # remove '¶' symbol + objnode = objnode.strip('\n') # remove surrounding new lines + assert objnode == self.expect + + # each signature has a dangling ',' at the end of its parameters lists + check_xpath(etree, fname, r'.//dt[@id="generic_foo"][1]', + chk('generic_foo[\nT,\n]()')) + check_xpath(etree, fname, r'.//dt[@id="generic_bar"][1]', + chk('generic_bar[\nT,\n](\nx: list[T],\n)')) + check_xpath(etree, fname, r'.//dt[@id="generic_ret"][1]', + chk('generic_ret[\nR,\n]() → R')) + check_xpath(etree, fname, r'.//dt[@id="MyGenericClass"][1]', + chk('class MyGenericClass[\nX,\n]')) + check_xpath(etree, fname, r'.//dt[@id="MyList"][1]', + chk('class MyList[\nT,\n](list[T])')) diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py index ece6f49..388c324 100644 --- a/tests/test_builders/test_build_html_5_output.py +++ b/tests/test_builders/test_build_html_5_output.py @@ -3,6 +3,7 @@ import re import pytest +from docutils import nodes from tests.test_builders.xpath_util import check_xpath @@ -25,6 +26,9 @@ def tail_check(check): ('images.html', ".//img[@src='_images/simg.png']", ''), ('images.html', ".//img[@src='_images/svgimg.svg']", ''), ('images.html', ".//a[@href='_sources/images.txt']", ''), + # Check svg options + ('images.html', ".//img[@src='_images/svgimg.svg'][@style='width: 2cm;']", ''), + ('images.html', ".//img[@src='_images/svgimg.svg'][@style='height: 2cm;']", ''), ('subdir/images.html', ".//img[@src='../_images/img1.png']", ''), ('subdir/images.html', ".//img[@src='../_images/rimg.png']", ''), @@ -255,6 +259,8 @@ def tail_check(check): ('extensions.html', ".//a[@href='https://python.org/dev/']", "https://python.org/dev/"), ('extensions.html', ".//a[@href='https://bugs.python.org/issue1000']", "issue 1000"), ('extensions.html', ".//a[@href='https://bugs.python.org/issue1042']", "explicit caption"), + ('extensions.html', ".//a[@class='extlink-pyurl reference external']", "https://python.org/dev/"), + ('extensions.html', ".//a[@class='extlink-issue reference external']", "issue 1000"), # index entries ('genindex.html', ".//a/strong", "Main"), @@ -270,7 +276,32 @@ def tail_check(check): ]) @pytest.mark.sphinx('html', tags=['testtag'], confoverrides={'html_context.hckey_co': 'hcval_co'}) -@pytest.mark.test_params(shared_result='test_build_html_output') def test_html5_output(app, cached_etree_parse, fname, path, check): app.build() check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check) + + +@pytest.mark.sphinx('html', testroot='markup-rubric') +def test_html5_rubric(app): + def insert_invalid_rubric_heading_level(app, doctree, docname): + if docname != 'index': + return + new_node = nodes.rubric('', 'INSERTED RUBRIC') + new_node['heading-level'] = 7 + doctree[0].append(new_node) + + app.connect('doctree-resolved', insert_invalid_rubric_heading_level) + app.build() + + warnings = app.warning.getvalue() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert '<p class="rubric">This is a rubric</p>' in content + assert '<h2 class="myclass rubric">A rubric with a heading level 2</h2>' in content + + # directive warning + assert '"7" unknown' in warnings + + # html writer warning + assert 'WARNING: unsupported rubric heading level: 7' in warnings + assert '</h7>' not in content + assert '<p class="rubric">INSERTED RUBRIC</p>' in content diff --git a/tests/test_builders/test_build_html_assets.py b/tests/test_builders/test_build_html_assets.py index fc7a987..e2c7c75 100644 --- a/tests/test_builders/test_build_html_assets.py +++ b/tests/test_builders/test_build_html_assets.py @@ -34,7 +34,7 @@ def test_html_assets(app): # html_extra_path assert (app.outdir / '.htaccess').exists() assert not (app.outdir / '.htpasswd').exists() - assert (app.outdir / 'API.html_t').exists() + assert (app.outdir / 'API.html.jinja').exists() assert (app.outdir / 'css/style.css').exists() assert (app.outdir / 'rimg.png').exists() assert not (app.outdir / '_build' / 'index.html').exists() diff --git a/tests/test_builders/test_build_html_download.py b/tests/test_builders/test_build_html_download.py index 1201c66..14332d8 100644 --- a/tests/test_builders/test_build_html_download.py +++ b/tests/test_builders/test_build_html_download.py @@ -5,7 +5,6 @@ import pytest @pytest.mark.sphinx('html') -@pytest.mark.test_params(shared_result='test_build_html_output') def test_html_download(app): app.build() diff --git a/tests/test_builders/test_build_html_image.py b/tests/test_builders/test_build_html_image.py index 08ed618..860beb6 100644 --- a/tests/test_builders/test_build_html_image.py +++ b/tests/test_builders/test_build_html_image.py @@ -29,7 +29,7 @@ def test_html_remote_logo(app, status, warning): app.build(force_all=True) result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<img class="logo" src="https://www.python.org/static/img/python-logo.png" alt="Logo"/>' in result) + assert ('<img class="logo" src="https://www.python.org/static/img/python-logo.png" alt="Logo of Project name not set"/>' in result) assert ('<link rel="icon" href="https://www.python.org/static/favicon.ico"/>' in result) assert not (app.outdir / 'python-logo.png').exists() @@ -39,7 +39,7 @@ def test_html_local_logo(app, status, warning): app.build(force_all=True) result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<img class="logo" src="_static/img.png" alt="Logo"/>' in result) + assert ('<img class="logo" src="_static/img.png" alt="Logo of Project name not set"/>' in result) assert (app.outdir / '_static/img.png').exists() @@ -72,6 +72,7 @@ def test_copy_images(app, status, warning): images_dir = Path(app.outdir) / '_images' images = {image.name for image in images_dir.rglob('*')} assert images == { + # 'ba30773957c3fe046897111afd65a80b81cad089.png', # html: image from data:image/png URI in source 'img.png', 'rimg.png', 'rimg1.png', diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py index 0776c74..56505b4 100644 --- a/tests/test_builders/test_build_latex.py +++ b/tests/test_builders/test_build_latex.py @@ -41,7 +41,7 @@ def kpsetest(*filenames): # compile latex document with app.config.latex_engine -def compile_latex_document(app, filename='python.tex', docclass='manual'): +def compile_latex_document(app, filename='projectnamenotset.tex', docclass='manual'): # now, try to run latex over it try: with chdir(app.outdir): @@ -158,21 +158,21 @@ def test_writer(app, status, warning): assert ('\\begin{wrapfigure}{r}{0pt}\n\\centering\n' '\\noindent\\sphinxincludegraphics{{rimg}.png}\n' - '\\caption{figure with align option}\\label{\\detokenize{markup:id9}}' + '\\caption{figure with align option}\\label{\\detokenize{markup:id10}}' '\\end{wrapfigure}\n\n' '\\mbox{}\\par\\vskip-\\dimexpr\\baselineskip+\\parskip\\relax' in result) assert ('\\begin{wrapfigure}{r}{0.500\\linewidth}\n\\centering\n' '\\noindent\\sphinxincludegraphics{{rimg}.png}\n' '\\caption{figure with align \\& figwidth option}' - '\\label{\\detokenize{markup:id10}}' + '\\label{\\detokenize{markup:id11}}' '\\end{wrapfigure}\n\n' '\\mbox{}\\par\\vskip-\\dimexpr\\baselineskip+\\parskip\\relax' in result) assert ('\\begin{wrapfigure}{r}{3cm}\n\\centering\n' '\\noindent\\sphinxincludegraphics[width=3cm]{{rimg}.png}\n' '\\caption{figure with align \\& width option}' - '\\label{\\detokenize{markup:id11}}' + '\\label{\\detokenize{markup:id12}}' '\\end{wrapfigure}\n\n' '\\mbox{}\\par\\vskip-\\dimexpr\\baselineskip+\\parskip\\relax' in result) @@ -255,7 +255,7 @@ def test_latex_basic_howto_ja(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-theme') def test_latex_theme(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) assert r'\def\sphinxdocclass{book}' in result assert r'\documentclass[a4paper,12pt,english]{sphinxbook}' in result @@ -266,7 +266,7 @@ def test_latex_theme(app, status, warning): 'pointsize': '9pt'}}) def test_latex_theme_papersize(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) assert r'\def\sphinxdocclass{book}' in result assert r'\documentclass[b5paper,9pt,english]{sphinxbook}' in result @@ -277,7 +277,7 @@ def test_latex_theme_papersize(app, status, warning): 'pointsize': '9pt'}}) def test_latex_theme_options(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) assert r'\def\sphinxdocclass{book}' in result assert r'\documentclass[b5paper,9pt,english]{sphinxbook}' in result @@ -330,7 +330,7 @@ def test_latex_release(app, status, warning): confoverrides={'numfig': True}) def test_numref(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -372,7 +372,7 @@ def test_numref(app, status, warning): 'section': 'SECTION-%s'}}) def test_numref_with_prefix1(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -420,7 +420,7 @@ def test_numref_with_prefix1(app, status, warning): 'section': 'SECTION_%s_'}}) def test_numref_with_prefix2(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -460,7 +460,7 @@ def test_numref_with_prefix2(app, status, warning): confoverrides={'numfig': True, 'language': 'ja'}) def test_numref_with_language_ja(app, status, warning): app.build() - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -556,7 +556,7 @@ def test_latex_add_latex_package(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-babel') def test_babel_with_no_language_settings(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -581,7 +581,7 @@ def test_babel_with_no_language_settings(app, status, warning): confoverrides={'language': 'de'}) def test_babel_with_language_de(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -606,7 +606,7 @@ def test_babel_with_language_de(app, status, warning): confoverrides={'language': 'ru'}) def test_babel_with_language_ru(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -631,7 +631,7 @@ def test_babel_with_language_ru(app, status, warning): confoverrides={'language': 'tr'}) def test_babel_with_language_tr(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -656,7 +656,7 @@ def test_babel_with_language_tr(app, status, warning): confoverrides={'language': 'ja'}) def test_babel_with_language_ja(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -680,7 +680,7 @@ def test_babel_with_language_ja(app, status, warning): confoverrides={'language': 'unknown'}) def test_babel_with_unknown_language(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -707,7 +707,7 @@ def test_babel_with_unknown_language(app, status, warning): confoverrides={'language': 'de', 'latex_engine': 'lualatex'}) def test_polyglossia_with_language_de(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -733,7 +733,7 @@ def test_polyglossia_with_language_de(app, status, warning): confoverrides={'language': 'de-1901', 'latex_engine': 'lualatex'}) def test_polyglossia_with_language_de_1901(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -786,7 +786,7 @@ def test_footnote(app, status, warning): @pytest.mark.sphinx('latex', testroot='footnotes') def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -826,7 +826,7 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): @pytest.mark.sphinx('latex', testroot='footnotes') def test_footnote_referred_multiple_times(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -848,7 +848,7 @@ def test_footnote_referred_multiple_times(app, status, warning): confoverrides={'latex_show_urls': 'inline'}) def test_latex_show_urls_is_inline(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -905,7 +905,7 @@ def test_latex_show_urls_is_inline(app, status, warning): confoverrides={'latex_show_urls': 'footnote'}) def test_latex_show_urls_is_footnote(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -967,7 +967,7 @@ def test_latex_show_urls_is_footnote(app, status, warning): confoverrides={'latex_show_urls': 'no'}) def test_latex_show_urls_is_no(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1022,7 +1022,7 @@ def test_latex_show_urls_footnote_and_substitutions(app, status, warning): @pytest.mark.sphinx('latex', testroot='image-in-section') def test_image_in_section(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1045,7 +1045,7 @@ def test_latex_logo_if_not_found(app, status, warning): @pytest.mark.sphinx('latex', testroot='toctree-maxdepth') def test_toctree_maxdepth_manual(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1057,12 +1057,12 @@ def test_toctree_maxdepth_manual(app, status, warning): @pytest.mark.sphinx( 'latex', testroot='toctree-maxdepth', confoverrides={'latex_documents': [ - ('index', 'python.tex', 'Sphinx Tests Documentation', + ('index', 'projectnamenotset.tex', 'Sphinx Tests Documentation', 'Georg Brandl', 'howto'), ]}) def test_toctree_maxdepth_howto(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1076,7 +1076,7 @@ def test_toctree_maxdepth_howto(app, status, warning): confoverrides={'root_doc': 'foo'}) def test_toctree_not_found(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1090,7 +1090,7 @@ def test_toctree_not_found(app, status, warning): confoverrides={'root_doc': 'bar'}) def test_toctree_without_maxdepth(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1103,7 +1103,7 @@ def test_toctree_without_maxdepth(app, status, warning): confoverrides={'root_doc': 'qux'}) def test_toctree_with_deeper_maxdepth(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1116,7 +1116,7 @@ def test_toctree_with_deeper_maxdepth(app, status, warning): confoverrides={'latex_toplevel_sectioning': None}) def test_latex_toplevel_sectioning_is_None(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1128,7 +1128,7 @@ def test_latex_toplevel_sectioning_is_None(app, status, warning): confoverrides={'latex_toplevel_sectioning': 'part'}) def test_latex_toplevel_sectioning_is_part(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1141,12 +1141,12 @@ def test_latex_toplevel_sectioning_is_part(app, status, warning): 'latex', testroot='toctree-maxdepth', confoverrides={'latex_toplevel_sectioning': 'part', 'latex_documents': [ - ('index', 'python.tex', 'Sphinx Tests Documentation', + ('index', 'projectnamenotset.tex', 'Sphinx Tests Documentation', 'Georg Brandl', 'howto'), ]}) def test_latex_toplevel_sectioning_is_part_with_howto(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1160,7 +1160,7 @@ def test_latex_toplevel_sectioning_is_part_with_howto(app, status, warning): confoverrides={'latex_toplevel_sectioning': 'chapter'}) def test_latex_toplevel_sectioning_is_chapter(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1171,12 +1171,12 @@ def test_latex_toplevel_sectioning_is_chapter(app, status, warning): 'latex', testroot='toctree-maxdepth', confoverrides={'latex_toplevel_sectioning': 'chapter', 'latex_documents': [ - ('index', 'python.tex', 'Sphinx Tests Documentation', + ('index', 'projectnamenotset.tex', 'Sphinx Tests Documentation', 'Georg Brandl', 'howto'), ]}) def test_latex_toplevel_sectioning_is_chapter_with_howto(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1188,7 +1188,7 @@ def test_latex_toplevel_sectioning_is_chapter_with_howto(app, status, warning): confoverrides={'latex_toplevel_sectioning': 'section'}) def test_latex_toplevel_sectioning_is_section(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) @@ -1199,11 +1199,11 @@ def test_latex_toplevel_sectioning_is_section(app, status, warning): @pytest.mark.sphinx('latex', testroot='maxlistdepth') def test_maxlistdepth_at_ten(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) print(warning.getvalue()) - compile_latex_document(app, 'python.tex') + compile_latex_document(app, 'projectnamenotset.tex') @pytest.mark.sphinx('latex', testroot='latex-table', @@ -1211,7 +1211,7 @@ def test_maxlistdepth_at_ten(app, status, warning): @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_tabulars(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') tables = {} for chap in re.split(r'\\(?:section|chapter){', result)[1:]: sectname, content = chap.split('}', 1) @@ -1282,7 +1282,7 @@ def test_latex_table_tabulars(app, status, warning): @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_longtable(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') tables = {} for chap in re.split(r'\\(?:section|chapter){', result)[1:]: sectname, content = chap.split('}', 1) @@ -1343,7 +1343,7 @@ def test_latex_table_longtable(app, status, warning): @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_complex_tables(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') tables = {} for chap in re.split(r'\\(?:section|renewcommand){', result)[1:]: sectname, content = chap.split('}', 1) @@ -1373,7 +1373,7 @@ def test_latex_table_complex_tables(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-table') def test_latex_table_with_booktabs_and_colorrows(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert r'\PassOptionsToPackage{booktabs}{sphinx}' in result assert r'\PassOptionsToPackage{colorrows}{sphinx}' in result # tabularcolumns @@ -1389,15 +1389,16 @@ def test_latex_table_with_booktabs_and_colorrows(app, status, warning): confoverrides={'templates_path': ['_mytemplates/latex']}) def test_latex_table_custom_template_caseA(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert 'SALUT LES COPAINS' in result + assert 'AU REVOIR, KANIGGETS' in result @pytest.mark.sphinx('latex', testroot='latex-table', confoverrides={'templates_path': ['_mytemplates']}) def test_latex_table_custom_template_caseB(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert 'SALUT LES COPAINS' not in result @@ -1405,14 +1406,14 @@ def test_latex_table_custom_template_caseB(app, status, warning): @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_custom_template_caseC(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert 'SALUT LES COPAINS' not in result @pytest.mark.sphinx('latex', testroot='directives-raw') def test_latex_raw_directive(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') # standard case assert 'standalone raw directive (HTML)' not in result @@ -1429,7 +1430,7 @@ def test_latex_images(app, status, warning): with http_server(RemoteImageHandler, port=7777): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') # images are copied assert '\\sphinxincludegraphics{{sphinx}.png}' in result @@ -1453,7 +1454,7 @@ def test_latex_images(app, status, warning): def test_latex_index(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert ('A \\index{famous@\\spxentry{famous}}famous ' '\\index{equation@\\spxentry{equation}}equation:\n' in result) assert ('\n\\index{Einstein@\\spxentry{Einstein}}' @@ -1467,7 +1468,7 @@ def test_latex_index(app, status, warning): def test_latex_equations(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') expected = (app.srcdir / 'expects' / 'latex-equations.tex').read_text(encoding='utf8').strip() assert expected in result @@ -1477,7 +1478,7 @@ def test_latex_equations(app, status, warning): def test_latex_image_in_parsed_literal(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert ('{\\sphinxunactivateextrasandspace \\raisebox{-0.5\\height}' '{\\sphinxincludegraphics[height=2.00000cm]{{pic}.png}}' '}AFTER') in result @@ -1487,7 +1488,7 @@ def test_latex_image_in_parsed_literal(app, status, warning): def test_latex_nested_enumerated_list(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert ('\\sphinxsetlistlabels{\\arabic}{enumi}{enumii}{}{.}%\n' '\\setcounter{enumi}{4}\n' in result) assert ('\\sphinxsetlistlabels{\\alph}{enumii}{enumiii}{}{.}%\n' @@ -1504,7 +1505,7 @@ def test_latex_nested_enumerated_list(app, status, warning): def test_latex_thebibliography(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) assert ('\\begin{sphinxthebibliography}{AuthorYe}\n' '\\bibitem[AuthorYear]{index:authoryear}\n\\sphinxAtStartPar\n' @@ -1517,7 +1518,7 @@ def test_latex_thebibliography(app, status, warning): def test_latex_glossary(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert (r'\sphinxlineitem{ähnlich\index{ähnlich@\spxentry{ähnlich}|spxpagem}' r'\phantomsection' r'\label{\detokenize{index:term-ahnlich}}}' in result) @@ -1541,7 +1542,7 @@ def test_latex_glossary(app, status, warning): def test_latex_labels(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') # figures assert (r'\caption{labeled figure}' @@ -1589,8 +1590,10 @@ def test_latex_labels(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-figure-in-admonition') def test_latex_figure_in_admonition(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') - assert r'\begin{figure}[H]' in result + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') + assert 'tabulary' not in result + for type in ('caution', 'note', 'seealso', 'todo'): + assert f'{type} directive.\n\n\\begin{{figure}}[H]' in result def test_default_latex_documents(): @@ -1619,7 +1622,7 @@ def test_includegraphics_oversized(app, status, warning): @pytest.mark.sphinx('latex', testroot='index_on_title') def test_index_on_title(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert ('\\chapter{Test for index in top level title}\n' '\\label{\\detokenize{contents:test-for-index-in-top-level-title}}' '\\index{index@\\spxentry{index}}\n' @@ -1630,7 +1633,7 @@ def test_index_on_title(app, status, warning): confoverrides={'latex_engine': 'pdflatex'}) def test_texescape_for_non_unicode_supported_engine(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) assert 'script small e: e' in result assert 'double struck italic small i: i' in result @@ -1642,7 +1645,7 @@ def test_texescape_for_non_unicode_supported_engine(app, status, warning): confoverrides={'latex_engine': 'xelatex'}) def test_texescape_for_unicode_supported_engine(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(result) assert 'script small e: e' in result assert 'double struck italic small i: i' in result @@ -1667,7 +1670,7 @@ def test_latex_nested_tables(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-container') def test_latex_container(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert r'\begin{sphinxuseclass}{classname}' in result assert r'\end{sphinxuseclass}' in result @@ -1675,7 +1678,7 @@ def test_latex_container(app, status, warning): @pytest.mark.sphinx('latex', testroot='reST-code-role') def test_latex_code_role(app): app.build() - content = (app.outdir / 'python.tex').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') common_content = ( r'\PYG{k}{def} ' @@ -1710,6 +1713,7 @@ def test_copy_images(app, status, warning): } images.discard('sphinx.png') assert images == { + 'ba30773957c3fe046897111afd65a80b81cad089.png', # latex: image from data:image/png URI in source 'img.pdf', 'rimg.png', 'testimäge.png', @@ -1719,7 +1723,7 @@ def test_copy_images(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-labels-before-module') def test_duplicated_labels_before_module(app, status, warning): app.build() - content: str = (app.outdir / 'python.tex').read_text(encoding='utf8') + content: str = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') def count_label(name): text = r'\phantomsection\label{\detokenize{%s}}' % name @@ -1750,10 +1754,40 @@ def test_duplicated_labels_before_module(app, status, warning): confoverrides={'python_maximum_signature_line_length': 23}) def test_one_parameter_per_line(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') # TODO: should these asserts check presence or absence of a final \sphinxparamcomma? # signature of 23 characters is too short to trigger one-param-per-line mark-up assert ('\\pysiglinewithargsret{\\sphinxbfcode{\\sphinxupquote{hello}}}' in result) assert ('\\pysigwithonelineperarg{\\sphinxbfcode{\\sphinxupquote{foo}}}' in result) + + # generic_arg[T] + assert ('\\pysiglinewithargsretwithtypelist{\\sphinxbfcode{\\sphinxupquote{generic\\_arg}}}' + '{\\sphinxtypeparam{\\DUrole{n}{T}}}{}{}' in result) + + # generic_foo[T]() + assert ('\\pysiglinewithargsretwithtypelist{\\sphinxbfcode{\\sphinxupquote{generic\\_foo}}}' in result) + + # generic_bar[T](x: list[T]) + assert ('\\pysigwithonelineperargwithtypelist{\\sphinxbfcode{\\sphinxupquote{generic\\_bar}}}' in result) + + # generic_ret[R]() -> R + assert ('\\pysiglinewithargsretwithtypelist{\\sphinxbfcode{\\sphinxupquote{generic\\_ret}}}' + '{\\sphinxtypeparam{\\DUrole{n}{R}}}{}{{ $\\rightarrow$ R}}' in result) + + # MyGenericClass[X] + assert ('\\pysiglinewithargsretwithtypelist{\\sphinxbfcode{\\sphinxupquote{class\\DUrole{w}{ ' + '}}}\\sphinxbfcode{\\sphinxupquote{MyGenericClass}}}' in result) + + # MyList[T](list[T]) + assert ('\\pysiglinewithargsretwithtypelist{\\sphinxbfcode{\\sphinxupquote{class\\DUrole{w}{ ' + '}}}\\sphinxbfcode{\\sphinxupquote{MyList}}}' in result) + + +@pytest.mark.sphinx('latex', testroot='markup-rubric') +def test_latex_rubric(app): + app.build() + content = (app.outdir / 'test.tex').read_text(encoding='utf8') + assert r'\subsubsection*{This is a rubric}' in content + assert r'\subsection*{A rubric with a heading level 2}' in content diff --git a/tests/test_builders/test_build_linkcheck.py b/tests/test_builders/test_build_linkcheck.py index c8d8515..0787661 100644 --- a/tests/test_builders/test_build_linkcheck.py +++ b/tests/test_builders/test_build_linkcheck.py @@ -11,6 +11,7 @@ import wsgiref.handlers from base64 import b64encode from http.server import BaseHTTPRequestHandler from queue import Queue +from typing import TYPE_CHECKING from unittest import mock import docutils @@ -20,6 +21,7 @@ from urllib3.poolmanager import PoolManager import sphinx.util.http_date from sphinx.builders.linkcheck import ( CheckRequest, + CheckResult, Hyperlink, HyperlinkAvailabilityCheckWorker, RateLimit, @@ -33,6 +35,12 @@ from tests.utils import CERT_FILE, serve_application ts_re = re.compile(r".*\[(?P<ts>.*)\].*") +if TYPE_CHECKING: + from collections.abc import Callable, Iterable + from io import StringIO + + from sphinx.application import Sphinx + class DefaultsHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" @@ -101,7 +109,7 @@ class ConnectionMeasurement: @pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True) -def test_defaults(app): +def test_defaults(app: Sphinx) -> None: with serve_application(app, DefaultsHandler) as address: with ConnectionMeasurement() as m: app.build() @@ -146,7 +154,7 @@ def test_defaults(app): 'info': '', } - def _missing_resource(filename: str, lineno: int): + def _missing_resource(filename: str, lineno: int) -> dict[str, str | int]: return { 'filename': 'links.rst', 'lineno': lineno, @@ -178,7 +186,7 @@ def test_defaults(app): @pytest.mark.sphinx( 'linkcheck', testroot='linkcheck', freshenv=True, confoverrides={'linkcheck_anchors': False}) -def test_check_link_response_only(app): +def test_check_link_response_only(app: Sphinx) -> None: with serve_application(app, DefaultsHandler) as address: app.build() @@ -192,7 +200,7 @@ def test_check_link_response_only(app): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-too-many-retries', freshenv=True) -def test_too_many_retries(app): +def test_too_many_retries(app: Sphinx) -> None: with serve_application(app, DefaultsHandler) as address: app.build() @@ -221,7 +229,7 @@ def test_too_many_retries(app): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-raw-node', freshenv=True) -def test_raw_node(app): +def test_raw_node(app: Sphinx) -> None: with serve_application(app, OKHandler) as address: # write an index file that contains a link back to this webserver's root # URL. docutils will replace the raw node with the contents retrieved.. @@ -254,7 +262,7 @@ def test_raw_node(app): @pytest.mark.sphinx( 'linkcheck', testroot='linkcheck-anchors-ignore', freshenv=True, confoverrides={'linkcheck_anchors_ignore': ["^!", "^top$"]}) -def test_anchors_ignored(app): +def test_anchors_ignored(app: Sphinx) -> None: with serve_application(app, OKHandler): app.build() @@ -266,6 +274,43 @@ def test_anchors_ignored(app): class AnchorsIgnoreForUrlHandler(BaseHTTPRequestHandler): + protocol_version = 'HTTP/1.1' + + def _chunk_content(self, content: str, *, max_chunk_size: int) -> Iterable[bytes]: + + def _encode_chunk(chunk: bytes) -> Iterable[bytes]: + """Encode a bytestring into a format suitable for HTTP chunked-transfer. + + https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding + """ + yield f'{len(chunk):X}'.encode('ascii') + yield b'\r\n' + yield chunk + yield b'\r\n' + + buffer = b'' + for char in content: + buffer += char.encode('utf-8') + if len(buffer) >= max_chunk_size: + chunk, buffer = buffer[:max_chunk_size], buffer[max_chunk_size:] + yield from _encode_chunk(chunk) + + # Flush remaining bytes, if any + if buffer: + yield from _encode_chunk(buffer) + + # Emit a final empty chunk to close the stream + yield from _encode_chunk(b'') + + def _send_chunked(self, content: str) -> bool: + for chunk in self._chunk_content(content, max_chunk_size=20): + try: + self.wfile.write(chunk) + except (BrokenPipeError, ConnectionResetError) as e: + self.log_message(str(e)) + return False + return True + def do_HEAD(self): if self.path in {'/valid', '/ignored'}: self.send_response(200, "OK") @@ -274,17 +319,24 @@ class AnchorsIgnoreForUrlHandler(BaseHTTPRequestHandler): self.end_headers() def do_GET(self): - self.do_HEAD() if self.path == '/valid': - self.wfile.write(b"<h1 id='valid-anchor'>valid anchor</h1>\n") + self.send_response(200, 'OK') + content = "<h1 id='valid-anchor'>valid anchor</h1>\n" elif self.path == '/ignored': - self.wfile.write(b"no anchor but page exists\n") + self.send_response(200, 'OK') + content = 'no anchor but page exists\n' + else: + self.send_response(404, 'Not Found') + content = 'not found\n' + self.send_header('Transfer-Encoding', 'chunked') + self.end_headers() + self._send_chunked(content) @pytest.mark.sphinx('linkcheck', testroot='linkcheck-anchors-ignore-for-url', freshenv=True) -def test_anchors_ignored_for_url(app): +def test_anchors_ignored_for_url(app: Sphinx) -> None: with serve_application(app, AnchorsIgnoreForUrlHandler) as address: - app.config.linkcheck_anchors_ignore_for_url = [ # type: ignore[attr-defined] + app.config.linkcheck_anchors_ignore_for_url = [ f'http://{address}/ignored', # existing page f'http://{address}/invalid', # unknown page ] @@ -295,7 +347,7 @@ def test_anchors_ignored_for_url(app): attrs = ('filename', 'lineno', 'status', 'code', 'uri', 'info') data = [json.loads(x) for x in content.splitlines()] - assert len(data) == 7 + assert len(data) == 8 assert all(all(attr in row for attr in attrs) for row in data) # rows may be unsorted due to network latency or @@ -304,6 +356,7 @@ def test_anchors_ignored_for_url(app): assert rows[f'http://{address}/valid']['status'] == 'working' assert rows[f'http://{address}/valid#valid-anchor']['status'] == 'working' + assert rows[f'http://{address}/valid#py:module::urllib.parse']['status'] == 'broken' assert rows[f'http://{address}/valid#invalid-anchor'] == { 'status': 'broken', 'info': "Anchor 'invalid-anchor' not found", @@ -323,7 +376,7 @@ def test_anchors_ignored_for_url(app): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-anchor', freshenv=True) -def test_raises_for_invalid_status(app): +def test_raises_for_invalid_status(app: Sphinx) -> None: class InternalServerErrorHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" @@ -340,6 +393,50 @@ def test_raises_for_invalid_status(app): ) +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-anchor', freshenv=True) +def test_incomplete_html_anchor(app): + class IncompleteHTMLDocumentHandler(BaseHTTPRequestHandler): + protocol_version = 'HTTP/1.1' + + def do_GET(self): + content = b'this is <div id="anchor">not</div> a valid HTML document' + self.send_response(200, 'OK') + self.send_header('Content-Length', str(len(content))) + self.end_headers() + self.wfile.write(content) + + with serve_application(app, IncompleteHTMLDocumentHandler): + app.build() + + content = (app.outdir / 'output.json').read_text(encoding='utf8') + assert len(content.splitlines()) == 1 + + row = json.loads(content) + assert row['status'] == 'working' + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-anchor', freshenv=True) +def test_decoding_error_anchor_ignored(app): + class NonASCIIHandler(BaseHTTPRequestHandler): + protocol_version = 'HTTP/1.1' + + def do_GET(self): + content = b'\x80\x00\x80\x00' # non-ASCII byte-string + self.send_response(200, 'OK') + self.send_header('Content-Length', str(len(content))) + self.end_headers() + self.wfile.write(content) + + with serve_application(app, NonASCIIHandler): + app.build() + + content = (app.outdir / 'output.json').read_text(encoding='utf8') + assert len(content.splitlines()) == 1 + + row = json.loads(content) + assert row['status'] == 'ignored' + + def custom_handler(valid_credentials=(), success_criteria=lambda _: True): """ Returns an HTTP request handler that authenticates the client and then determines @@ -352,25 +449,27 @@ def custom_handler(valid_credentials=(), success_criteria=lambda _: True): expected_token = b64encode(":".join(valid_credentials).encode()).decode("utf-8") del valid_credentials + def authenticated( + method: Callable[[CustomHandler], None] + ) -> Callable[[CustomHandler], None]: + def method_if_authenticated(self): + if expected_token is None: + return method(self) + elif not self.headers["Authorization"]: + self.send_response(401, "Unauthorized") + self.end_headers() + elif self.headers["Authorization"] == f"Basic {expected_token}": + return method(self) + else: + self.send_response(403, "Forbidden") + self.send_header("Content-Length", "0") + self.end_headers() + + return method_if_authenticated + class CustomHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" - def authenticated(method): - def method_if_authenticated(self): - if expected_token is None: - return method(self) - elif not self.headers["Authorization"]: - self.send_response(401, "Unauthorized") - self.end_headers() - elif self.headers["Authorization"] == f"Basic {expected_token}": - return method(self) - else: - self.send_response(403, "Forbidden") - self.send_header("Content-Length", "0") - self.end_headers() - - return method_if_authenticated - @authenticated def do_HEAD(self): self.do_GET() @@ -389,9 +488,9 @@ def custom_handler(valid_credentials=(), success_criteria=lambda _: True): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) -def test_auth_header_uses_first_match(app): +def test_auth_header_uses_first_match(app: Sphinx) -> None: with serve_application(app, custom_handler(valid_credentials=("user1", "password"))) as address: - app.config.linkcheck_auth = [ # type: ignore[attr-defined] + app.config.linkcheck_auth = [ (r'^$', ('no', 'match')), (fr'^http://{re.escape(address)}/$', ('user1', 'password')), (r'.*local.*', ('user2', 'hunter2')), @@ -408,7 +507,7 @@ def test_auth_header_uses_first_match(app): @pytest.mark.sphinx( 'linkcheck', testroot='linkcheck-localserver', freshenv=True, confoverrides={'linkcheck_allow_unauthorized': False}) -def test_unauthorized_broken(app): +def test_unauthorized_broken(app: Sphinx) -> None: with serve_application(app, custom_handler(valid_credentials=("user1", "password"))): app.build() @@ -422,7 +521,7 @@ def test_unauthorized_broken(app): @pytest.mark.sphinx( 'linkcheck', testroot='linkcheck-localserver', freshenv=True, confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]}) -def test_auth_header_no_match(app): +def test_auth_header_no_match(app: Sphinx) -> None: with ( serve_application(app, custom_handler(valid_credentials=("user1", "password"))), pytest.warns(RemovedInSphinx80Warning, match='linkcheck builder encountered an HTTP 401'), @@ -438,14 +537,14 @@ def test_auth_header_no_match(app): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) -def test_linkcheck_request_headers(app): +def test_linkcheck_request_headers(app: Sphinx) -> None: def check_headers(self): if "X-Secret" in self.headers: return False return self.headers["Accept"] == "text/html" with serve_application(app, custom_handler(success_criteria=check_headers)) as address: - app.config.linkcheck_request_headers = { # type: ignore[attr-defined] + app.config.linkcheck_request_headers = { f"http://{address}/": {"Accept": "text/html"}, "*": {"X-Secret": "open sesami"}, } @@ -458,14 +557,14 @@ def test_linkcheck_request_headers(app): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) -def test_linkcheck_request_headers_no_slash(app): +def test_linkcheck_request_headers_no_slash(app: Sphinx) -> None: def check_headers(self): if "X-Secret" in self.headers: return False return self.headers["Accept"] == "application/json" with serve_application(app, custom_handler(success_criteria=check_headers)) as address: - app.config.linkcheck_request_headers = { # type: ignore[attr-defined] + app.config.linkcheck_request_headers = { f"http://{address}": {"Accept": "application/json"}, "*": {"X-Secret": "open sesami"}, } @@ -483,7 +582,7 @@ def test_linkcheck_request_headers_no_slash(app): "http://do.not.match.org": {"Accept": "application/json"}, "*": {"X-Secret": "open sesami"}, }}) -def test_linkcheck_request_headers_default(app): +def test_linkcheck_request_headers_default(app: Sphinx) -> None: def check_headers(self): if self.headers["X-Secret"] != "open sesami": return False @@ -566,9 +665,9 @@ def test_follows_redirects_on_GET(app, capsys, warning): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-warn-redirects') -def test_linkcheck_allowed_redirects(app, warning): +def test_linkcheck_allowed_redirects(app: Sphinx, warning: StringIO) -> None: with serve_application(app, make_redirect_handler(support_head=False)) as address: - app.config.linkcheck_allowed_redirects = {f'http://{address}/.*1': '.*'} # type: ignore[attr-defined] + app.config.linkcheck_allowed_redirects = {f'http://{address}/.*1': '.*'} compile_linkcheck_allowed_redirects(app, app.config) app.build() @@ -626,7 +725,7 @@ def test_invalid_ssl(get_request, app): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) -def test_connect_to_selfsigned_fails(app): +def test_connect_to_selfsigned_fails(app: Sphinx) -> None: with serve_application(app, OKHandler, tls_enabled=True) as address: app.build() @@ -639,9 +738,9 @@ def test_connect_to_selfsigned_fails(app): assert "[SSL: CERTIFICATE_VERIFY_FAILED]" in content["info"] -@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) -def test_connect_to_selfsigned_with_tls_verify_false(app): - app.config.tls_verify = False +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True, + confoverrides={'tls_verify': False}) +def test_connect_to_selfsigned_with_tls_verify_false(app: Sphinx) -> None: with serve_application(app, OKHandler, tls_enabled=True) as address: app.build() @@ -657,9 +756,9 @@ def test_connect_to_selfsigned_with_tls_verify_false(app): } -@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) -def test_connect_to_selfsigned_with_tls_cacerts(app): - app.config.tls_cacerts = CERT_FILE +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True, + confoverrides={'tls_cacerts': CERT_FILE}) +def test_connect_to_selfsigned_with_tls_cacerts(app: Sphinx) -> None: with serve_application(app, OKHandler, tls_enabled=True) as address: app.build() @@ -693,9 +792,9 @@ def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app): } -@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) -def test_connect_to_selfsigned_nonexistent_cert_file(app): - app.config.tls_cacerts = "does/not/exist" +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True, + confoverrides={'tls_cacerts': "does/not/exist"}) +def test_connect_to_selfsigned_nonexistent_cert_file(app: Sphinx) -> None: with serve_application(app, OKHandler, tls_enabled=True) as address: app.build() @@ -863,7 +962,7 @@ def test_too_many_requests_retry_after_without_header(app, capsys): 'linkcheck_timeout': 0.01, } ) -def test_requests_timeout(app): +def test_requests_timeout(app: Sphinx) -> None: class DelayedResponseHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" @@ -882,9 +981,9 @@ def test_requests_timeout(app): assert content["status"] == "timeout" -@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) -def test_too_many_requests_user_timeout(app): - app.config.linkcheck_rate_limit_timeout = 0.0 +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True, + confoverrides={'linkcheck_rate_limit_timeout': 0.0}) +def test_too_many_requests_user_timeout(app: Sphinx) -> None: with serve_application(app, make_retry_after_handler([(429, None)])) as address: app.build() content = (app.outdir / 'output.json').read_text(encoding='utf8') @@ -903,21 +1002,21 @@ class FakeResponse: url = "http://localhost/" -def test_limit_rate_default_sleep(app): +def test_limit_rate_default_sleep(app: Sphinx) -> None: worker = HyperlinkAvailabilityCheckWorker(app.config, Queue(), Queue(), {}) with mock.patch('time.time', return_value=0.0): next_check = worker.limit_rate(FakeResponse.url, FakeResponse.headers.get("Retry-After")) assert next_check == 60.0 -def test_limit_rate_user_max_delay(app): - app.config.linkcheck_rate_limit_timeout = 0.0 +@pytest.mark.sphinx(confoverrides={'linkcheck_rate_limit_timeout': 0.0}) +def test_limit_rate_user_max_delay(app: Sphinx) -> None: worker = HyperlinkAvailabilityCheckWorker(app.config, Queue(), Queue(), {}) next_check = worker.limit_rate(FakeResponse.url, FakeResponse.headers.get("Retry-After")) assert next_check is None -def test_limit_rate_doubles_previous_wait_time(app): +def test_limit_rate_doubles_previous_wait_time(app: Sphinx) -> None: rate_limits = {"localhost": RateLimit(60.0, 0.0)} worker = HyperlinkAvailabilityCheckWorker(app.config, Queue(), Queue(), rate_limits) with mock.patch('time.time', return_value=0.0): @@ -925,21 +1024,23 @@ def test_limit_rate_doubles_previous_wait_time(app): assert next_check == 120.0 -def test_limit_rate_clips_wait_time_to_max_time(app): - app.config.linkcheck_rate_limit_timeout = 90.0 +@pytest.mark.sphinx(confoverrides={'linkcheck_rate_limit_timeout': 90}) +def test_limit_rate_clips_wait_time_to_max_time(app: Sphinx, warning: StringIO) -> None: rate_limits = {"localhost": RateLimit(60.0, 0.0)} worker = HyperlinkAvailabilityCheckWorker(app.config, Queue(), Queue(), rate_limits) with mock.patch('time.time', return_value=0.0): next_check = worker.limit_rate(FakeResponse.url, FakeResponse.headers.get("Retry-After")) assert next_check == 90.0 + assert warning.getvalue() == '' -def test_limit_rate_bails_out_after_waiting_max_time(app): - app.config.linkcheck_rate_limit_timeout = 90.0 +@pytest.mark.sphinx(confoverrides={'linkcheck_rate_limit_timeout': 90.0}) +def test_limit_rate_bails_out_after_waiting_max_time(app: Sphinx, warning: StringIO) -> None: rate_limits = {"localhost": RateLimit(90.0, 0.0)} worker = HyperlinkAvailabilityCheckWorker(app.config, Queue(), Queue(), rate_limits) next_check = worker.limit_rate(FakeResponse.url, FakeResponse.headers.get("Retry-After")) assert next_check is None + assert warning.getvalue() == '' @mock.patch('sphinx.util.requests.requests.Session.get_adapter') @@ -957,11 +1058,13 @@ def test_connection_contention(get_adapter, app, capsys): # Place a workload into the linkcheck queue link_count = 10 - rqueue, wqueue = Queue(), Queue() + wqueue: Queue[CheckRequest] = Queue() + rqueue: Queue[CheckResult] = Queue() for _ in range(link_count): wqueue.put(CheckRequest(0, Hyperlink(f"http://{address}", "test", "test.rst", 1))) - begin, checked = time.time(), [] + begin = time.time() + checked: list[CheckResult] = [] threads = [ HyperlinkAvailabilityCheckWorker( config=app.config, @@ -997,7 +1100,7 @@ class ConnectionResetHandler(BaseHTTPRequestHandler): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) -def test_get_after_head_raises_connection_error(app): +def test_get_after_head_raises_connection_error(app: Sphinx) -> None: with serve_application(app, ConnectionResetHandler) as address: app.build() content = (app.outdir / 'output.txt').read_text(encoding='utf8') @@ -1014,7 +1117,7 @@ def test_get_after_head_raises_connection_error(app): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-documents_exclude', freshenv=True) -def test_linkcheck_exclude_documents(app): +def test_linkcheck_exclude_documents(app: Sphinx) -> None: with serve_application(app, DefaultsHandler): app.build() diff --git a/tests/test_builders/test_build_manpage.py b/tests/test_builders/test_build_manpage.py index 7172281..31d75d6 100644 --- a/tests/test_builders/test_build_manpage.py +++ b/tests/test_builders/test_build_manpage.py @@ -7,6 +7,8 @@ from sphinx.builders.manpage import default_man_pages from sphinx.config import Config +@pytest.mark.xfail(docutils.__version_info__[:2] > (0, 21), + reason='Docutils has removed the reference key in master') @pytest.mark.sphinx('man') def test_all(app, status, warning): app.build(force_all=True) @@ -44,13 +46,15 @@ def test_man_pages_empty_description(app, status, warning): confoverrides={'man_make_section_directory': True}) def test_man_make_section_directory(app, status, warning): app.build() - assert (app.outdir / 'man1' / 'python.1').exists() + assert (app.outdir / 'man1' / 'projectnamenotset.1').exists() +@pytest.mark.xfail(docutils.__version_info__[:2] > (0, 21), + reason='Docutils has removed the reference key in master') @pytest.mark.sphinx('man', testroot='directive-code') def test_captioned_code_block(app, status, warning): app.build(force_all=True) - content = (app.outdir / 'python.1').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.1').read_text(encoding='utf8') if docutils.__version_info__[:2] < (0, 21): expected = """\ @@ -100,5 +104,5 @@ def test_default_man_pages(): @pytest.mark.sphinx('man', testroot='markup-rubric') def test_rubric(app, status, warning): app.build() - content = (app.outdir / 'python.1').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.1').read_text(encoding='utf8') assert 'This is a rubric\n' in content diff --git a/tests/test_builders/test_build_texinfo.py b/tests/test_builders/test_build_texinfo.py index f9effb2..6abbc96 100644 --- a/tests/test_builders/test_build_texinfo.py +++ b/tests/test_builders/test_build_texinfo.py @@ -40,16 +40,17 @@ def test_texinfo(app, status, warning): def test_texinfo_rubric(app, status, warning): app.build() - output = (app.outdir / 'python.texi').read_text(encoding='utf8') + output = (app.outdir / 'projectnamenotset.texi').read_text(encoding='utf8') assert '@heading This is a rubric' in output assert '@heading This is a multiline rubric' in output + assert '@heading A rubric with a heading level' in output @pytest.mark.sphinx('texinfo', testroot='markup-citation') def test_texinfo_citation(app, status, warning): app.build(force_all=True) - output = (app.outdir / 'python.texi').read_text(encoding='utf8') + output = (app.outdir / 'projectnamenotset.texi').read_text(encoding='utf8') assert 'This is a citation ref; @ref{1,,[CITE1]} and @ref{2,,[CITE2]}.' in output assert ('@anchor{index cite1}@anchor{1}@w{(CITE1)} \n' 'This is a citation\n') in output @@ -87,7 +88,7 @@ def test_texinfo_escape_id(app, status, warning): def test_texinfo_footnote(app, status, warning): app.build(force_all=True) - output = (app.outdir / 'python.texi').read_text(encoding='utf8') + output = (app.outdir / 'projectnamenotset.texi').read_text(encoding='utf8') assert 'First footnote: @footnote{\nFirst\n}' in output @@ -120,10 +121,11 @@ def test_texinfo_samp_with_variable(app, status, warning): def test_copy_images(app, status, warning): app.build() - images_dir = Path(app.outdir) / 'python-figures' + images_dir = Path(app.outdir) / 'projectnamenotset-figures' images = {image.name for image in images_dir.rglob('*')} images.discard('python-logo.png') assert images == { + 'ba30773957c3fe046897111afd65a80b81cad089.png', # texinfo: image from data:image/png URI in source 'img.png', 'rimg.png', 'testimäge.png', diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index e1cb1b0..e58044e 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -403,7 +403,7 @@ def test_errors_if_setup_is_not_callable(tmp_path, make_app): assert 'callable' in str(excinfo.value) -@pytest.fixture() +@pytest.fixture def make_app_with_empty_project(make_app, tmp_path): (tmp_path / 'conf.py').write_text('', encoding='utf8') @@ -803,3 +803,19 @@ def test_gettext_compact_command_line_str(): # regression test for #8549 (-D gettext_compact=spam) assert config.gettext_compact == 'spam' + + +def test_root_doc_and_master_doc_are_synchronized(): + c = Config() + assert c.master_doc == 'index' + assert c.root_doc == c.master_doc + + c = Config() + c.master_doc = '1234' + assert c.master_doc == '1234' + assert c.root_doc == c.master_doc + + c = Config() + c.root_doc = '1234' + assert c.master_doc == '1234' + assert c.root_doc == c.master_doc diff --git a/tests/test_directives/test_directive_code.py b/tests/test_directives/test_directive_code.py index 2783d8f..6180195 100644 --- a/tests/test_directives/test_directive_code.py +++ b/tests/test_directives/test_directive_code.py @@ -104,7 +104,7 @@ def test_LiteralIncludeReader_lines_and_lineno_match1(literal_inc_path): assert reader.lineno_start == 3 -@pytest.mark.sphinx() # init locale for errors +@pytest.mark.sphinx # init locale for errors def test_LiteralIncludeReader_lines_and_lineno_match2(literal_inc_path, app, status, warning): options = {'lines': '0,3,5', 'lineno-match': True} reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) @@ -112,7 +112,7 @@ def test_LiteralIncludeReader_lines_and_lineno_match2(literal_inc_path, app, sta reader.read() -@pytest.mark.sphinx() # init locale for errors +@pytest.mark.sphinx # init locale for errors def test_LiteralIncludeReader_lines_and_lineno_match3(literal_inc_path, app, status, warning): options = {'lines': '100-', 'lineno-match': True} reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) @@ -330,7 +330,7 @@ def test_code_block_caption_html(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_code_block_caption_latex(app, status, warning): app.build(force_all=True) - latex = (app.outdir / 'python.tex').read_text(encoding='utf8') + latex = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') caption = '\\sphinxSetupCaptionForVerbatim{caption \\sphinxstyleemphasis{test} rb}' label = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:id1}}}' link = '\\hyperref[\\detokenize{caption:name-test-rb}]' \ @@ -343,7 +343,7 @@ def test_code_block_caption_latex(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_code_block_namedlink_latex(app, status, warning): app.build(force_all=True) - latex = (app.outdir / 'python.tex').read_text(encoding='utf8') + latex = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') label1 = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:name-test-rb}}}' link1 = '\\hyperref[\\detokenize{caption:name-test-rb}]'\ '{\\sphinxcrossref{\\DUrole{std,std-ref}{Ruby}}' @@ -360,7 +360,7 @@ def test_code_block_namedlink_latex(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_code_block_emphasize_latex(app, status, warning): app.build(filenames=[app.srcdir / 'emphasize.rst']) - latex = (app.outdir / 'python.tex').read_text(encoding='utf8').replace('\r\n', '\n') + latex = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8').replace('\r\n', '\n') includes = '\\fvset{hllines={, 5, 6, 13, 14, 15, 24, 25, 26,}}%\n' assert includes in latex includes = '\\end{sphinxVerbatim}\n\\sphinxresetverbatimhllines\n' @@ -424,7 +424,7 @@ def test_literal_include_linenos(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_literalinclude_file_whole_of_emptyline(app, status, warning): app.build(force_all=True) - latex = (app.outdir / 'python.tex').read_text(encoding='utf8').replace('\r\n', '\n') + latex = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8').replace('\r\n', '\n') includes = ( '\\begin{sphinxVerbatim}' '[commandchars=\\\\\\{\\},numbers=left,firstnumber=1,stepnumber=1]\n' @@ -450,7 +450,7 @@ def test_literalinclude_caption_html(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_literalinclude_caption_latex(app, status, warning): app.build(filenames='index') - latex = (app.outdir / 'python.tex').read_text(encoding='utf8') + latex = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') caption = '\\sphinxSetupCaptionForVerbatim{caption \\sphinxstylestrong{test} py}' label = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:id2}}}' link = '\\hyperref[\\detokenize{caption:name-test-py}]' \ @@ -463,7 +463,7 @@ def test_literalinclude_caption_latex(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_literalinclude_namedlink_latex(app, status, warning): app.build(filenames='index') - latex = (app.outdir / 'python.tex').read_text(encoding='utf8') + latex = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') label1 = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:name-test-py}}}' link1 = '\\hyperref[\\detokenize{caption:name-test-py}]'\ '{\\sphinxcrossref{\\DUrole{std,std-ref}{Python}}' diff --git a/tests/test_directives/test_directive_other.py b/tests/test_directives/test_directive_other.py index 1feb251..e00e291 100644 --- a/tests/test_directives/test_directive_other.py +++ b/tests/test_directives/test_directive_other.py @@ -137,6 +137,18 @@ def test_reversed_toctree(app): @pytest.mark.sphinx(testroot='toctree-glob') +def test_toctree_class(app): + text = ('.. toctree::\n' + ' :class: custom-toc\n' + '\n' + ' foo\n') + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert doctree[0].attributes['classes'] == ['toctree-wrapper', 'custom-toc'] + + +@pytest.mark.sphinx(testroot='toctree-glob') def test_toctree_twice(app): text = (".. toctree::\n" "\n" diff --git a/tests/test_domains/test_domain_cpp.py b/tests/test_domains/test_domain_cpp.py index abd0f82..d8e612e 100644 --- a/tests/test_domains/test_domain_cpp.py +++ b/tests/test_domains/test_domain_cpp.py @@ -1046,19 +1046,21 @@ def test_domain_cpp_ast_attributes(): check('enumerator', '{key}Foo [[attr1]] [[attr2]] = 42', {2: '3Foo'}) +def check_ast_xref_parsing(target): + class Config: + cpp_id_attributes = ["id_attr"] + cpp_paren_attributes = ["paren_attr"] + + parser = DefinitionParser(target, location='', config=Config()) + parser.parse_xref_object() + parser.assert_end() + + def test_domain_cpp_ast_xref_parsing(): - def check(target): - class Config: - cpp_id_attributes = ["id_attr"] - cpp_paren_attributes = ["paren_attr"] - parser = DefinitionParser(target, location=None, - config=Config()) - ast, isShorthand = parser.parse_xref_object() - parser.assert_end() - check('f') - check('f()') - check('void f()') - check('T f()') + check_ast_xref_parsing('f') + check_ast_xref_parsing('f()') + check_ast_xref_parsing('void f()') + check_ast_xref_parsing('T f()') @pytest.mark.parametrize( @@ -1213,18 +1215,12 @@ def test_domain_cpp_build_misuse_of_roles(app, status, warning): def test_domain_cpp_build_with_add_function_parentheses_is_True(app, status, warning): app.build(force_all=True) - def check(spec, text, file): - pattern = '<li><p>%s<a .*?><code .*?><span .*?>%s</span></code></a></p></li>' % spec - res = re.search(pattern, text) - if not res: - print(f"Pattern\n\t{pattern}\nnot found in {file}") - raise AssertionError rolePatterns = [ - ('', 'Sphinx'), - ('', 'Sphinx::version'), - ('', 'version'), - ('', 'List'), - ('', 'MyEnum'), + 'Sphinx', + 'Sphinx::version', + 'version', + 'List', + 'MyEnum', ] parenPatterns = [ ('ref function without parens ', r'paren_1\(\)'), @@ -1237,35 +1233,33 @@ def test_domain_cpp_build_with_add_function_parentheses_is_True(app, status, war ('ref op call with parens, explicit title ', 'paren_8_title'), ] - f = 'roles.html' - t = (app.outdir / f).read_text(encoding='utf8') - for s in rolePatterns: - check(s, t, f) - for s in parenPatterns: - check(s, t, f) + text = (app.outdir / 'roles.html').read_text(encoding='utf8') + for ref_text in rolePatterns: + pattern = f'<li><p><a .*?><code .*?><span .*?>{ref_text}</span></code></a></p></li>' + match = re.search(pattern, text) + assert match is not None, f"Pattern not found in roles.html:\n\t{pattern}" + for (desc_text, ref_text) in parenPatterns: + pattern = f'<li><p>{desc_text}<a .*?><code .*?><span .*?>{ref_text}</span></code></a></p></li>' + match = re.search(pattern, text) + assert match is not None, f"Pattern not found in roles.html:\n\t{pattern}" - f = 'any-role.html' - t = (app.outdir / f).read_text(encoding='utf8') - for s in parenPatterns: - check(s, t, f) + text = (app.outdir / 'any-role.html').read_text(encoding='utf8') + for (desc_text, ref_text) in parenPatterns: + pattern = f'<li><p>{desc_text}<a .*?><code .*?><span .*?>{ref_text}</span></code></a></p></li>' + match = re.search(pattern, text) + assert match is not None, f"Pattern not found in any-role.html:\n\t{pattern}" @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'add_function_parentheses': False}) def test_domain_cpp_build_with_add_function_parentheses_is_False(app, status, warning): app.build(force_all=True) - def check(spec, text, file): - pattern = '<li><p>%s<a .*?><code .*?><span .*?>%s</span></code></a></p></li>' % spec - res = re.search(pattern, text) - if not res: - print(f"Pattern\n\t{pattern}\nnot found in {file}") - raise AssertionError rolePatterns = [ - ('', 'Sphinx'), - ('', 'Sphinx::version'), - ('', 'version'), - ('', 'List'), - ('', 'MyEnum'), + 'Sphinx', + 'Sphinx::version', + 'version', + 'List', + 'MyEnum', ] parenPatterns = [ ('ref function without parens ', 'paren_1'), @@ -1278,17 +1272,21 @@ def test_domain_cpp_build_with_add_function_parentheses_is_False(app, status, wa ('ref op call with parens, explicit title ', 'paren_8_title'), ] - f = 'roles.html' - t = (app.outdir / f).read_text(encoding='utf8') - for s in rolePatterns: - check(s, t, f) - for s in parenPatterns: - check(s, t, f) - - f = 'any-role.html' - t = (app.outdir / f).read_text(encoding='utf8') - for s in parenPatterns: - check(s, t, f) + text = (app.outdir / 'roles.html').read_text(encoding='utf8') + for ref_text in rolePatterns: + pattern = f'<li><p><a .*?><code .*?><span .*?>{ref_text}</span></code></a></p></li>' + match = re.search(pattern, text) + assert match is not None, f"Pattern not found in roles.html:\n\t{pattern}" + for (desc_text, ref_text) in parenPatterns: + pattern = f'<li><p>{desc_text}<a .*?><code .*?><span .*?>{ref_text}</span></code></a></p></li>' + match = re.search(pattern, text) + assert match is not None, f"Pattern not found in roles.html:\n\t{pattern}" + + text = (app.outdir / 'any-role.html').read_text(encoding='utf8') + for (desc_text, ref_text) in parenPatterns: + pattern = f'<li><p>{desc_text}<a .*?><code .*?><span .*?>{ref_text}</span></code></a></p></li>' + match = re.search(pattern, text) + assert match is not None, f"Pattern not found in any-role.html:\n\t{pattern}" @pytest.mark.sphinx(testroot='domain-cpp') diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py index e653c80..ce3d444 100644 --- a/tests/test_domains/test_domain_py.py +++ b/tests/test_domains/test_domain_py.py @@ -92,19 +92,21 @@ def test_domain_py_xrefs(app, status, warning): refnodes = list(doctree.findall(pending_xref)) assert_refnode(refnodes[0], None, None, 'TopLevel', 'class') assert_refnode(refnodes[1], None, None, 'top_level', 'meth') - assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth') - assert_refnode(refnodes[3], None, 'NestedParentA', 'NestedChildA.subchild_2', 'meth') - assert_refnode(refnodes[4], None, 'NestedParentA', 'child_2', 'meth') - assert_refnode(refnodes[5], False, 'NestedParentA', 'any_child', domain='') - assert_refnode(refnodes[6], None, 'NestedParentA', 'NestedChildA', 'class') - assert_refnode(refnodes[7], None, 'NestedParentA.NestedChildA', 'subchild_2', 'meth') - assert_refnode(refnodes[8], None, 'NestedParentA.NestedChildA', + assert_refnode(refnodes[2], None, None, 'TopLevelType', 'type') + assert_refnode(refnodes[3], None, 'NestedParentA', 'child_1', 'meth') + assert_refnode(refnodes[4], None, 'NestedParentA', 'NestedChildA.subchild_2', 'meth') + assert_refnode(refnodes[5], None, 'NestedParentA', 'child_2', 'meth') + assert_refnode(refnodes[6], False, 'NestedParentA', 'any_child', domain='') + assert_refnode(refnodes[7], None, 'NestedParentA', 'NestedChildA', 'class') + assert_refnode(refnodes[8], None, 'NestedParentA.NestedChildA', 'subchild_2', 'meth') + assert_refnode(refnodes[9], None, 'NestedParentA.NestedChildA', 'NestedParentA.child_1', 'meth') - assert_refnode(refnodes[9], None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') - assert_refnode(refnodes[10], None, 'NestedParentB', 'child_1', 'meth') - assert_refnode(refnodes[11], None, 'NestedParentB', 'NestedParentB', 'class') - assert_refnode(refnodes[12], None, None, 'NestedParentA.NestedChildA', 'class') - assert len(refnodes) == 13 + assert_refnode(refnodes[10], None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') + assert_refnode(refnodes[11], None, 'NestedParentB', 'child_1', 'meth') + assert_refnode(refnodes[12], None, 'NestedParentB', 'NestedParentB', 'class') + assert_refnode(refnodes[13], None, None, 'NestedParentA.NestedChildA', 'class') + assert_refnode(refnodes[14], None, None, 'NestedParentA.NestedTypeA', 'type') + assert len(refnodes) == 15 doctree = app.env.get_doctree('module') refnodes = list(doctree.findall(pending_xref)) @@ -135,7 +137,10 @@ def test_domain_py_xrefs(app, status, warning): assert_refnode(refnodes[15], False, False, 'index', 'doc', domain='std') assert_refnode(refnodes[16], False, False, 'typing.Literal', 'obj', domain='py') assert_refnode(refnodes[17], False, False, 'typing.Literal', 'obj', domain='py') - assert len(refnodes) == 18 + assert_refnode(refnodes[18], False, False, 'list', 'class', domain='py') + assert_refnode(refnodes[19], False, False, 'int', 'class', domain='py') + assert_refnode(refnodes[20], False, False, 'str', 'class', domain='py') + assert len(refnodes) == 21 doctree = app.env.get_doctree('module_option') refnodes = list(doctree.findall(pending_xref)) @@ -191,7 +196,9 @@ def test_domain_py_objects(app, status, warning): assert objects['TopLevel'][2] == 'class' assert objects['top_level'][2] == 'method' + assert objects['TopLevelType'][2] == 'type' assert objects['NestedParentA'][2] == 'class' + assert objects['NestedParentA.NestedTypeA'][2] == 'type' assert objects['NestedParentA.child_1'][2] == 'method' assert objects['NestedParentA.any_child'][2] == 'method' assert objects['NestedParentA.NestedChildA'][2] == 'class' @@ -233,6 +240,9 @@ def test_domain_py_find_obj(app, status, warning): assert (find_obj(None, None, 'NONEXISTANT', 'class') == []) assert (find_obj(None, None, 'NestedParentA', 'class') == [('NestedParentA', ('roles', 'NestedParentA', 'class', False))]) + assert (find_obj(None, None, 'NestedParentA.NestedTypeA', 'type') == + [('NestedParentA.NestedTypeA', + ('roles', 'NestedParentA.NestedTypeA', 'type', False))]) assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') == [('NestedParentA.NestedChildA', ('roles', 'NestedParentA.NestedChildA', 'class', False))]) @@ -360,6 +370,27 @@ def test_parse_annotation(app): [desc_sig_punctuation, "]"])) assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Literal") + # Annotated type with callable gets parsed + doctree = _parse_annotation("Annotated[Optional[str], annotated_types.MaxLen(max_length=10)]", app.env) + assert_node(doctree, ( + [pending_xref, 'Annotated'], + [desc_sig_punctuation, '['], + [pending_xref, 'str'], + [desc_sig_space, ' '], + [desc_sig_punctuation, '|'], + [desc_sig_space, ' '], + [pending_xref, 'None'], + [desc_sig_punctuation, ','], + [desc_sig_space, ' '], + [pending_xref, 'annotated_types.MaxLen'], + [desc_sig_punctuation, '('], + [desc_sig_name, 'max_length'], + [desc_sig_operator, '='], + [desc_sig_literal_number, '10'], + [desc_sig_punctuation, ')'], + [desc_sig_punctuation, ']'], + )) + def test_parse_annotation_suppress(app): doctree = _parse_annotation("~typing.Dict[str, str]", app.env) @@ -743,7 +774,7 @@ def test_function_pep_695(app): S,\ T: int,\ U: (int, str),\ - R: int | int,\ + R: int | str,\ A: int | Annotated[int, ctype("char")],\ *V,\ **P\ @@ -785,14 +816,29 @@ def test_function_pep_695(app): desc_sig_space, [desc_sig_punctuation, '|'], desc_sig_space, - [pending_xref, 'int'], + [pending_xref, 'str'], )], )], [desc_type_parameter, ( [desc_sig_name, 'A'], [desc_sig_punctuation, ':'], desc_sig_space, - [desc_sig_name, ([pending_xref, 'int | Annotated[int, ctype("char")]'])], + [desc_sig_name, ( + [pending_xref, 'int'], + [desc_sig_space, ' '], + [desc_sig_punctuation, '|'], + [desc_sig_space, ' '], + [pending_xref, 'Annotated'], + [desc_sig_punctuation, '['], + [pending_xref, 'int'], + [desc_sig_punctuation, ','], + [desc_sig_space, ' '], + [pending_xref, 'ctype'], + [desc_sig_punctuation, '('], + [desc_sig_literal_string, "'char'"], + [desc_sig_punctuation, ')'], + [desc_sig_punctuation, ']'], + )], )], [desc_type_parameter, ( [desc_sig_operator, '*'], @@ -977,7 +1023,7 @@ def test_class_def_pep_696(app): ('[T:(*Ts)|int]', '[T: (*Ts) | int]'), ('[T:(int|(*Ts))]', '[T: (int | (*Ts))]'), ('[T:((*Ts)|int)]', '[T: ((*Ts) | int)]'), - ('[T:Annotated[int,ctype("char")]]', '[T: Annotated[int, ctype("char")]]'), + ("[T:Annotated[int,ctype('char')]]", "[T: Annotated[int, ctype('char')]]"), ]) def test_pep_695_and_pep_696_whitespaces_in_bound(app, tp_list, tptext): text = f'.. py:function:: f{tp_list}()' diff --git a/tests/test_domains/test_domain_py_pyobject.py b/tests/test_domains/test_domain_py_pyobject.py index 04f9341..adc0453 100644 --- a/tests/test_domains/test_domain_py_pyobject.py +++ b/tests/test_domains/test_domain_py_pyobject.py @@ -2,6 +2,7 @@ from __future__ import annotations +import pytest from docutils import nodes from sphinx import addnodes @@ -362,6 +363,76 @@ def test_pyproperty(app): assert domain.objects['Class.prop2'] == ('index', 'Class.prop2', 'property', False) +def test_py_type_alias(app): + text = (".. py:module:: example\n" + ".. py:type:: Alias1\n" + " :canonical: list[str | int]\n" + "\n" + ".. py:class:: Class\n" + "\n" + " .. py:type:: Alias2\n" + " :canonical: int\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + addnodes.index, + nodes.target, + [desc, ([desc_signature, ([desc_annotation, ('type', desc_sig_space)], + [desc_addname, 'example.'], + [desc_name, 'Alias1'], + [desc_annotation, (desc_sig_space, + [desc_sig_punctuation, '='], + desc_sig_space, + [pending_xref, 'list'], + [desc_sig_punctuation, '['], + [pending_xref, 'str'], + desc_sig_space, + [desc_sig_punctuation, '|'], + desc_sig_space, + [pending_xref, 'int'], + [desc_sig_punctuation, ']'], + )])], + [desc_content, ()])], + addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ('class', desc_sig_space)], + [desc_addname, 'example.'], + [desc_name, 'Class'])], + [desc_content, (addnodes.index, + desc)])])) + assert_node(doctree[5][1][0], addnodes.index, + entries=[('single', 'Alias2 (type alias in example.Class)', 'example.Class.Alias2', '', None)]) + assert_node(doctree[5][1][1], ([desc_signature, ([desc_annotation, ('type', desc_sig_space)], + [desc_name, 'Alias2'], + [desc_annotation, (desc_sig_space, + [desc_sig_punctuation, '='], + desc_sig_space, + [pending_xref, 'int'])])], + [desc_content, ()])) + assert 'example.Alias1' in domain.objects + assert domain.objects['example.Alias1'] == ('index', 'example.Alias1', 'type', False) + assert 'example.Class.Alias2' in domain.objects + assert domain.objects['example.Class.Alias2'] == ('index', 'example.Class.Alias2', 'type', False) + + +@pytest.mark.sphinx('html', testroot='domain-py', freshenv=True) +def test_domain_py_type_alias(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'type_alias.html').read_text(encoding='utf8') + assert ('<em class="property"><span class="pre">type</span><span class="w"> </span></em>' + '<span class="sig-prename descclassname"><span class="pre">module_one.</span></span>' + '<span class="sig-name descname"><span class="pre">MyAlias</span></span>' + '<em class="property"><span class="w"> </span><span class="p"><span class="pre">=</span></span>' + '<span class="w"> </span><span class="pre">list</span>' + '<span class="p"><span class="pre">[</span></span>' + '<span class="pre">int</span><span class="w"> </span>' + '<span class="p"><span class="pre">|</span></span><span class="w"> </span>' + '<a class="reference internal" href="#module_two.SomeClass" title="module_two.SomeClass">' + '<span class="pre">module_two.SomeClass</span></a>' + '<span class="p"><span class="pre">]</span></span></em>' in content) + assert warning.getvalue() == '' + + def test_pydecorator_signature(app): text = ".. py:decorator:: deco" domain = app.env.get_domain('py') diff --git a/tests/test_environment/test_environment.py b/tests/test_environment/test_environment.py index 8a34457..82de597 100644 --- a/tests/test_environment/test_environment.py +++ b/tests/test_environment/test_environment.py @@ -34,7 +34,7 @@ def test_config_status(make_app, app_params): assert app3.env.config_status == CONFIG_CHANGED app3.build() shutil.move(fname[:-4] + 'x.rst', fname) - assert "[config changed ('root_doc')] 1 added" in app3._status.getvalue() + assert "[config changed ('master_doc')] 1 added" in app3._status.getvalue() # incremental build (extension changed) app4 = make_app(*args, confoverrides={'extensions': ['sphinx.ext.autodoc']}, **kwargs) diff --git a/tests/test_environment/test_environment_indexentries.py b/tests/test_environment/test_environment_indexentries.py index 4cfdc28..19cb5a7 100644 --- a/tests/test_environment/test_environment_indexentries.py +++ b/tests/test_environment/test_environment_indexentries.py @@ -22,22 +22,22 @@ def test_create_single_index(app): restructuredtext.parse(app, text) index = IndexEntries(app.env).create_index(app.builder) assert len(index) == 6 - assert index[0] == ('Symbols', [('&-symbol', [[('', '#index-9')], [], None]), - ('9-symbol', [[('', '#index-8')], [], None]), - ('£100', [[('', '#index-10')], [], None])]) - assert index[1] == ('D', [('docutils', [[('', '#index-0')], [], None])]) - assert index[2] == ('P', [('pip', [[], [('install', [('', '#index-2')]), - ('upgrade', [('', '#index-3')])], None]), - ('Python', [[('', '#index-1')], [], None])]) - assert index[3] == ('S', [('Sphinx', [[('', '#index-4')], [], None])]) + assert index[0] == ('Symbols', [('&-symbol', ([('', '#index-9')], [], None)), + ('9-symbol', ([('', '#index-8')], [], None)), + ('£100', ([('', '#index-10')], [], None))]) + assert index[1] == ('D', [('docutils', ([('', '#index-0')], [], None))]) + assert index[2] == ('P', [('pip', ([], [('install', [('', '#index-2')]), + ('upgrade', [('', '#index-3')])], None)), + ('Python', ([('', '#index-1')], [], None))]) + assert index[3] == ('S', [('Sphinx', ([('', '#index-4')], [], None))]) assert index[4] == ('Е', - [('ёлка', [[('', '#index-6')], [], None]), - ('Ель', [[('', '#index-5')], [], None])]) + [('ёлка', ([('', '#index-6')], [], None)), + ('Ель', ([('', '#index-5')], [], None))]) # Here the word starts with U+200F RIGHT-TO-LEFT MARK, which should be # ignored when getting the first letter. assert index[5] == ('\u05e2', [( '\N{RIGHT-TO-LEFT MARK}\u05e2\u05d1\u05e8\u05d9\u05ea\N{LEFT-TO-RIGHT MARK}', - [[('', '#index-7')], [], None], + ([('', '#index-7')], [], None), )]) @@ -52,24 +52,24 @@ def test_create_pair_index(app): restructuredtext.parse(app, text) index = IndexEntries(app.env).create_index(app.builder) assert len(index) == 7 - assert index[0] == ('Symbols', [(':+1:', [[], [('Sphinx', [('', '#index-3')])], None])]) + assert index[0] == ('Symbols', [(':+1:', ([], [('Sphinx', [('', '#index-3')])], None))]) assert index[1] == ('D', - [('documentation tool', [[], [('Sphinx', [('', '#index-2')])], None]), - ('docutils', [[], [('reStructuredText', [('', '#index-0')])], None])]) - assert index[2] == ('I', [('interpreter', [[], [('Python', [('', '#index-1')])], None])]) - assert index[3] == ('P', [('Python', [[], [('interpreter', [('', '#index-1')])], None])]) + [('documentation tool', ([], [('Sphinx', [('', '#index-2')])], None)), + ('docutils', ([], [('reStructuredText', [('', '#index-0')])], None))]) + assert index[2] == ('I', [('interpreter', ([], [('Python', [('', '#index-1')])], None))]) + assert index[3] == ('P', [('Python', ([], [('interpreter', [('', '#index-1')])], None))]) assert index[4] == ('R', - [('reStructuredText', [[], [('docutils', [('', '#index-0')])], None])]) + [('reStructuredText', ([], [('docutils', [('', '#index-0')])], None))]) assert index[5] == ('S', - [('Sphinx', [[], + [('Sphinx', ([], [(':+1:', [('', '#index-3')]), ('documentation tool', [('', '#index-2')]), ('ёлка', [('', '#index-5')]), ('Ель', [('', '#index-4')])], - None])]) + None))]) assert index[6] == ('Е', - [('ёлка', [[], [('Sphinx', [('', '#index-5')])], None]), - ('Ель', [[], [('Sphinx', [('', '#index-4')])], None])]) + [('ёлка', ([], [('Sphinx', [('', '#index-5')])], None)), + ('Ель', ([], [('Sphinx', [('', '#index-4')])], None))]) @pytest.mark.sphinx('dummy', freshenv=True) @@ -79,12 +79,12 @@ def test_create_triple_index(app): restructuredtext.parse(app, text) index = IndexEntries(app.env).create_index(app.builder) assert len(index) == 5 - assert index[0] == ('B', [('bar', [[], [('baz, foo', [('', '#index-0')])], None]), - ('baz', [[], [('foo bar', [('', '#index-0')])], None])]) - assert index[1] == ('F', [('foo', [[], [('bar baz', [('', '#index-0')])], None])]) - assert index[2] == ('P', [('Python', [[], [('Sphinx reST', [('', '#index-1')])], None])]) - assert index[3] == ('R', [('reST', [[], [('Python Sphinx', [('', '#index-1')])], None])]) - assert index[4] == ('S', [('Sphinx', [[], [('reST, Python', [('', '#index-1')])], None])]) + assert index[0] == ('B', [('bar', ([], [('baz, foo', [('', '#index-0')])], None)), + ('baz', ([], [('foo bar', [('', '#index-0')])], None))]) + assert index[1] == ('F', [('foo', ([], [('bar baz', [('', '#index-0')])], None))]) + assert index[2] == ('P', [('Python', ([], [('Sphinx reST', [('', '#index-1')])], None))]) + assert index[3] == ('R', [('reST', ([], [('Python Sphinx', [('', '#index-1')])], None))]) + assert index[4] == ('S', [('Sphinx', ([], [('reST, Python', [('', '#index-1')])], None))]) @pytest.mark.sphinx('dummy', freshenv=True) @@ -95,9 +95,9 @@ def test_create_see_index(app): restructuredtext.parse(app, text) index = IndexEntries(app.env).create_index(app.builder) assert len(index) == 3 - assert index[0] == ('D', [('docutils', [[], [('see reStructuredText', [])], None])]) - assert index[1] == ('P', [('Python', [[], [('see interpreter', [])], None])]) - assert index[2] == ('S', [('Sphinx', [[], [('see documentation tool', [])], None])]) + assert index[0] == ('D', [('docutils', ([], [('see reStructuredText', [])], None))]) + assert index[1] == ('P', [('Python', ([], [('see interpreter', [])], None))]) + assert index[2] == ('S', [('Sphinx', ([], [('see documentation tool', [])], None))]) @pytest.mark.sphinx('dummy', freshenv=True) @@ -108,9 +108,9 @@ def test_create_seealso_index(app): restructuredtext.parse(app, text) index = IndexEntries(app.env).create_index(app.builder) assert len(index) == 3 - assert index[0] == ('D', [('docutils', [[], [('see also reStructuredText', [])], None])]) - assert index[1] == ('P', [('Python', [[], [('see also interpreter', [])], None])]) - assert index[2] == ('S', [('Sphinx', [[], [('see also documentation tool', [])], None])]) + assert index[0] == ('D', [('docutils', ([], [('see also reStructuredText', [])], None))]) + assert index[1] == ('P', [('Python', ([], [('see also interpreter', [])], None))]) + assert index[2] == ('S', [('Sphinx', ([], [('see also documentation tool', [])], None))]) @pytest.mark.sphinx('dummy', freshenv=True) @@ -122,10 +122,10 @@ def test_create_main_index(app): restructuredtext.parse(app, text) index = IndexEntries(app.env).create_index(app.builder) assert len(index) == 2 - assert index[0] == ('D', [('docutils', [[('main', '#index-0'), - ('', '#index-1')], [], None])]) - assert index[1] == ('P', [('pip', [[], [('install', [('main', '#index-3'), - ('', '#index-2')])], None])]) + assert index[0] == ('D', [('docutils', ([('main', '#index-0'), + ('', '#index-1')], [], None))]) + assert index[1] == ('P', [('pip', ([], [('install', [('main', '#index-3'), + ('', '#index-2')])], None))]) @pytest.mark.sphinx('dummy', freshenv=True) @@ -140,9 +140,9 @@ def test_create_index_with_name(app): # check index is created correctly assert len(index) == 3 - assert index[0] == ('D', [('docutils', [[('', '#ref1')], [], None])]) - assert index[1] == ('P', [('Python', [[('', '#ref2')], [], None])]) - assert index[2] == ('S', [('Sphinx', [[('', '#index-0')], [], None])]) + assert index[0] == ('D', [('docutils', ([('', '#ref1')], [], None))]) + assert index[1] == ('P', [('Python', ([('', '#ref2')], [], None))]) + assert index[2] == ('S', [('Sphinx', ([('', '#index-0')], [], None))]) # check the reference labels are created correctly std = app.env.get_domain('std') @@ -161,6 +161,6 @@ def test_create_index_by_key(app): restructuredtext.parse(app, text) index = IndexEntries(app.env).create_index(app.builder) assert len(index) == 3 - assert index[0] == ('D', [('docutils', [[('main', '#term-docutils')], [], None])]) - assert index[1] == ('P', [('Python', [[('main', '#term-Python')], [], None])]) - assert index[2] == ('ス', [('スフィンクス', [[('main', '#term-0')], [], 'ス'])]) + assert index[0] == ('D', [('docutils', ([('main', '#term-docutils')], [], None))]) + assert index[1] == ('P', [('Python', ([('main', '#term-Python')], [], None))]) + assert index[2] == ('ス', [('スフィンクス', ([('main', '#term-0')], [], 'ス'))]) diff --git a/tests/test_environment/test_environment_toctree.py b/tests/test_environment/test_environment_toctree.py index 175c6ab..6979a12 100644 --- a/tests/test_environment/test_environment_toctree.py +++ b/tests/test_environment/test_environment_toctree.py @@ -132,7 +132,7 @@ def test_domain_objects(app): assert app.env.toc_num_entries['index'] == 0 assert app.env.toc_num_entries['domains'] == 9 - assert app.env.toctree_includes['index'] == ['domains'] + assert app.env.toctree_includes['index'] == ['domains', 'document_scoping'] assert 'index' in app.env.files_to_rebuild['domains'] assert app.env.glob_toctrees == set() assert app.env.numbered_toctrees == {'index'} @@ -161,6 +161,41 @@ def test_domain_objects(app): [list_item, ([compact_paragraph, reference, literal, "HelloWorldPrinter.print()"])]) +@pytest.mark.sphinx('dummy', testroot='toctree-domain-objects') +def test_domain_objects_document_scoping(app): + app.build() + + # tocs + toctree = app.env.tocs['document_scoping'] + assert_node( + toctree, + [bullet_list, list_item, ( + compact_paragraph, # [0][0] + [bullet_list, ( # [0][1] + [list_item, compact_paragraph, reference, literal, 'ClassLevel1a'], # [0][1][0] + [list_item, ( # [0][1][1] + [compact_paragraph, reference, literal, 'ClassLevel1b'], # [0][1][1][0] + [bullet_list, list_item, compact_paragraph, reference, literal, 'ClassLevel1b.f()'], # [0][1][1][1][0] + )], + [list_item, compact_paragraph, reference, literal, 'ClassLevel1a.g()'], # [0][1][2] + [list_item, compact_paragraph, reference, literal, 'ClassLevel1b.g()'], # [0][1][3] + [list_item, ( # [0][1][4] + [compact_paragraph, reference, 'Level 2'], # [0][1][4][0] + [bullet_list, ( # [0][1][4][1] + [list_item, compact_paragraph, reference, literal, 'ClassLevel2a'], # [0][1][4][1][0] + [list_item, ( # [0][1][4][1][1] + [compact_paragraph, reference, literal, 'ClassLevel2b'], # [0][1][4][1][1][0] + [bullet_list, list_item, compact_paragraph, reference, literal, 'ClassLevel2b.f()'], # [0][1][4][1][1][1][0] + )], + [list_item, compact_paragraph, reference, literal, 'ClassLevel2a.g()'], # [0][1][4][1][2] + [list_item, compact_paragraph, reference, literal, 'ClassLevel2b.g()'], # [0][1][4][1][3] + )], + )], + )], + )], + ) + + @pytest.mark.sphinx('xml', testroot='toctree') @pytest.mark.test_params(shared_result='test_environment_toctree_basic') def test_document_toc(app): diff --git a/tests/test_extensions/test_ext_apidoc.py b/tests/test_extensions/test_ext_apidoc.py index c3c979f..13c43df 100644 --- a/tests/test_extensions/test_ext_apidoc.py +++ b/tests/test_extensions/test_ext_apidoc.py @@ -2,6 +2,7 @@ import os.path from collections import namedtuple +from pathlib import Path import pytest @@ -9,7 +10,7 @@ import sphinx.ext.apidoc from sphinx.ext.apidoc import main as apidoc_main -@pytest.fixture() +@pytest.fixture def apidoc(rootdir, tmp_path, apidoc_params): _, kwargs = apidoc_params coderoot = rootdir / kwargs.get('coderoot', 'test-root') @@ -20,7 +21,7 @@ def apidoc(rootdir, tmp_path, apidoc_params): return namedtuple('apidoc', 'coderoot,outdir')(coderoot, outdir) -@pytest.fixture() +@pytest.fixture def apidoc_params(request): pargs = {} kwargs = {} @@ -661,3 +662,23 @@ def test_no_duplicates(rootdir, tmp_path): finally: sphinx.ext.apidoc.PY_SUFFIXES = original_suffixes + + +def test_remove_old_files(tmp_path: Path): + """Test that old files are removed when using the -r option. + + Also ensure that pre-existing files are not re-written, if unchanged. + This is required to avoid unnecessary rebuilds. + """ + module_dir = tmp_path / 'module' + module_dir.mkdir() + (module_dir / 'example.py').write_text('', encoding='utf8') + gen_dir = tmp_path / 'gen' + gen_dir.mkdir() + (gen_dir / 'other.rst').write_text('', encoding='utf8') + apidoc_main(['-o', str(gen_dir), str(module_dir)]) + assert set(gen_dir.iterdir()) == {gen_dir / 'modules.rst', gen_dir / 'example.rst', gen_dir / 'other.rst'} + example_mtime = (gen_dir / 'example.rst').stat().st_mtime + apidoc_main(['--remove-old', '-o', str(gen_dir), str(module_dir)]) + assert set(gen_dir.iterdir()) == {gen_dir / 'modules.rst', gen_dir / 'example.rst'} + assert (gen_dir / 'example.rst').stat().st_mtime == example_mtime diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index 54f81f2..e10850b 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -429,7 +429,7 @@ def _assert_getter_works(app, directive, objtype, name, attrs=(), **kw): hooked_members = {s[1] for s in getattr_spy} documented_members = {s[1] for s in processed_signatures} for attr in attrs: - fullname = '.'.join((name, attr)) + fullname = f'{name}.{attr}' assert attr in hooked_members assert fullname not in documented_members, f'{fullname!r} not intercepted' @@ -838,7 +838,7 @@ def test_autodoc_special_members(app): "special-members": None, } if sys.version_info >= (3, 13, 0, 'alpha', 5): - options["exclude-members"] = "__static_attributes__" + options["exclude-members"] = "__static_attributes__,__firstlineno__" actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', @@ -1479,7 +1479,7 @@ class _EnumFormatter: return self.entry(name, doc, role='attribute', indent=indent, **rst_options) -@pytest.fixture() +@pytest.fixture def autodoc_enum_options() -> dict[str, object]: """Default autodoc options to use when testing enum's documentation.""" return {"members": None, "undoc-members": None} @@ -2321,18 +2321,62 @@ def test_autodoc_TypeVar(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_Annotated(app): - options = {"members": None} + options = {'members': None, 'member-order': 'bysource'} actual = do_autodoc(app, 'module', 'target.annotated', options) assert list(actual) == [ '', '.. py:module:: target.annotated', '', '', - '.. py:function:: hello(name: str) -> None', + '.. py:class:: FuncValidator(func: function)', + ' :module: target.annotated', + '', + '', + '.. py:class:: MaxLen(max_length: int, whitelisted_words: list[str])', + ' :module: target.annotated', + '', + '', + '.. py:data:: ValidatedString', + ' :module: target.annotated', + '', + ' Type alias for a validated string.', + '', + ' alias of :py:class:`~typing.Annotated`\\ [:py:class:`str`, ' + ':py:class:`~target.annotated.FuncValidator`\\ (func=\\ :py:class:`~target.annotated.validate`)]', + '', + '', + ".. py:function:: hello(name: ~typing.Annotated[str, 'attribute']) -> None", + ' :module: target.annotated', + '', + ' docstring', + '', + '', + '.. py:class:: AnnotatedAttributes()', ' :module: target.annotated', '', ' docstring', '', + '', + ' .. py:attribute:: AnnotatedAttributes.name', + ' :module: target.annotated', + " :type: ~typing.Annotated[str, 'attribute']", + '', + ' Docstring about the ``name`` attribute.', + '', + '', + ' .. py:attribute:: AnnotatedAttributes.max_len', + ' :module: target.annotated', + " :type: list[~typing.Annotated[str, ~target.annotated.MaxLen(max_length=10, whitelisted_words=['word_one', 'word_two'])]]", + '', + ' Docstring about the ``max_len`` attribute.', + '', + '', + ' .. py:attribute:: AnnotatedAttributes.validated', + ' :module: target.annotated', + ' :type: ~typing.Annotated[str, ~target.annotated.FuncValidator(func=~target.annotated.validate)]', + '', + ' Docstring about the ``validated`` attribute.', + '', ] diff --git a/tests/test_extensions/test_ext_autodoc_automodule.py b/tests/test_extensions/test_ext_autodoc_automodule.py index 92565ae..c6ced7e 100644 --- a/tests/test_extensions/test_ext_autodoc_automodule.py +++ b/tests/test_extensions/test_ext_autodoc_automodule.py @@ -4,7 +4,9 @@ This tests mainly the Documenters; the auto directives are tested in a test source file translated by test_build. """ +import inspect import sys +import typing import pytest @@ -185,8 +187,22 @@ def test_automodule_inherited_members(app): 'sphinx.missing_module4']}) @pytest.mark.usefixtures("rollback_sysmodules") def test_subclass_of_mocked_object(app): + from sphinx.ext.autodoc.mock import _MockObject sys.modules.pop('target', None) # unload target module to clear the module cache options = {'members': None} actual = do_autodoc(app, 'module', 'target.need_mocks', options) + # ``typing.Any`` is not available at runtime on ``_MockObject.__new__`` + assert '.. py:class:: Inherited(*args: Any, **kwargs: Any)' in actual + + # make ``typing.Any`` available at runtime on ``_MockObject.__new__`` + sig = inspect.signature(_MockObject.__new__) + parameters = sig.parameters.copy() + for name in ('args', 'kwargs'): + parameters[name] = parameters[name].replace(annotation=typing.Any) + sig = sig.replace(parameters=tuple(parameters.values())) + _MockObject.__new__.__signature__ = sig # type: ignore[attr-defined] + + options = {'members': None} + actual = do_autodoc(app, 'module', 'target.need_mocks', options) assert '.. py:class:: Inherited(*args: ~typing.Any, **kwargs: ~typing.Any)' in actual diff --git a/tests/test_extensions/test_ext_autodoc_configs.py b/tests/test_extensions/test_ext_autodoc_configs.py index 6c2af5a..1262b15 100644 --- a/tests/test_extensions/test_ext_autodoc_configs.py +++ b/tests/test_extensions/test_ext_autodoc_configs.py @@ -679,6 +679,10 @@ def test_autodoc_typehints_signature(app): type_o = "~typing.Any | None" else: type_o = "~typing.Any" + if sys.version_info[:2] >= (3, 13): + type_ppp = "pathlib._local.PurePosixPath" + else: + type_ppp = "pathlib.PurePosixPath" options = {"members": None, "undoc-members": None} @@ -703,7 +707,7 @@ def test_autodoc_typehints_signature(app): '', '.. py:data:: CONST3', ' :module: target.typehints', - ' :type: ~pathlib.PurePosixPath', + f' :type: ~{type_ppp}', " :value: PurePosixPath('/a/b/c')", '', ' docstring', @@ -726,7 +730,7 @@ def test_autodoc_typehints_signature(app): '', ' .. py:attribute:: Math.CONST3', ' :module: target.typehints', - ' :type: ~pathlib.PurePosixPath', + f' :type: ~{type_ppp}', " :value: PurePosixPath('/a/b/c')", '', '', @@ -748,7 +752,7 @@ def test_autodoc_typehints_signature(app): '', ' .. py:property:: Math.path', ' :module: target.typehints', - ' :type: ~pathlib.PurePosixPath', + f' :type: ~{type_ppp}', '', '', ' .. py:property:: Math.prop', @@ -773,7 +777,7 @@ def test_autodoc_typehints_signature(app): '', ' docstring', '', - " alias of TypeVar('T', bound=\\ :py:class:`~pathlib.PurePosixPath`)", + f" alias of TypeVar('T', bound=\\ :py:class:`~{type_ppp}`)", '', '', '.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, ' @@ -802,6 +806,10 @@ def test_autodoc_typehints_signature(app): @pytest.mark.sphinx('html', testroot='ext-autodoc', confoverrides={'autodoc_typehints': "none"}) def test_autodoc_typehints_none(app): + if sys.version_info[:2] >= (3, 13): + type_ppp = "pathlib._local.PurePosixPath" + else: + type_ppp = "pathlib.PurePosixPath" options = {"members": None, "undoc-members": None} actual = do_autodoc(app, 'module', 'target.typehints', options) @@ -887,7 +895,7 @@ def test_autodoc_typehints_none(app): '', ' docstring', '', - " alias of TypeVar('T', bound=\\ :py:class:`~pathlib.PurePosixPath`)", + f" alias of TypeVar('T', bound=\\ :py:class:`~{type_ppp}`)", '', '', '.. py:function:: complex_func(arg1, arg2, arg3=None, *args, **kwargs)', @@ -1417,7 +1425,10 @@ def test_autodoc_typehints_format_fully_qualified(app): type_o = "typing.Any | None" else: type_o = "typing.Any" - + if sys.version_info[:2] >= (3, 13): + type_ppp = "pathlib._local.PurePosixPath" + else: + type_ppp = "pathlib.PurePosixPath" options = {"members": None, "undoc-members": None} actual = do_autodoc(app, 'module', 'target.typehints', options) @@ -1441,7 +1452,7 @@ def test_autodoc_typehints_format_fully_qualified(app): '', '.. py:data:: CONST3', ' :module: target.typehints', - ' :type: pathlib.PurePosixPath', + f' :type: {type_ppp}', " :value: PurePosixPath('/a/b/c')", '', ' docstring', @@ -1464,7 +1475,7 @@ def test_autodoc_typehints_format_fully_qualified(app): '', ' .. py:attribute:: Math.CONST3', ' :module: target.typehints', - ' :type: pathlib.PurePosixPath', + f' :type: {type_ppp}', " :value: PurePosixPath('/a/b/c')", '', '', @@ -1486,7 +1497,7 @@ def test_autodoc_typehints_format_fully_qualified(app): '', ' .. py:property:: Math.path', ' :module: target.typehints', - ' :type: pathlib.PurePosixPath', + f' :type: {type_ppp}', '', '', ' .. py:property:: Math.prop', @@ -1511,7 +1522,7 @@ def test_autodoc_typehints_format_fully_qualified(app): '', ' docstring', '', - " alias of TypeVar('T', bound=\\ :py:class:`pathlib.PurePosixPath`)", + f" alias of TypeVar('T', bound=\\ :py:class:`{type_ppp}`)", '', '', '.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, ' diff --git a/tests/test_extensions/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py index d761978..e3f034c 100644 --- a/tests/test_extensions/test_ext_autosummary.py +++ b/tests/test_extensions/test_ext_autosummary.py @@ -506,12 +506,20 @@ def test_autosummary_recursive(app, status, warning): # Check content of recursively generated stub-files content = (app.srcdir / 'generated' / 'package.rst').read_text(encoding='utf8') - assert 'package.module' in content - assert 'package.package' in content - assert 'package.module_importfail' in content + assert 'module' in content + assert 'package' in content + assert 'module_importfail' in content + # we no longer generate fully-qualified module names. + assert 'package.module' not in content + assert 'package.package' not in content + assert 'package.module_importfail' not in content content = (app.srcdir / 'generated' / 'package.package.rst').read_text(encoding='utf8') - assert 'package.package.module' in content + assert 'module' in content + assert 'package.package.module' not in content + + warnings = app.warning.getvalue() + assert 'Summarised items should not include the current module.' not in warnings @pytest.mark.sphinx('dummy', testroot='ext-autosummary-recursive', @@ -545,7 +553,7 @@ def test_autosummary_filename_map(app, status, warning): @pytest.mark.sphinx('latex', **default_kw) def test_autosummary_latex_table_colspec(app, status, warning): app.build(force_all=True) - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') print(status.getvalue()) print(warning.getvalue()) assert r'\begin{longtable}{\X{1}{2}\X{1}{2}}' in result @@ -599,11 +607,11 @@ def test_autosummary_imported_members(app, status, warning): assert (' .. autosummary::\n' ' \n' ' Bar\n' - ' \n' in module) + ' ' in module) assert (' .. autosummary::\n' ' \n' ' foo\n' - ' \n' in module) + ' ' in module) finally: sys.modules.pop('autosummary_dummy_package', None) @@ -627,7 +635,7 @@ def test_autosummary_module_all(app, status, warning): assert ('.. autosummary::\n' ' :toctree:\n' ' :recursive:\n\n' - ' autosummary_dummy_package_all.extra_dummy_module\n\n' in module) + ' extra_dummy_module\n' in module) finally: sys.modules.pop('autosummary_dummy_package_all', None) @@ -684,3 +692,17 @@ def test_autogen(rootdir, tmp_path): args = ['-o', str(tmp_path), '-t', '.', 'autosummary_templating.txt'] autogen_main(args) assert (tmp_path / 'sphinx.application.TemplateBridge.rst').exists() + + +def test_autogen_remove_old(rootdir, tmp_path): + """Test the ``--remove-old`` option.""" + tmp_path.joinpath('other.rst').write_text('old content') + with chdir(rootdir / 'test-templating'): + args = ['-o', str(tmp_path), '-t', '.', 'autosummary_templating.txt'] + autogen_main(args) + assert set(tmp_path.iterdir()) == { + tmp_path / 'sphinx.application.TemplateBridge.rst', + tmp_path / 'other.rst' + } + autogen_main([*args, '--remove-old']) + assert set(tmp_path.iterdir()) == {tmp_path / 'sphinx.application.TemplateBridge.rst'} diff --git a/tests/test_extensions/test_ext_autosummary_imports.py b/tests/test_extensions/test_ext_autosummary_imports.py new file mode 100644 index 0000000..7420c99 --- /dev/null +++ b/tests/test_extensions/test_ext_autosummary_imports.py @@ -0,0 +1,49 @@ +"""Test autosummary for import cycles.""" + +import pytest +from docutils import nodes + +from sphinx import addnodes +from sphinx.ext.autosummary import autosummary_table +from sphinx.testing.util import assert_node + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary-import_cycle') +@pytest.mark.usefixtures("rollback_sysmodules") +def test_autosummary_import_cycle(app, warning): + app.build() + + doctree = app.env.get_doctree('index') + app.env.apply_post_transforms(doctree, 'index') + + assert len(list(doctree.findall(nodes.reference))) == 1 + + assert_node(doctree, + (addnodes.index, # [0] + nodes.target, # [1] + nodes.paragraph, # [2] + addnodes.tabular_col_spec, # [3] + [autosummary_table, nodes.table, nodes.tgroup, (nodes.colspec, # [4][0][0][0] + nodes.colspec, # [4][0][0][1] + [nodes.tbody, nodes.row])], # [4][0][0][2][1] + addnodes.index, # [5] + addnodes.desc)) # [6] + assert_node(doctree[4][0][0][2][0], + ([nodes.entry, nodes.paragraph, (nodes.reference, nodes.Text)], nodes.entry)) + assert_node(doctree[4][0][0][2][0][0][0][0], nodes.reference, + refid='spam.eggs.Ham', reftitle='spam.eggs.Ham') + + expected = ( + "Summarised items should not include the current module. " + "Replace 'spam.eggs.Ham' with 'Ham'." + ) + assert expected in app.warning.getvalue() + + +@pytest.mark.sphinx('dummy', testroot='ext-autosummary-module_prefix') +@pytest.mark.usefixtures("rollback_sysmodules") +def test_autosummary_generate_prefixes(app, warning): + app.build() + warnings = app.warning.getvalue() + assert 'Summarised items should not include the current module.' not in warnings + assert warnings == '' diff --git a/tests/test_extensions/test_ext_coverage.py b/tests/test_extensions/test_ext_coverage.py index c9e9ba9..ed7b5ad 100644 --- a/tests/test_extensions/test_ext_coverage.py +++ b/tests/test_extensions/test_ext_coverage.py @@ -10,8 +10,10 @@ def test_build(app, status, warning): app.build(force_all=True) py_undoc = (app.outdir / 'python.txt').read_text(encoding='utf8') - assert py_undoc.startswith('Undocumented Python objects\n' - '===========================\n') + assert py_undoc.startswith( + 'Undocumented Python objects\n' + '===========================\n', + ) assert 'autodoc_target\n--------------\n' in py_undoc assert ' * Class -- missing methods:\n' in py_undoc assert ' * raises\n' in py_undoc @@ -23,8 +25,10 @@ def test_build(app, status, warning): assert "undocumented py" not in status.getvalue() c_undoc = (app.outdir / 'c.txt').read_text(encoding='utf8') - assert c_undoc.startswith('Undocumented C API elements\n' - '===========================\n') + assert c_undoc.startswith( + 'Undocumented C API elements\n' + '===========================\n', + ) assert 'api.h' in c_undoc assert ' * Py_SphinxTest' in c_undoc @@ -54,16 +58,26 @@ Undocumented Python objects Statistics ---------- -+----------------------+----------+--------------+ -| Module | Coverage | Undocumented | -+======================+==========+==============+ -| coverage_not_ignored | 0.00% | 2 | -+----------------------+----------+--------------+ -| TOTAL | 0.00% | 2 | -+----------------------+----------+--------------+ ++---------------------------+----------+--------------+ +| Module | Coverage | Undocumented | ++===========================+==========+==============+ +| grog | 100.00% | 0 | ++---------------------------+----------+--------------+ +| grog.coverage_missing | 100.00% | 0 | ++---------------------------+----------+--------------+ +| grog.coverage_not_ignored | 0.00% | 2 | ++---------------------------+----------+--------------+ +| TOTAL | 0.00% | 2 | ++---------------------------+----------+--------------+ + +grog.coverage_missing +--------------------- -coverage_not_ignored --------------------- +Classes: + * Missing + +grog.coverage_not_ignored +------------------------- Classes: * Documented -- missing methods: diff --git a/tests/test_extensions/test_ext_graphviz.py b/tests/test_extensions/test_ext_graphviz.py index 866a92a..cd1fd92 100644 --- a/tests/test_extensions/test_ext_graphviz.py +++ b/tests/test_extensions/test_ext_graphviz.py @@ -105,7 +105,7 @@ def test_graphviz_svg_html(app, status, warning): def test_graphviz_latex(app, status, warning): app.build(force_all=True) - content = (app.outdir / 'python.tex').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') macro = ('\\\\begin{figure}\\[htbp\\]\n\\\\centering\n\\\\capstart\n\n' '\\\\sphinxincludegraphics\\[\\]{graphviz-\\w+.pdf}\n' '\\\\caption{caption of graph}\\\\label{.*}\\\\end{figure}') diff --git a/tests/test_extensions/test_ext_imgconverter.py b/tests/test_extensions/test_ext_imgconverter.py index c1d2061..fee6593 100644 --- a/tests/test_extensions/test_ext_imgconverter.py +++ b/tests/test_extensions/test_ext_imgconverter.py @@ -5,7 +5,7 @@ import subprocess import pytest -@pytest.fixture() +@pytest.fixture def _if_converter_found(app): image_converter = getattr(app.config, 'image_converter', '') try: @@ -24,7 +24,7 @@ def _if_converter_found(app): def test_ext_imgconverter(app, status, warning): app.build(force_all=True) - content = (app.outdir / 'python.tex').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') # supported image (not converted) assert '\\sphinxincludegraphics{{img}.pdf}' in content diff --git a/tests/test_extensions/test_ext_imgmockconverter.py b/tests/test_extensions/test_ext_imgmockconverter.py index 4c3c64e..c155274 100644 --- a/tests/test_extensions/test_ext_imgmockconverter.py +++ b/tests/test_extensions/test_ext_imgmockconverter.py @@ -7,7 +7,7 @@ import pytest def test_ext_imgmockconverter(app, status, warning): app.build(force_all=True) - content = (app.outdir / 'python.tex').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') # check identical basenames give distinct files assert '\\sphinxincludegraphics{{svgimg}.pdf}' in content diff --git a/tests/test_extensions/test_ext_inheritance_diagram.py b/tests/test_extensions/test_ext_inheritance_diagram.py index c13ccea..45a5ff0 100644 --- a/tests/test_extensions/test_ext_inheritance_diagram.py +++ b/tests/test_extensions/test_ext_inheritance_diagram.py @@ -251,7 +251,7 @@ def test_inheritance_diagram_svg_html(tmp_path, app): def test_inheritance_diagram_latex(app, status, warning): app.build(force_all=True) - content = (app.outdir / 'python.tex').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') pattern = ('\\\\begin{figure}\\[htbp]\n\\\\centering\n\\\\capstart\n\n' '\\\\sphinxincludegraphics\\[\\]{inheritance-\\w+.pdf}\n' diff --git a/tests/test_extensions/test_ext_intersphinx.py b/tests/test_extensions/test_ext_intersphinx.py index ef5a9b1..d475c60 100644 --- a/tests/test_extensions/test_ext_intersphinx.py +++ b/tests/test_extensions/test_ext_intersphinx.py @@ -7,10 +7,8 @@ import pytest from docutils import nodes from sphinx import addnodes +from sphinx.builders.html import INVENTORY_FILENAME from sphinx.ext.intersphinx import ( - INVENTORY_FILENAME, - _get_safe_url, - _strip_basic_auth, fetch_inventory, inspect_main, load_mappings, @@ -18,9 +16,14 @@ from sphinx.ext.intersphinx import ( normalize_intersphinx_mapping, ) from sphinx.ext.intersphinx import setup as intersphinx_setup +from sphinx.ext.intersphinx._load import _get_safe_url, _strip_basic_auth from sphinx.util.console import strip_colors -from tests.test_util.intersphinx_data import INVENTORY_V2, INVENTORY_V2_NO_VERSION +from tests.test_util.intersphinx_data import ( + INVENTORY_V2, + INVENTORY_V2_AMBIGUOUS_TERMS, + INVENTORY_V2_NO_VERSION, +) from tests.utils import http_server @@ -46,8 +49,8 @@ def set_config(app, mapping): app.config.intersphinx_disabled_reftypes = [] -@mock.patch('sphinx.ext.intersphinx.InventoryFile') -@mock.patch('sphinx.ext.intersphinx._read_from_url') +@mock.patch('sphinx.ext.intersphinx._load.InventoryFile') +@mock.patch('sphinx.ext.intersphinx._load._read_from_url') def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status, warning): # NoQA: PT019 intersphinx_setup(app) _read_from_url().readline.return_value = b'# Sphinx inventory version 2' @@ -248,6 +251,24 @@ def test_missing_reference_stddomain(tmp_path, app, status, warning): assert rn.astext() == 'The Julia Domain' +def test_ambiguous_reference_warning(tmp_path, app, warning): + inv_file = tmp_path / 'inventory' + inv_file.write_bytes(INVENTORY_V2_AMBIGUOUS_TERMS) + set_config(app, { + 'cmd': ('https://docs.python.org/', str(inv_file)), + }) + + # load the inventory + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + # term reference (case insensitive) + node, contnode = fake_node('std', 'term', 'A TERM', 'A TERM') + missing_reference(app, app.env, node, contnode) + + assert 'multiple matches found for std:term:A TERM' in warning.getvalue() + + @pytest.mark.sphinx('html', testroot='ext-intersphinx-cppdomain') def test_missing_reference_cppdomain(tmp_path, app, status, warning): inv_file = tmp_path / 'inventory' diff --git a/tests/test_extensions/test_ext_math.py b/tests/test_extensions/test_ext_math.py index b673f83..80a5ae7 100644 --- a/tests/test_extensions/test_ext_math.py +++ b/tests/test_extensions/test_ext_math.py @@ -127,7 +127,7 @@ def test_math_number_all_mathjax(app, status, warning): def test_math_number_all_latex(app, status, warning): app.build() - content = (app.outdir / 'python.tex').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') macro = (r'\\begin{equation\*}\s*' r'\\begin{split}a\^2\+b\^2=c\^2\\end{split}\s*' r'\\end{equation\*}') @@ -170,7 +170,7 @@ def test_math_eqref_format_html(app, status, warning): def test_math_eqref_format_latex(app, status, warning): app.build(force_all=True) - content = (app.outdir / 'python.tex').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') macro = (r'Referencing equation Eq.\\ref{equation:math:foo} and ' r'Eq.\\ref{equation:math:foo}.') assert re.search(macro, content, re.DOTALL) @@ -194,6 +194,24 @@ def test_mathjax_numfig_html(app, status, warning): @pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax'], + 'numfig': True, + 'math_numfig': True, + 'math_numsep': '-'}) +def test_mathjax_numsep_html(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'math.html').read_text(encoding='utf8') + html = ('<div class="math notranslate nohighlight" id="equation-math-0">\n' + '<span class="eqno">(1-2)') + assert html in content + html = ('<p>Referencing equation <a class="reference internal" ' + 'href="#equation-foo">(1-1)</a> and ' + '<a class="reference internal" href="#equation-foo">(1-1)</a>.</p>') + assert html in content + + +@pytest.mark.sphinx('html', testroot='ext-math', confoverrides={'extensions': ['sphinx.ext.imgmath'], 'numfig': True, 'numfig_secnum_depth': 0, diff --git a/tests/test_extensions/test_ext_todo.py b/tests/test_extensions/test_ext_todo.py index 1903f9f..5acfcac 100644 --- a/tests/test_extensions/test_ext_todo.py +++ b/tests/test_extensions/test_ext_todo.py @@ -89,7 +89,7 @@ def test_todo_valid_link(app, status, warning): # Ensure the LaTeX output is built. app.build(force_all=True) - content = (app.outdir / 'python.tex').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') # Look for the link to foo. Note that there are two of them because the # source document uses todolist twice. We could equally well look for links diff --git a/tests/test_extensions/test_ext_viewcode.py b/tests/test_extensions/test_ext_viewcode.py index b2c6fc0..800904a 100644 --- a/tests/test_extensions/test_ext_viewcode.py +++ b/tests/test_extensions/test_ext_viewcode.py @@ -42,6 +42,7 @@ def check_viewcode_output(app, warning): @pytest.mark.sphinx(testroot='ext-viewcode', freshenv=True, confoverrides={"viewcode_line_numbers": True}) +@pytest.mark.usefixtures("rollback_sysmodules") def test_viewcode_linenos(app, warning): shutil.rmtree(app.outdir / '_modules', ignore_errors=True) app.build(force_all=True) @@ -52,6 +53,7 @@ def test_viewcode_linenos(app, warning): @pytest.mark.sphinx(testroot='ext-viewcode', freshenv=True, confoverrides={"viewcode_line_numbers": False}) +@pytest.mark.usefixtures("rollback_sysmodules") def test_viewcode(app, warning): shutil.rmtree(app.outdir / '_modules', ignore_errors=True) app.build(force_all=True) @@ -61,6 +63,7 @@ def test_viewcode(app, warning): @pytest.mark.sphinx('epub', testroot='ext-viewcode') +@pytest.mark.usefixtures("rollback_sysmodules") def test_viewcode_epub_default(app, status, warning): shutil.rmtree(app.outdir) app.build(force_all=True) @@ -73,6 +76,7 @@ def test_viewcode_epub_default(app, status, warning): @pytest.mark.sphinx('epub', testroot='ext-viewcode', confoverrides={'viewcode_enable_epub': True}) +@pytest.mark.usefixtures("rollback_sysmodules") def test_viewcode_epub_enabled(app, status, warning): app.build(force_all=True) diff --git a/tests/test_intl/test_catalogs.py b/tests/test_intl/test_catalogs.py index b7fd7be..70c78c5 100644 --- a/tests/test_intl/test_catalogs.py +++ b/tests/test_intl/test_catalogs.py @@ -5,7 +5,7 @@ from pathlib import Path import pytest -@pytest.fixture() +@pytest.fixture def _setup_test(app_params): assert isinstance(app_params.kwargs['srcdir'], Path) srcdir = app_params.kwargs['srcdir'] diff --git a/tests/test_intl/test_intl.py b/tests/test_intl/test_intl.py index 6b1e9ba..e95a78b 100644 --- a/tests/test_intl/test_intl.py +++ b/tests/test_intl/test_intl.py @@ -180,8 +180,11 @@ def test_text_inconsistency_warnings(app, warning): }) assert re.search(expected_warning_expr, warnings), f'{expected_warning_expr!r} did not match {warnings!r}' + expected_citation_ref_warning_expr = ( + '.*/refs_inconsistency.txt:\\d+: WARNING: Citation \\[ref2\\] is not referenced.') + assert re.search(expected_citation_ref_warning_expr, warnings), f'{expected_citation_ref_warning_expr!r} did not match {warnings!r}' + expected_citation_warning_expr = ( - '.*/refs_inconsistency.txt:\\d+: WARNING: Citation \\[ref2\\] is not referenced.\n' + '.*/refs_inconsistency.txt:\\d+: WARNING: citation not found: ref3') assert re.search(expected_citation_warning_expr, warnings), f'{expected_citation_warning_expr!r} did not match {warnings!r}' @@ -286,7 +289,7 @@ VVV """) assert result == expect warnings = getwarning(warning) - assert 'term not in glossary' not in warnings + assert warnings.count('term not in glossary') == 1 @sphinx_intl @@ -298,7 +301,8 @@ def test_text_glossary_term_inconsistencies(app, warning): result = (app.outdir / 'glossary_terms_inconsistency.txt').read_text(encoding='utf8') expect = ("19. I18N WITH GLOSSARY TERMS INCONSISTENCY" "\n******************************************\n" - "\n1. LINK TO *SOME NEW TERM*.\n") + "\n1. LINK TO *SOME NEW TERM*.\n" + "\n2. LINK TO *TERM NOT IN GLOSSARY*.\n") assert result == expect warnings = getwarning(warning) @@ -308,6 +312,10 @@ def test_text_glossary_term_inconsistencies(app, warning): " original: \\[':term:`Some term`', ':term:`Some other term`'\\]," " translated: \\[':term:`SOME NEW TERM`'\\]\n") assert re.search(expected_warning_expr, warnings), f'{expected_warning_expr!r} did not match {warnings!r}' + expected_warning_expr = ( + '.*/glossary_terms_inconsistency.txt:\\d+:<translated>:1: ' + "WARNING: term not in glossary: 'TERM NOT IN GLOSSARY'") + assert re.search(expected_warning_expr, warnings), f'{expected_warning_expr!r} did not match {warnings!r}' @sphinx_intl @@ -729,7 +737,7 @@ class _MockUnixClock(_MockClock): time.sleep(ds) -@pytest.fixture() +@pytest.fixture def mock_time_and_i18n( monkeypatch: pytest.MonkeyPatch, ) -> tuple[pytest.MonkeyPatch, _MockClock]: @@ -930,6 +938,16 @@ def test_html_index_entries(app): start_tag2 = "<%s[^>]*>" % childtag return fr"{start_tag1}\s*{keyword}\s*{start_tag2}" expected_exprs = [ + wrap('h2', 'Symbols'), + wrap('h2', 'C'), + wrap('h2', 'E'), + wrap('h2', 'F'), + wrap('h2', 'M'), + wrap('h2', 'N'), + wrap('h2', 'R'), + wrap('h2', 'S'), + wrap('h2', 'T'), + wrap('h2', 'V'), wrap('a', 'NEWSLETTER'), wrap('a', 'MAILING LIST'), wrap('a', 'RECIPIENTS LIST'), @@ -1191,6 +1209,15 @@ def test_xml_role_xref(app): ['i18n-role-xref', 'index', 'glossary_terms#term-Some-term']) + sec1_1, = sec1.findall('section') + title, = sec1_1.findall('title') + assert_elem( + title, + ['LINK TO', "I18N ROCK'N ROLE XREF", ',', 'CONTENTS', ',', + 'SOME NEW TERM', '.'], + ['i18n-role-xref', 'index', + 'glossary_terms#term-Some-term']) + para2 = sec2.findall('paragraph') assert_elem( para2[0], @@ -1231,7 +1258,7 @@ def test_xml_warnings(app, warning): app.build() # warnings warnings = getwarning(warning) - assert 'term not in glossary' not in warnings + assert warnings.count('term not in glossary') == 1 assert 'undefined label' not in warnings assert 'unknown document' not in warnings @@ -1294,6 +1321,19 @@ def test_xml_label_targets(app): @sphinx_intl +@pytest.mark.sphinx('xml') +@pytest.mark.test_params(shared_result='test_intl_basic') +def test_xml_strange_markup(app): + app.build() + et = etree_parse(app.outdir / 'markup.xml') + secs = et.findall('section') + + subsec1, = secs[0].findall('section') + title1, = subsec1.findall('title') + assert_elem(title1, ['1. TITLE STARTING WITH 1.']) + + +@sphinx_intl @pytest.mark.sphinx('html') @pytest.mark.test_params(shared_result='test_intl_basic') def test_additional_targets_should_not_be_translated(app): @@ -1377,6 +1417,15 @@ def test_additional_targets_should_be_translated(app): # [literalblock.txt] result = (app.outdir / 'literalblock.html').read_text(encoding='utf8') + # basic literal bloc should be translated + expected_expr = ('<span class="n">THIS</span> <span class="n">IS</span>\n' + '<span class="n">LITERAL</span> <span class="n">BLOCK</span>') + assert_count(expected_expr, result, 1) + + # literalinclude should be translated + expected_expr = '<span class="s2">"HTTPS://SPHINX-DOC.ORG"</span>' + assert_count(expected_expr, result, 1) + # title should be translated expected_expr = 'CODE-BLOCKS' assert_count(expected_expr, result, 2) @@ -1411,7 +1460,7 @@ def test_additional_targets_should_be_translated(app): """<span class="c1"># SYS IMPORTING</span>""") assert_count(expected_expr, result, 1) - # '#noqa' should remain in literal blocks. + # 'noqa' comments should remain in literal blocks. assert_count("#noqa", result, 1) # [raw.txt] diff --git a/tests/test_markup/test_markup.py b/tests/test_markup/test_markup.py index c933481..a23219c 100644 --- a/tests/test_markup/test_markup.py +++ b/tests/test_markup/test_markup.py @@ -21,7 +21,7 @@ from sphinx.writers.html import HTML5Translator, HTMLWriter from sphinx.writers.latex import LaTeXTranslator, LaTeXWriter -@pytest.fixture() +@pytest.fixture def settings(app): texescape.init() # otherwise done by the latex builder with warnings.catch_warnings(): @@ -42,7 +42,7 @@ def settings(app): domain_context.disable() -@pytest.fixture() +@pytest.fixture def new_document(settings): def create(): document = utils.new_document('test data', settings) @@ -52,14 +52,14 @@ def new_document(settings): return create -@pytest.fixture() +@pytest.fixture def inliner(new_document): document = new_document() document.reporter.get_source_and_line = lambda line=1: ('dummy.rst', line) return SimpleNamespace(document=document, reporter=document.reporter) -@pytest.fixture() +@pytest.fixture def parse(new_document): def parse_(rst): document = new_document() @@ -90,7 +90,7 @@ class ForgivingLaTeXTranslator(LaTeXTranslator, ForgivingTranslator): pass -@pytest.fixture() +@pytest.fixture def verify_re_html(app, parse): def verify(rst, html_expected): document = parse(rst) @@ -102,7 +102,7 @@ def verify_re_html(app, parse): return verify -@pytest.fixture() +@pytest.fixture def verify_re_latex(app, parse): def verify(rst, latex_expected): document = parse(rst) @@ -117,7 +117,7 @@ def verify_re_latex(app, parse): return verify -@pytest.fixture() +@pytest.fixture def verify_re(verify_re_html, verify_re_latex): def verify_re_(rst, html_expected, latex_expected): if html_expected: @@ -127,7 +127,7 @@ def verify_re(verify_re_html, verify_re_latex): return verify_re_ -@pytest.fixture() +@pytest.fixture def verify(verify_re_html, verify_re_latex): def verify_(rst, html_expected, latex_expected): if html_expected: @@ -137,7 +137,7 @@ def verify(verify_re_html, verify_re_latex): return verify_ -@pytest.fixture() +@pytest.fixture def get_verifier(verify, verify_re): v = { 'verify': verify, diff --git a/tests/test_markup/test_smartquotes.py b/tests/test_markup/test_smartquotes.py index 6c84386..b35f05f 100644 --- a/tests/test_markup/test_smartquotes.py +++ b/tests/test_markup/test_smartquotes.py @@ -41,7 +41,7 @@ def test_text_builder(app, status, warning): def test_man_builder(app, status, warning): app.build() - content = (app.outdir / 'python.1').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.1').read_text(encoding='utf8') assert r'\-\- \(dqSphinx\(dq is a tool that makes it easy ...' in content @@ -49,7 +49,7 @@ def test_man_builder(app, status, warning): def test_latex_builder(app, status, warning): app.build() - content = (app.outdir / 'python.tex').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert '\\textendash{} “Sphinx” is a tool that makes it easy …' in content @@ -94,5 +94,5 @@ def test_smartquotes_excludes_language(app, status, warning): def test_smartquotes_excludes_builders(app, status, warning): app.build() - content = (app.outdir / 'python.1').read_text(encoding='utf8') + content = (app.outdir / 'projectnamenotset.1').read_text(encoding='utf8') assert '– “Sphinx” is a tool that makes it easy …' in content diff --git a/tests/test_search.py b/tests/test_search.py index 63443a8..3687911 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -11,6 +11,14 @@ from docutils.parsers import rst from sphinx.search import IndexBuilder +from tests.utils import TESTS_ROOT + +JAVASCRIPT_TEST_ROOTS = [ + directory + for directory in (TESTS_ROOT / 'js' / 'roots').iterdir() + if (directory / 'conf.py').exists() +] + class DummyEnvironment: def __init__(self, version, domains): @@ -67,6 +75,9 @@ section_title .. test that comments are not indexed: boson +another_title +============= + test that non-comments are indexed: fermion ''' @@ -164,6 +175,10 @@ def test_IndexBuilder(): 'docname2_1': 'title2_1', 'docname2_2': 'title2_2'} assert index._filenames == {'docname1_1': 'filename1_1', 'docname1_2': 'filename1_2', 'docname2_1': 'filename2_1', 'docname2_2': 'filename2_2'} + # note: element iteration order (sort order) is important when the index + # is frozen (serialized) during build -- however, the _mapping-related + # dictionaries below may be iterated in arbitrary order by Python at + # runtime. assert index._mapping == { 'ar': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, 'fermion': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, @@ -172,7 +187,10 @@ def test_IndexBuilder(): 'index': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, 'test': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, } - assert index._title_mapping == {'section_titl': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}} + assert index._title_mapping == { + 'another_titl': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, + 'section_titl': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, + } assert index._objtypes == {} assert index._objnames == {} @@ -192,8 +210,14 @@ def test_IndexBuilder(): 'non': [0, 1, 2, 3], 'test': [0, 1, 2, 3]}, 'titles': ('title1_1', 'title1_2', 'title2_1', 'title2_2'), - 'titleterms': {'section_titl': [0, 1, 2, 3]}, - 'alltitles': {'section_title': [(0, 'section-title'), (1, 'section-title'), (2, 'section-title'), (3, 'section-title')]}, + 'titleterms': { + 'another_titl': [0, 1, 2, 3], + 'section_titl': [0, 1, 2, 3], + }, + 'alltitles': { + 'another_title': [(0, 'another-title'), (1, 'another-title'), (2, 'another-title'), (3, 'another-title')], + 'section_title': [(0, None), (1, None), (2, None), (3, None)], + }, 'indexentries': {}, } assert index._objtypes == {('dummy1', 'objtype1'): 0, ('dummy2', 'objtype1'): 1} @@ -234,7 +258,10 @@ def test_IndexBuilder(): 'index': {'docname1_2', 'docname2_2'}, 'test': {'docname1_2', 'docname2_2'}, } - assert index._title_mapping == {'section_titl': {'docname1_2', 'docname2_2'}} + assert index._title_mapping == { + 'another_titl': {'docname1_2', 'docname2_2'}, + 'section_titl': {'docname1_2', 'docname2_2'}, + } assert index._objtypes == {('dummy1', 'objtype1'): 0, ('dummy2', 'objtype1'): 1} assert index._objnames == {0: ('dummy1', 'objtype1', 'objtype1'), 1: ('dummy2', 'objtype1', 'objtype1')} @@ -253,8 +280,14 @@ def test_IndexBuilder(): 'non': [0, 1], 'test': [0, 1]}, 'titles': ('title1_2', 'title2_2'), - 'titleterms': {'section_titl': [0, 1]}, - 'alltitles': {'section_title': [(0, 'section-title'), (1, 'section-title')]}, + 'titleterms': { + 'another_titl': [0, 1], + 'section_titl': [0, 1], + }, + 'alltitles': { + 'another_title': [(0, 'another-title'), (1, 'another-title')], + 'section_title': [(0, None), (1, None)], + }, 'indexentries': {}, } assert index._objtypes == {('dummy1', 'objtype1'): 0, ('dummy2', 'objtype1'): 1} @@ -343,6 +376,19 @@ def assert_is_sorted(item, path: str): assert_is_sorted(value, f'{path}.{key}') elif isinstance(item, list): if not is_title_tuple_type(item) and path not in lists_not_to_sort: - assert item == sorted(item), f'{err_path} is not sorted' + # sort nulls last; http://stackoverflow.com/questions/19868767/ + assert item == sorted(item, key=lambda x: (x is None, x)), f'{err_path} is not sorted' for i, child in enumerate(item): assert_is_sorted(child, f'{path}[{i}]') + + +@pytest.mark.parametrize('directory', JAVASCRIPT_TEST_ROOTS) +def test_check_js_search_indexes(make_app, sphinx_test_tempdir, directory): + app = make_app('html', srcdir=directory, builddir=sphinx_test_tempdir / directory.name) + app.build() + + fresh_searchindex = (app.outdir / 'searchindex.js') + existing_searchindex = (TESTS_ROOT / 'js' / 'fixtures' / directory.name / 'searchindex.js') + + msg = f"Search index fixture {existing_searchindex} does not match regenerated copy." + assert fresh_searchindex.read_bytes() == existing_searchindex.read_bytes(), msg diff --git a/tests/test_theming/test_theming.py b/tests/test_theming/test_theming.py index 867f8a0..680465b 100644 --- a/tests/test_theming/test_theming.py +++ b/tests/test_theming/test_theming.py @@ -109,10 +109,15 @@ def test_nested_zipped_theme(app, status, warning): @pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'staticfiles'}) def test_staticfiles(app, status, warning): app.build() + assert (app.outdir / '_static' / 'legacytmpl.html').exists() + assert (app.outdir / '_static' / 'legacytmpl.html').read_text(encoding='utf8') == ( + '<!-- testing legacy _t static templates -->\n' + '<html><project>project name not set</project></html>' + ) assert (app.outdir / '_static' / 'staticimg.png').exists() assert (app.outdir / '_static' / 'statictmpl.html').exists() assert (app.outdir / '_static' / 'statictmpl.html').read_text(encoding='utf8') == ( - '<!-- testing static templates -->\n<html><project>Python</project></html>' + '<!-- testing static templates -->\n<html><project>Project name not set</project></html>' ) result = (app.outdir / 'index.html').read_text(encoding='utf8') diff --git a/tests/test_transforms/test_transforms_post_transforms.py b/tests/test_transforms/test_transforms_post_transforms.py index c4e699b..4bd446b 100644 --- a/tests/test_transforms/test_transforms_post_transforms.py +++ b/tests/test_transforms/test_transforms_post_transforms.py @@ -89,7 +89,7 @@ class TestSigElementFallbackTransform: """Fixture returning an ordered view on the original value of :data:`!sphinx.addnodes.SIG_ELEMENTS`.""" return self._builtin_sig_elements - @pytest.fixture() + @pytest.fixture def document( self, app: SphinxTestApp, builtin_sig_elements: tuple[type[addnodes.desc_sig_element], ...], ) -> nodes.document: @@ -103,13 +103,13 @@ class TestSigElementFallbackTransform: doc += addnodes.desc_inline('py') return doc - @pytest.fixture() + @pytest.fixture def with_desc_sig_elements(self, value: Any) -> bool: """Dynamic fixture acting as the identity on booleans.""" assert isinstance(value, bool) return value - @pytest.fixture() + @pytest.fixture def add_visitor_method_for(self, value: Any) -> list[str]: """Dynamic fixture acting as the identity on a list of strings.""" assert isinstance(value, list) diff --git a/tests/test_transforms/test_transforms_post_transforms_code.py b/tests/test_transforms/test_transforms_post_transforms_code.py index 4423d5b..96d5a0c 100644 --- a/tests/test_transforms/test_transforms_post_transforms_code.py +++ b/tests/test_transforms/test_transforms_post_transforms_code.py @@ -34,7 +34,7 @@ def test_trim_doctest_flags_disabled(app, status, warning): def test_trim_doctest_flags_latex(app, status, warning): app.build() - result = (app.outdir / 'python.tex').read_text(encoding='utf8') + result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8') assert 'FOO' not in result assert 'BAR' in result assert 'BAZ' not in result diff --git a/tests/test_transforms/test_transforms_post_transforms_images.py b/tests/test_transforms/test_transforms_post_transforms_images.py new file mode 100644 index 0000000..bb5d076 --- /dev/null +++ b/tests/test_transforms/test_transforms_post_transforms_images.py @@ -0,0 +1,46 @@ +from types import SimpleNamespace + +from docutils import nodes + +from sphinx.transforms.post_transforms.images import ImageConverter +from sphinx.util.docutils import new_document + +WEBP_DATA = ( + b'RIFF\xa8\x01\x00\x00WEBPVP8X\n\x00\x00\x00' + b"\x10\x00\x00\x00\x0f\x00\x00\x0f\x00\x00ALPH\xc3\x00\x00\x00\x01'" + b'\xa2\xa8\x91$\xe5z\xe7\x18_\xe7\xdf*\x99\x88\x98\xfftq\x8d\xe0' + b'&0\xe2\xe1\x8bw2\xc8\xc1\x11\\\x83+0\xe8\xb0x\x15\x8ex' + b'Q5\xc1\x08\x0c\x02O\x92\xa0j\xb0U\x19\x1c\xd6\xb6mF/N' + b'\xc6v<\xb6\xedw\xfb\xaf)\xae!\xa2\xffI\xd1\xfd\x8f\x90\xf7\xba' + b'DI$\x1b:%\x914\xf3\x14m\x0e\xc7\xd3\xe5\x16 \xf4\x0b\x14' + b'\xbe\x90\xe1\x83\xb7\x1a2\x9e6\x82\x7f\x1d)~Nv\x08\xfb\x88\x9e' + b'\xb3\x91\xef\x99sF\xe82\x82\xdb\xf8\xccH\xb2\xf7E0} \xfd' + b'6\x17\x8c!2V-\xa5\xd6k#\xbc]\xe3\xa5Y\x15\xd5\x9c\x81' + b'\xa4\xd9n\x96u\x8a\x181\x0f\x8a\xaa,P4\xfa0\x82\xdf\xbak' + b'PR)\xb5-\xcf\xe9T\x14\n\x01\x00\x00\x00VP8 \xbe\x00' + b'\x00\x00\x90\x02\x00\x9d\x01*\x10\x00\x10\x00\x03\x004%\xb0\x02t0' + b'O\x08\x85\x0c|\x03\x1d\x08,\xfd\xe8\x00\xfe\xfdt\xa0\xfd\x02\x9b\x1f' + b'\x8a\xf7C|\x9c7\xf6\xd2\x0c\xaf\xd3\xff5h\xe2\xee\xa7\xbd\xc9o' + b'\x1b\xf4\xaa\xc5c\xae\xba\x9f\x97\x84\xdfA\xa2;\xda[\xe4\xef\xf8\xcb' + b'\xf1\xbd\x7f\xe1\xaf\xfa?\xe5\t\xec\xf4\xbbf_\xff\xaa)\xd9\x7f\xc9' + b'l\xe7\x86\xe6\xac\x97\xb9\xe4\xc6\xf4\x93#\x8c_\xdd\x8f9U \x7f' + b'\x95O\xfc9\xf8\xffo\xd2k\x03\xe8\x9f\xbc\x83\x98fm\xb1\xd5\x13' + b'\xffv\x17\xe6\xb1\xfe]\x8a\xe4\x9fG\xbf\xb3\xfa\xbf\xfe\x1d\x1d\xf3\x12' + b'\x8f\xfe\\\xcf\xc1\xfa\xf9\x18\xc3\xbd\xcf\xcf\x1f\x919\xa0\x01\xfd\x9a\x01' + b'K1,\xde\xbc\xd9{\xaa\xac\x00\x00\x00' +) + + +def test_guess_mimetype_webp(tmp_path): + document = new_document('<source>') + document.settings.env = SimpleNamespace(app=SimpleNamespace(srcdir=tmp_path)) + converter = ImageConverter(document) + + file_webp = 'webp-image.webp' + image = nodes.image(uri=file_webp, candidates={'*': file_webp}) + assert converter.guess_mimetypes(image) == ['image/webp'] + + file_dat = 'webp-image.dat' + tmp_path.joinpath(file_dat).write_bytes(WEBP_DATA) + image = nodes.image(uri=file_dat, candidates={'*': file_dat}) + assert converter.guess_mimetypes(image) == ['image/webp'] diff --git a/tests/test_util/intersphinx_data.py b/tests/test_util/intersphinx_data.py index 042ee76..95cf80a 100644 --- a/tests/test_util/intersphinx_data.py +++ b/tests/test_util/intersphinx_data.py @@ -50,3 +50,15 @@ INVENTORY_V2_NO_VERSION: Final[bytes] = b'''\ ''' + zlib.compress(b'''\ module1 py:module 0 foo.html#module-module1 Long Module desc ''') + +INVENTORY_V2_AMBIGUOUS_TERMS: Final[bytes] = b'''\ +# Sphinx inventory version 2 +# Project: foo +# Version: 2.0 +# The remainder of this file is compressed with zlib. +''' + zlib.compress(b'''\ +a term std:term -1 glossary.html#term-a-term - +A term std:term -1 glossary.html#term-a-term - +b term std:term -1 document.html#id5 - +B term std:term -1 document.html#B - +''') diff --git a/tests/test_util/test_util_docutils_sphinx_directive.py b/tests/test_util/test_util_docutils_sphinx_directive.py new file mode 100644 index 0000000..8f5ab3f --- /dev/null +++ b/tests/test_util/test_util_docutils_sphinx_directive.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +from types import SimpleNamespace + +from docutils import nodes +from docutils.parsers.rst.languages import en as english # type: ignore[attr-defined] +from docutils.parsers.rst.states import Inliner, RSTState, RSTStateMachine, state_classes +from docutils.statemachine import StringList + +from sphinx.util.docutils import SphinxDirective, new_document + + +def make_directive(*, env: SimpleNamespace, input_lines: StringList | None = None) -> SphinxDirective: + _, directive = make_directive_and_state(env=env, input_lines=input_lines) + return directive + + +def make_directive_and_state(*, env: SimpleNamespace, input_lines: StringList | None = None) -> tuple[RSTState, SphinxDirective]: + sm = RSTStateMachine(state_classes, initial_state='Body') + sm.reporter = object() + if input_lines is not None: + sm.input_lines = input_lines + state = RSTState(sm) + state.document = new_document('<tests>') + state.document.settings.env = env + state.document.settings.tab_width = 4 + state.document.settings.pep_references = None + state.document.settings.rfc_references = None + inliner = Inliner() + inliner.init_customizations(state.document.settings) + state.inliner = inliner + state.parent = None + state.memo = SimpleNamespace( + document=state.document, + language=english, + inliner=state.inliner, + reporter=state.document.reporter, + section_level=0, + title_styles=[], + ) + directive = SphinxDirective( + name='test_directive', + arguments=[], + options={}, + content=StringList(), + lineno=0, + content_offset=0, + block_text='', + state=state, + state_machine=state.state_machine, + ) + return state, directive + + +def test_sphinx_directive_env(): + state, directive = make_directive_and_state(env=SimpleNamespace()) + + assert hasattr(directive, 'env') + assert directive.env is state.document.settings.env + + +def test_sphinx_directive_config(): + env = SimpleNamespace(config=object()) + state, directive = make_directive_and_state(env=env) + + assert hasattr(directive, 'config') + assert directive.config is directive.env.config + assert directive.config is state.document.settings.env.config + + +def test_sphinx_directive_get_source_info(): + env = SimpleNamespace() + input_lines = StringList(['spam'], source='<source>') + directive = make_directive(env=env, input_lines=input_lines) + + assert directive.get_source_info() == ('<source>', 1) + + +def test_sphinx_directive_set_source_info(): + env = SimpleNamespace() + input_lines = StringList(['spam'], source='<source>') + directive = make_directive(env=env, input_lines=input_lines) + + node = nodes.Element() + directive.set_source_info(node) + assert node.source == '<source>' + assert node.line == 1 + + +def test_sphinx_directive_get_location(): + env = SimpleNamespace() + input_lines = StringList(['spam'], source='<source>') + directive = make_directive(env=env, input_lines=input_lines) + + assert directive.get_location() == '<source>:1' + + +def test_sphinx_directive_parse_content_to_nodes(): + directive = make_directive(env=SimpleNamespace()) + content = 'spam\n====\n\nEggs! *Lobster thermidor.*' + directive.content = StringList(content.split('\n'), source='<source>') + + parsed = directive.parse_content_to_nodes(allow_section_headings=True) + assert len(parsed) == 1 + node = parsed[0] + assert isinstance(node, nodes.section) + assert len(node.children) == 2 + assert isinstance(node.children[0], nodes.title) + assert node.children[0].astext() == 'spam' + assert isinstance(node.children[1], nodes.paragraph) + assert node.children[1].astext() == 'Eggs! Lobster thermidor.' + + +def test_sphinx_directive_parse_text_to_nodes(): + directive = make_directive(env=SimpleNamespace()) + content = 'spam\n====\n\nEggs! *Lobster thermidor.*' + + parsed = directive.parse_text_to_nodes(content, allow_section_headings=True) + assert len(parsed) == 1 + node = parsed[0] + assert isinstance(node, nodes.section) + assert len(node.children) == 2 + assert isinstance(node.children[0], nodes.title) + assert node.children[0].astext() == 'spam' + assert isinstance(node.children[1], nodes.paragraph) + assert node.children[1].astext() == 'Eggs! Lobster thermidor.' + + +def test_sphinx_directive_parse_inline(): + directive = make_directive(env=SimpleNamespace()) + content = 'Eggs! *Lobster thermidor.*' + + parsed, messages = directive.parse_inline(content) + assert len(parsed) == 2 + assert messages == [] + assert parsed[0] == nodes.Text('Eggs! ') + assert isinstance(parsed[1], nodes.emphasis) + assert parsed[1].rawsource == '*Lobster thermidor.*' + assert parsed[1][0] == nodes.Text('Lobster thermidor.') diff --git a/tests/test_util/test_util_fileutil.py b/tests/test_util/test_util_fileutil.py index 9c23821..2071fc3 100644 --- a/tests/test_util/test_util_fileutil.py +++ b/tests/test_util/test_util_fileutil.py @@ -2,8 +2,11 @@ from unittest import mock +import pytest + from sphinx.jinja2glue import BuiltinTemplateLoader -from sphinx.util.fileutil import copy_asset, copy_asset_file +from sphinx.util import strip_colors +from sphinx.util.fileutil import _template_basename, copy_asset, copy_asset_file class DummyTemplateLoader(BuiltinTemplateLoader): @@ -28,9 +31,9 @@ def test_copy_asset_file(tmp_path): assert src.read_text(encoding='utf8') == dest.read_text(encoding='utf8') # copy template file - src = (tmp_path / 'asset.txt_t') + src = (tmp_path / 'asset.txt.jinja') src.write_text('# {{var1}} data', encoding='utf8') - dest = (tmp_path / 'output.txt_t') + dest = (tmp_path / 'output.txt.jinja') copy_asset_file(str(src), str(dest), {'var1': 'template'}, renderer) assert not dest.exists() @@ -38,7 +41,7 @@ def test_copy_asset_file(tmp_path): assert (tmp_path / 'output.txt').read_text(encoding='utf8') == '# template data' # copy template file to subdir - src = (tmp_path / 'asset.txt_t') + src = (tmp_path / 'asset.txt.jinja') src.write_text('# {{var1}} data', encoding='utf8') subdir1 = (tmp_path / 'subdir') subdir1.mkdir(parents=True, exist_ok=True) @@ -48,14 +51,14 @@ def test_copy_asset_file(tmp_path): assert (subdir1 / 'asset.txt').read_text(encoding='utf8') == '# template data' # copy template file without context - src = (tmp_path / 'asset.txt_t') + src = (tmp_path / 'asset.txt.jinja') subdir2 = (tmp_path / 'subdir2') subdir2.mkdir(parents=True, exist_ok=True) copy_asset_file(src, subdir2) assert not (subdir2 / 'asset.txt').exists() - assert (subdir2 / 'asset.txt_t').exists() - assert (subdir2 / 'asset.txt_t').read_text(encoding='utf8') == '# {{var1}} data' + assert (subdir2 / 'asset.txt.jinja').exists() + assert (subdir2 / 'asset.txt.jinja').read_text(encoding='utf8') == '# {{var1}} data' def test_copy_asset(tmp_path): @@ -65,12 +68,12 @@ def test_copy_asset(tmp_path): source = (tmp_path / 'source') source.mkdir(parents=True, exist_ok=True) (source / 'index.rst').write_text('index.rst', encoding='utf8') - (source / 'foo.rst_t').write_text('{{var1}}.rst', encoding='utf8') + (source / 'foo.rst.jinja').write_text('{{var1}}.rst', encoding='utf8') (source / '_static').mkdir(parents=True, exist_ok=True) (source / '_static' / 'basic.css').write_text('basic.css', encoding='utf8') (source / '_templates').mkdir(parents=True, exist_ok=True) (source / '_templates' / 'layout.html').write_text('layout.html', encoding='utf8') - (source / '_templates' / 'sidebar.html_t').write_text('sidebar: {{var2}}', encoding='utf8') + (source / '_templates' / 'sidebar.html.jinja').write_text('sidebar: {{var2}}', encoding='utf8') # copy a single file assert not (tmp_path / 'test1').exists() @@ -101,3 +104,25 @@ def test_copy_asset(tmp_path): assert not (destdir / '_static' / 'basic.css').exists() assert (destdir / '_templates' / 'layout.html').exists() assert not (destdir / '_templates' / 'sidebar.html').exists() + + +@pytest.mark.sphinx('html', testroot='util-copyasset_overwrite') +def test_copy_asset_overwrite(app): + app.build() + src = app.srcdir / 'myext_static' / 'custom-styles.css' + dst = app.outdir / '_static' / 'custom-styles.css' + assert ( + f'Copying the source path {src} to {dst} will overwrite data, ' + 'as a file already exists at the destination path ' + 'and the content does not match.\n' + ) in strip_colors(app.status.getvalue()) + + +def test_template_basename(): + assert _template_basename('asset.txt') is None + assert _template_basename('asset.txt.jinja') == 'asset.txt' + assert _template_basename('sidebar.html.jinja') == 'sidebar.html' + + +def test_legacy_template_basename(): + assert _template_basename('asset.txt_t') == 'asset.txt' diff --git a/tests/test_util/test_util_i18n.py b/tests/test_util/test_util_i18n.py index f6baa04..f2f3249 100644 --- a/tests/test_util/test_util_i18n.py +++ b/tests/test_util/test_util_i18n.py @@ -75,16 +75,10 @@ def test_format_date(): format = '%x' assert i18n.format_date(format, date=datet, language='en') == 'Feb 7, 2016' format = '%X' - if BABEL_VERSION >= (2, 12): - assert i18n.format_date(format, date=datet, language='en') == '5:11:17\u202fAM' - else: - assert i18n.format_date(format, date=datet, language='en') == '5:11:17 AM' + assert i18n.format_date(format, date=datet, language='en') == '5:11:17\u202fAM' assert i18n.format_date(format, date=date, language='en') == 'Feb 7, 2016' format = '%c' - if BABEL_VERSION >= (2, 12): - assert i18n.format_date(format, date=datet, language='en') == 'Feb 7, 2016, 5:11:17\u202fAM' - else: - assert i18n.format_date(format, date=datet, language='en') == 'Feb 7, 2016, 5:11:17 AM' + assert i18n.format_date(format, date=datet, language='en') == 'Feb 7, 2016, 5:11:17\u202fAM' assert i18n.format_date(format, date=date, language='en') == 'Feb 7, 2016' # timezone diff --git a/tests/test_util/test_util_inspect.py b/tests/test_util/test_util_inspect.py index 32840b8..764ca20 100644 --- a/tests/test_util/test_util_inspect.py +++ b/tests/test_util/test_util_inspect.py @@ -359,6 +359,10 @@ def test_signature_annotations(): sig = inspect.signature(mod.f25) assert stringify_signature(sig) == '(a, b, /)' + # collapse Literal types + sig = inspect.signature(mod.f26) + assert stringify_signature(sig) == "(x: typing.Literal[1, 2, 3] = 1, y: typing.Literal['a', 'b'] = 'a') -> None" + def test_signature_from_str_basic(): signature = '(a, b, *args, c=0, d="blah", **kwargs)' @@ -662,7 +666,7 @@ def test_getslots(): __slots__ = {'attr': 'docstring'} class Qux: - __slots__ = 'attr' + __slots__ = 'attr' # NoQA: PLC0205 assert inspect.getslots(Foo) is None assert inspect.getslots(Bar) == {'attr': None} diff --git a/tests/test_util/test_util_inventory.py b/tests/test_util/test_util_inventory.py index 81d31b0..211dc17 100644 --- a/tests/test_util/test_util_inventory.py +++ b/tests/test_util/test_util_inventory.py @@ -10,6 +10,7 @@ from sphinx.util.inventory import InventoryFile from tests.test_util.intersphinx_data import ( INVENTORY_V1, INVENTORY_V2, + INVENTORY_V2_AMBIGUOUS_TERMS, INVENTORY_V2_NO_VERSION, ) @@ -48,6 +49,24 @@ def test_read_inventory_v2_not_having_version(): ('foo', '', '/util/foo.html#module-module1', 'Long Module desc') +def test_ambiguous_definition_warning(warning, status): + f = BytesIO(INVENTORY_V2_AMBIGUOUS_TERMS) + InventoryFile.load(f, '/util', posixpath.join) + + def _multiple_defs_notice_for(entity: str) -> str: + return f'contains multiple definitions for {entity}' + + # was warning-level; reduced to info-level - see https://github.com/sphinx-doc/sphinx/issues/12613 + mult_defs_a, mult_defs_b = ( + _multiple_defs_notice_for('std:term:a'), + _multiple_defs_notice_for('std:term:b'), + ) + assert mult_defs_a not in warning.getvalue().lower() + assert mult_defs_a not in status.getvalue().lower() + assert mult_defs_b not in warning.getvalue().lower() + assert mult_defs_b in status.getvalue().lower() + + def _write_appconfig(dir, language, prefix=None): prefix = prefix or language os.makedirs(dir / prefix, exist_ok=True) diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py index 9c28029..956cffe 100644 --- a/tests/test_util/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -1,6 +1,9 @@ """Tests util.typing functions.""" +import dataclasses import sys +import typing as t +from collections import abc from contextvars import Context, ContextVar, Token from enum import Enum from numbers import Integral @@ -28,12 +31,12 @@ from types import ( WrapperDescriptorType, ) from typing import ( + Annotated, Any, - Callable, Dict, - Generator, - Iterator, + ForwardRef, List, + Literal, NewType, Optional, Tuple, @@ -71,6 +74,11 @@ class BrokenType: __args__ = int +@dataclasses.dataclass(frozen=True) +class Gt: + gt: float + + def test_restify(): assert restify(int) == ":py:class:`int`" assert restify(int, "smart") == ":py:class:`int`" @@ -173,20 +181,36 @@ def test_restify_type_hints_containers(): assert restify(MyList[Tuple[int, int]]) == (":py:class:`tests.test_util.test_util_typing.MyList`\\ " "[:py:class:`~typing.Tuple`\\ " "[:py:class:`int`, :py:class:`int`]]") - assert restify(Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ " - "[:py:obj:`None`, :py:obj:`None`, " - ":py:obj:`None`]") - assert restify(Iterator[None]) == (":py:class:`~typing.Iterator`\\ " - "[:py:obj:`None`]") + assert restify(t.Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ " + "[:py:obj:`None`, :py:obj:`None`, " + ":py:obj:`None`]") + assert restify(abc.Generator[None, None, None]) == (":py:class:`collections.abc.Generator`\\ " + "[:py:obj:`None`, :py:obj:`None`, " + ":py:obj:`None`]") + assert restify(t.Iterator[None]) == (":py:class:`~typing.Iterator`\\ " + "[:py:obj:`None`]") + assert restify(abc.Iterator[None]) == (":py:class:`collections.abc.Iterator`\\ " + "[:py:obj:`None`]") -def test_restify_type_hints_Callable(): - assert restify(Callable) == ":py:class:`~typing.Callable`" +def test_restify_Annotated(): + assert restify(Annotated[str, "foo", "bar"]) == ":py:class:`~typing.Annotated`\\ [:py:class:`str`, 'foo', 'bar']" + assert restify(Annotated[str, "foo", "bar"], 'smart') == ":py:class:`~typing.Annotated`\\ [:py:class:`str`, 'foo', 'bar']" + assert restify(Annotated[float, Gt(-10.0)]) == ':py:class:`~typing.Annotated`\\ [:py:class:`float`, :py:class:`tests.test_util.test_util_typing.Gt`\\ (gt=\\ -10.0)]' + assert restify(Annotated[float, Gt(-10.0)], 'smart') == ':py:class:`~typing.Annotated`\\ [:py:class:`float`, :py:class:`~tests.test_util.test_util_typing.Gt`\\ (gt=\\ -10.0)]' - assert restify(Callable[[str], int]) == (":py:class:`~typing.Callable`\\ " - "[[:py:class:`str`], :py:class:`int`]") - assert restify(Callable[..., int]) == (":py:class:`~typing.Callable`\\ " - "[[...], :py:class:`int`]") + +def test_restify_type_hints_Callable(): + assert restify(t.Callable) == ":py:class:`~typing.Callable`" + assert restify(t.Callable[[str], int]) == (":py:class:`~typing.Callable`\\ " + "[[:py:class:`str`], :py:class:`int`]") + assert restify(t.Callable[..., int]) == (":py:class:`~typing.Callable`\\ " + "[[...], :py:class:`int`]") + assert restify(abc.Callable) == ":py:class:`collections.abc.Callable`" + assert restify(abc.Callable[[str], int]) == (":py:class:`collections.abc.Callable`\\ " + "[[:py:class:`str`], :py:class:`int`]") + assert restify(abc.Callable[..., int]) == (":py:class:`collections.abc.Callable`\\ " + "[[...], :py:class:`int`]") def test_restify_type_hints_Union(): @@ -276,7 +300,6 @@ def test_restify_type_hints_alias(): def test_restify_type_ForwardRef(): - from typing import ForwardRef # type: ignore[attr-defined] assert restify(ForwardRef("MyInt")) == ":py:class:`MyInt`" assert restify(list[ForwardRef("MyInt")]) == ":py:class:`list`\\ [:py:class:`MyInt`]" @@ -285,7 +308,6 @@ def test_restify_type_ForwardRef(): def test_restify_type_Literal(): - from typing import Literal # type: ignore[attr-defined] assert restify(Literal[1, "2", "\r"]) == ":py:obj:`~typing.Literal`\\ [1, '2', '\\r']" assert restify(Literal[MyEnum.a], 'fully-qualified-except-typing') == ':py:obj:`~typing.Literal`\\ [:py:attr:`tests.test_util.test_util_typing.MyEnum.a`]' @@ -317,6 +339,30 @@ def test_restify_pep_585(): ":py:class:`int`]") +def test_restify_Unpack(): + from typing_extensions import Unpack as UnpackCompat + + class X(t.TypedDict): + x: int + y: int + label: str + + # Unpack is considered as typing special form so we always have '~' + if sys.version_info[:2] >= (3, 12): + expect = r':py:obj:`~typing.Unpack`\ [:py:class:`X`]' + assert restify(UnpackCompat['X'], 'fully-qualified-except-typing') == expect + assert restify(UnpackCompat['X'], 'smart') == expect + else: + expect = r':py:obj:`~typing_extensions.Unpack`\ [:py:class:`X`]' + assert restify(UnpackCompat['X'], 'fully-qualified-except-typing') == expect + assert restify(UnpackCompat['X'], 'smart') == expect + + if sys.version_info[:2] >= (3, 11): + expect = r':py:obj:`~typing.Unpack`\ [:py:class:`X`]' + assert restify(t.Unpack['X'], 'fully-qualified-except-typing') == expect + assert restify(t.Unpack['X'], 'smart') == expect + + @pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.') def test_restify_type_union_operator(): assert restify(int | None) == ":py:class:`int` | :py:obj:`None`" # type: ignore[attr-defined] @@ -339,6 +385,21 @@ def test_restify_mock(): assert restify(unknown.secret.Class, "smart") == ':py:class:`~unknown.secret.Class`' +@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='ParamSpec not supported in Python 3.9.') +def test_restify_type_hints_paramspec(): + from typing import ParamSpec + P = ParamSpec('P') + + assert restify(P) == ":py:obj:`tests.test_util.test_util_typing.P`" + assert restify(P, "smart") == ":py:obj:`~tests.test_util.test_util_typing.P`" + + assert restify(P.args) == "P.args" + assert restify(P.args, "smart") == "P.args" + + assert restify(P.kwargs) == "P.kwargs" + assert restify(P.kwargs, "smart") == "P.kwargs" + + def test_stringify_annotation(): assert stringify_annotation(int, 'fully-qualified-except-typing') == "int" assert stringify_annotation(int, "smart") == "int" @@ -409,13 +470,21 @@ def test_stringify_type_hints_containers(): assert stringify_annotation(MyList[Tuple[int, int]], "fully-qualified") == "tests.test_util.test_util_typing.MyList[typing.Tuple[int, int]]" assert stringify_annotation(MyList[Tuple[int, int]], "smart") == "~tests.test_util.test_util_typing.MyList[~typing.Tuple[int, int]]" - assert stringify_annotation(Generator[None, None, None], 'fully-qualified-except-typing') == "Generator[None, None, None]" - assert stringify_annotation(Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]" - assert stringify_annotation(Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]" + assert stringify_annotation(t.Generator[None, None, None], 'fully-qualified-except-typing') == "Generator[None, None, None]" + assert stringify_annotation(t.Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]" + assert stringify_annotation(t.Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]" + + assert stringify_annotation(abc.Generator[None, None, None], 'fully-qualified-except-typing') == "collections.abc.Generator[None, None, None]" + assert stringify_annotation(abc.Generator[None, None, None], "fully-qualified") == "collections.abc.Generator[None, None, None]" + assert stringify_annotation(abc.Generator[None, None, None], "smart") == "~collections.abc.Generator[None, None, None]" - assert stringify_annotation(Iterator[None], 'fully-qualified-except-typing') == "Iterator[None]" - assert stringify_annotation(Iterator[None], "fully-qualified") == "typing.Iterator[None]" - assert stringify_annotation(Iterator[None], "smart") == "~typing.Iterator[None]" + assert stringify_annotation(t.Iterator[None], 'fully-qualified-except-typing') == "Iterator[None]" + assert stringify_annotation(t.Iterator[None], "fully-qualified") == "typing.Iterator[None]" + assert stringify_annotation(t.Iterator[None], "smart") == "~typing.Iterator[None]" + + assert stringify_annotation(abc.Iterator[None], 'fully-qualified-except-typing') == "collections.abc.Iterator[None]" + assert stringify_annotation(abc.Iterator[None], "fully-qualified") == "collections.abc.Iterator[None]" + assert stringify_annotation(abc.Iterator[None], "smart") == "~collections.abc.Iterator[None]" def test_stringify_type_hints_pep_585(): @@ -453,9 +522,36 @@ def test_stringify_type_hints_pep_585(): def test_stringify_Annotated(): - from typing import Annotated # type: ignore[attr-defined] - assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "str" - assert stringify_annotation(Annotated[str, "foo", "bar"], "smart") == "str" + assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "Annotated[str, 'foo', 'bar']" + assert stringify_annotation(Annotated[str, "foo", "bar"], 'smart') == "~typing.Annotated[str, 'foo', 'bar']" + assert stringify_annotation(Annotated[float, Gt(-10.0)], 'fully-qualified-except-typing') == "Annotated[float, tests.test_util.test_util_typing.Gt(gt=-10.0)]" + assert stringify_annotation(Annotated[float, Gt(-10.0)], 'smart') == "~typing.Annotated[float, ~tests.test_util.test_util_typing.Gt(gt=-10.0)]" + + +def test_stringify_Unpack(): + from typing_extensions import Unpack as UnpackCompat + + class X(t.TypedDict): + x: int + y: int + label: str + + if sys.version_info[:2] >= (3, 11): + # typing.Unpack is introduced in 3.11 but typing_extensions.Unpack only + # uses typing.Unpack in 3.12+, so the objects are not synchronised with + # each other, but we will assume that users use typing.Unpack. + import typing + + UnpackCompat = typing.Unpack # NoQA: F811 + assert stringify_annotation(UnpackCompat['X']) == 'Unpack[X]' + assert stringify_annotation(UnpackCompat['X'], 'smart') == '~typing.Unpack[X]' + else: + assert stringify_annotation(UnpackCompat['X']) == 'typing_extensions.Unpack[X]' + assert stringify_annotation(UnpackCompat['X'], 'smart') == '~typing_extensions.Unpack[X]' + + if sys.version_info[:2] >= (3, 11): + assert stringify_annotation(t.Unpack['X']) == 'Unpack[X]' + assert stringify_annotation(t.Unpack['X'], 'smart') == '~typing.Unpack[X]' def test_stringify_type_hints_string(): @@ -489,17 +585,29 @@ def test_stringify_type_hints_string(): def test_stringify_type_hints_Callable(): - assert stringify_annotation(Callable, 'fully-qualified-except-typing') == "Callable" - assert stringify_annotation(Callable, "fully-qualified") == "typing.Callable" - assert stringify_annotation(Callable, "smart") == "~typing.Callable" + assert stringify_annotation(t.Callable, 'fully-qualified-except-typing') == "Callable" + assert stringify_annotation(t.Callable, "fully-qualified") == "typing.Callable" + assert stringify_annotation(t.Callable, "smart") == "~typing.Callable" + + assert stringify_annotation(t.Callable[[str], int], 'fully-qualified-except-typing') == "Callable[[str], int]" + assert stringify_annotation(t.Callable[[str], int], "fully-qualified") == "typing.Callable[[str], int]" + assert stringify_annotation(t.Callable[[str], int], "smart") == "~typing.Callable[[str], int]" - assert stringify_annotation(Callable[[str], int], 'fully-qualified-except-typing') == "Callable[[str], int]" - assert stringify_annotation(Callable[[str], int], "fully-qualified") == "typing.Callable[[str], int]" - assert stringify_annotation(Callable[[str], int], "smart") == "~typing.Callable[[str], int]" + assert stringify_annotation(t.Callable[..., int], 'fully-qualified-except-typing') == "Callable[[...], int]" + assert stringify_annotation(t.Callable[..., int], "fully-qualified") == "typing.Callable[[...], int]" + assert stringify_annotation(t.Callable[..., int], "smart") == "~typing.Callable[[...], int]" - assert stringify_annotation(Callable[..., int], 'fully-qualified-except-typing') == "Callable[[...], int]" - assert stringify_annotation(Callable[..., int], "fully-qualified") == "typing.Callable[[...], int]" - assert stringify_annotation(Callable[..., int], "smart") == "~typing.Callable[[...], int]" + assert stringify_annotation(abc.Callable, 'fully-qualified-except-typing') == "collections.abc.Callable" + assert stringify_annotation(abc.Callable, "fully-qualified") == "collections.abc.Callable" + assert stringify_annotation(abc.Callable, "smart") == "~collections.abc.Callable" + + assert stringify_annotation(abc.Callable[[str], int], 'fully-qualified-except-typing') == "collections.abc.Callable[[str], int]" + assert stringify_annotation(abc.Callable[[str], int], "fully-qualified") == "collections.abc.Callable[[str], int]" + assert stringify_annotation(abc.Callable[[str], int], "smart") == "~collections.abc.Callable[[str], int]" + + assert stringify_annotation(abc.Callable[..., int], 'fully-qualified-except-typing') == "collections.abc.Callable[[...], int]" + assert stringify_annotation(abc.Callable[..., int], "fully-qualified") == "collections.abc.Callable[[...], int]" + assert stringify_annotation(abc.Callable[..., int], "smart") == "~collections.abc.Callable[[...], int]" def test_stringify_type_hints_Union(): @@ -578,7 +686,6 @@ def test_stringify_type_hints_alias(): def test_stringify_type_Literal(): - from typing import Literal # type: ignore[attr-defined] assert stringify_annotation(Literal[1, "2", "\r"], 'fully-qualified-except-typing') == "Literal[1, '2', '\\r']" assert stringify_annotation(Literal[1, "2", "\r"], "fully-qualified") == "typing.Literal[1, '2', '\\r']" assert stringify_annotation(Literal[1, "2", "\r"], "smart") == "~typing.Literal[1, '2', '\\r']" @@ -620,8 +727,6 @@ def test_stringify_mock(): def test_stringify_type_ForwardRef(): - from typing import ForwardRef # type: ignore[attr-defined] - assert stringify_annotation(ForwardRef("MyInt")) == "MyInt" assert stringify_annotation(ForwardRef("MyInt"), 'smart') == "MyInt" @@ -631,3 +736,21 @@ def test_stringify_type_ForwardRef(): assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]]) == "Tuple[dict[MyInt, str], list[List[int]]]" # type: ignore[attr-defined] assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]], 'fully-qualified-except-typing') == "Tuple[dict[MyInt, str], list[List[int]]]" # type: ignore[attr-defined] assert stringify_annotation(Tuple[dict[ForwardRef("MyInt"), str], list[List[int]]], 'smart') == "~typing.Tuple[dict[MyInt, str], list[~typing.List[int]]]" # type: ignore[attr-defined] + + +@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='ParamSpec not supported in Python 3.9.') +def test_stringify_type_hints_paramspec(): + from typing import ParamSpec + P = ParamSpec('P') + + assert stringify_annotation(P, 'fully-qualified') == "~P" + assert stringify_annotation(P, 'fully-qualified-except-typing') == "~P" + assert stringify_annotation(P, "smart") == "~P" + + assert stringify_annotation(P.args, 'fully-qualified') == "typing.~P" + assert stringify_annotation(P.args, 'fully-qualified-except-typing') == "~P" + assert stringify_annotation(P.args, "smart") == "~typing.~P" + + assert stringify_annotation(P.kwargs, 'fully-qualified') == "typing.~P" + assert stringify_annotation(P.kwargs, 'fully-qualified-except-typing') == "~P" + assert stringify_annotation(P.kwargs, "smart") == "~typing.~P" diff --git a/tests/test_util/typing_test_data.py b/tests/test_util/typing_test_data.py index e29b600..0588836 100644 --- a/tests/test_util/typing_test_data.py +++ b/tests/test_util/typing_test_data.py @@ -1,6 +1,6 @@ from inspect import Signature from numbers import Integral -from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union +from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, TypeVar, Union def f0(x: int, y: Integral) -> None: @@ -121,6 +121,10 @@ def f25(a, b, /): pass +def f26(x: Literal[1, 2, 3] = 1, y: Union[Literal["a"], Literal["b"]] = "a") -> None: + pass + + class Node: def __init__(self, parent: Optional['Node']) -> None: pass |