diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 06309f991..f90d4aeb7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,6 +46,8 @@ jobs: if: endsWith(matrix.python, '-dev') with: python-version: ${{ matrix.python }} + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true - name: Check Python version run: python --version - name: Install graphviz diff --git a/CHANGES b/CHANGES index 0e9575105..b6bfa0e93 100644 --- a/CHANGES +++ b/CHANGES @@ -40,6 +40,8 @@ Bugs fixed * #4606: autodoc: the location of the warning is incorrect for inherited method * #8105: autodoc: the signature of class constructor is incorrect if the class is decorated +* #8434: autodoc: :confval:`autodoc_type_aliases` does not effect to variables + and attributes Testing -------- diff --git a/setup.cfg b/setup.cfg index 56d19663b..a4d46044b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,9 +29,11 @@ directory = sphinx/locale/ [flake8] max-line-length = 95 ignore = E116,E241,E251,E741,W504,I101 -exclude = .git,.tox,.venv,tests/*,build/*,doc/_build/*,sphinx/search/*,doc/usage/extensions/example*.py +exclude = .git,.tox,.venv,tests/roots/*,build/*,doc/_build/*,sphinx/search/*,doc/usage/extensions/example*.py application-import-names = sphinx import-order-style = smarkets +per-file-ignores = + tests/*: E501 [flake8:local-plugins] extension = diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index eaf2ed3f7..ffe9012d3 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1703,7 +1703,8 @@ class DataDocumenter(ModuleLevelDocumenter): if not self.options.annotation: # obtain annotation for this data try: - annotations = get_type_hints(self.parent) + annotations = get_type_hints(self.parent, None, + self.config.autodoc_type_aliases) except NameError: # Failed to evaluate ForwardRef (maybe TYPE_CHECKING) annotations = safe_getattr(self.parent, '__annotations__', {}) @@ -2095,7 +2096,8 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): if not self.options.annotation: # obtain type annotation for this attribute try: - annotations = get_type_hints(self.parent) + annotations = get_type_hints(self.parent, None, + self.config.autodoc_type_aliases) except NameError: # Failed to evaluate ForwardRef (maybe TYPE_CHECKING) annotations = safe_getattr(self.parent, '__annotations__', {}) @@ -2272,8 +2274,8 @@ class SlotsAttributeDocumenter(AttributeDocumenter): % self.__class__.__name__, RemovedInSphinx50Warning, stacklevel=2) name = self.objpath[-1] - __slots__ = safe_getattr(self.parent, '__slots__', []) - if isinstance(__slots__, dict) and isinstance(__slots__.get(name), str): + __slots__ = inspect.getslots(self.parent) + if __slots__ and isinstance(__slots__.get(name, None), str): docstring = prepare_docstring(__slots__[name]) return [docstring] else: diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 29c56c54b..adfa6d05d 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -16,7 +16,7 @@ from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Optional, Tup from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.pycode import ModuleAnalyzer from sphinx.util import logging -from sphinx.util.inspect import isclass, isenumclass, safe_getattr +from sphinx.util.inspect import getslots, isclass, isenumclass, safe_getattr if False: # For type annotation @@ -203,14 +203,15 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, members[name] = Attribute(name, True, value) # members in __slots__ - if isclass(subject) and getattr(subject, '__slots__', None) is not None: - from sphinx.ext.autodoc import SLOTSATTR + try: + __slots__ = getslots(subject) + if __slots__: + from sphinx.ext.autodoc import SLOTSATTR - slots = subject.__slots__ - if isinstance(slots, str): - slots = [slots] - for name in slots: - members[name] = Attribute(name, True, SLOTSATTR) + for name in __slots__: + members[name] = Attribute(name, True, SLOTSATTR) + except (TypeError, ValueError): + pass # other members for name in dir(subject): diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index fbe21c7c0..4f4f12e1d 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -137,6 +137,27 @@ def unwrap_all(obj: Any, *, stop: Callable = None) -> Any: return obj +def getslots(obj: Any) -> Optional[Dict]: + """Get __slots__ attribute of the class as dict. + + Return None if gienv *obj* does not have __slots__. + """ + if not inspect.isclass(obj): + raise TypeError + + __slots__ = safe_getattr(obj, '__slots__', None) + if __slots__ is None: + return None + elif isinstance(__slots__, dict): + return __slots__ + elif isinstance(__slots__, str): + return {__slots__: None} + elif isinstance(__slots__, (list, tuple)): + return {e: None for e in __slots__} + else: + raise ValueError + + def isenumclass(x: Any) -> bool: """Check if the object is subclass of enum.""" return inspect.isclass(x) and issubclass(x, enum.Enum) diff --git a/tests/certs/cert.pem b/tests/certs/cert.pem index 4e48764bd..6f8c35c6b 100644 --- a/tests/certs/cert.pem +++ b/tests/certs/cert.pem @@ -1,50 +1,50 @@ -----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDLbT8rECMOtm7H -6KwFnCiS2kUjZCy/3yJGpIeGoTLXP+N8a1LUUIX3MJ8qu1vsPK6Yz1h/+Xiv73xy -WjwK6Di17q35VyYVYiRjuhIsiH1dwwYfNE/hO3QhLOEtBK7+BTWUQ36K+wRFBf/Y -VynHh7EvKawCWSmaZVyqjj2XS83mp1A8Jt0MTA4A4RyhV4aS16VviSeRf3OcVd69 -qtvf3MClrDFRyFnCIE6ajfp1hrZ5wC7Fv5d0A0dKJ2MGYoblUPT/qq+b8jn1nxPv -uSHo4XhjWXq+k05idIl7mLk7wurmgHjmslB0/ZdE4wNa6oANLevF4vxiCGJ6c9gf -8Q56gaUZAgMBAAECggEAXoXguC3DXG7AgvtGE0VARRxOy+ccM/uGfbStlI0KhqIV -HhbwYd8YoIdjLgPo7pgzuKV/xdcxkO6CsM/k3lyRHVhOVnF8LKtxpTUshKzXM94O -1ikEhO+PQmsMJlLqzPW2s7G49vM0RK5I90lpDGGsnvGKD31Gq4s1x7pYPBjpD6cS -LBzbvPgrrfUNgLGeaIyyJDJSyBrSaNbtHTDjtF/i72KSNUEIfRepCm8IQYrvnEYU -E8+fzJNvDz2qF1lA58AkkaDznCLTa9pV1DEFwvXJ3N4nN+CZdsZ9jmrSPQ4uTcJG -bSU9jPrfFvsP7nQJHx66uD5qEy/SWqDfN7pDqsgFkQKBgQD1TWxI0CihfxM/z0mu -V6nkaYBNtuJxllFu1azJpMBAqu/40GW4EtEq6s2lEig/6ct2+bLZcT+u6DVUHeGv -DzA3Wu8mrzIChT7+4c080CMBKX8zcADK1IpeTV5DMR1sGgUV+OqYG7z8Vz+PyfMs -UzR1dCGjcdAcRFVg6nlIeDAlBwKBgQDUTFHvHMyPhzY+jwKFHxMIIvMaZZQ1z7uN -Oks0DHl7yD+4qH/fRWJBS0YtxWXIuA7xnnz1MMWVEJAXrR/CvnrBuyYWYhZnziU4 -rWwqEK1o+sTS8QY+5NcQ4Hrzmszx3qtNjBBUwudkmwiCP5b9eyk68Xj2mbdNjhfK -xCIqPc583wKBgHk5KLEXBW1Bwj5/btcUhWXWaUx+e4tMkLOoLrp7i3Kpxut7+Tit -O+bsoHHZ9kAXhrAmF6dzWthR8sC9/6Cmbdp9OsAwRhOOy6Hj7qwF47aYTj8aM5oI -zNRrgZDM/dBFT4wbNbuzwYImj8e8MksOV1dP66u8++5sKpE5bnRMyOYTAoGACkZ3 -YL9gF0JQGc8KLC9I2If4hDqOZdxcE4XSxf4kkx0qGGHvbnsJOmfOScDYIFLoRkGJ -gsSNi511m+/BLcfSYTYRrdupgfS0UH30UkTkX8RjamJIDxs8XZC/4rKHYN2KJQK2 -d6PHV1M5ojQ5tqMTZ8rwM99Uw+gwtpuvm6PKLrkCgYAxlz2Aw4VWDuHhGOYM3htw -6mdU0kcC9oupQGFbWrw6S8kJhSN3cS2e3ZtIfy841fFvi0zNIxPNW25CiP+aplZ3 -EFUTEUpVg7OQP57KBRcztp2N5FZUKSwq/FUOuxJWCxIYe/nsqQ7Xet5cxRaK7vb4 -hFJQbfh2LfWCK7O+kpXXtg== +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9fzHGBPNaZNcN +nL/1nvO2xJR/E64vFua3QfPQQ5HpigjrK/HRUlRGztRKJ+CEjCXNYNfQ4dUcV45o +k5uPH3U1CkAw2d/We+kZnAHkNuw4mRC0ohdzpUByyDOA5WtUWPn9SwhXCVz6fM7e +I52auvzpUE6soVDM3nucnqZDJ3Ua9KgB02FrqX13S76Uq+uf8Q2hpTruO/nBzB4p +6xFwJJ1taXEEWi8swg6HO8/+0x0AeripV6JieNUptEFuV9kLvRz9qGg0CO2f7AdI +jNeFDGrgO7qJ+VxXV9Gnbi6ph4vsUwtJZB3phRGGomdgiRd6PSma81nvTe1z69x/ +g+8P091pAgMBAAECggEAIrTABfd0JpMffAPAeJjjJA8+70NIfKFiIiA3Kmalu7Mn +TQMgZ+j/PHS3FtnU2hHc/o+FF2G1KVqz311heUYWrl8xQIE26M6K88DJ6+VPQFJw +Z9TkHK8gbaVTIYFjNfCR4J00atRxLgNb0/2L6QHkPksSDbYB2XPKCfZYlyYL4aKq +dePghFu9ePXhUXooPCqke+kP0b8OmHzPlmJpxbeb8ujiox2+4wYjN8lWPz8xHv8i +IM7V5hAbPIaQfu/joKrRKk+Kk8UqGurkKQ75KLLL+1oaJO/GLTQ4bk5tpRgfWPda +aEBzSPrnqame2CKUWtBughuRWSxdTIMvdXIC/ym1gQKBgQDx6Nyio/L6I5CdlXwC +HAzBCy1mnj70Kj97tQc+A/0z8dD7fCSE/oo8IiEKixcjnaSxHk8VjINF/w17n63W +8neE7pVsuDwxfhiQ9ZRI1WpV0LsFEoTrEWG7Ax8UzbHXCQbNJ9SI0HJRo9UN0f/Z +t+ZT+HNUzdcpCwTvdRVDisbXcQKBgQDIiMz58GFEwdGPXJKEhSyQ3kSQBjeqo0Vl +wMDuDvFEckHl/p1RnDo0lzaq6FivOX84ymvGNdQW14TnQp3A/mkQ5o6k/e1pfAA6 +X0Y6tBH/QppVo5sFvOufyn02k48k5pFAjLHH9L9i0dyWqq4V6PgA2uk4qilFxEg/ +CJEVfq4ZeQKBgQCZPHKWq9f8T48J42kcRPxnRFdMC63BKQnxqOifhhNcVi+VPjw7 +6qlSEiRv80+DBhcPAy4BbnKxYjD+QFX0NL80+5S3u7SVfVS+bnGx+U5UcdYmDmcY +KHiJ6B5GJU4j8tnWFwbwa2ofAPKywHWbSnyicF1OON20aACGVtpTYJM4YQKBgBW4 +09NDGZY0FHoeAfT+4/vxR6X+NmtyciL6hSuETNgoNEEwmmPrs1ZdBtvufSTF6qUB +MDlxPT8YK1pNmf78z+63ur3ej6f8eZ3ZEidruANZeJRMO4+cjj1p1rRhuYC6xQMj ++mH5ff27U9SyOlc/PBYDoH212PCouVaym9yjM0KpAoGBALr583slY55ESOthLrfX +1ecoET5xxRm431XbZMnxu0uUvHWNfqoojtmD7laclb9HwkpShPB6PT1egBIvDWWM +bVUuXzJ8gP0tIG3dHgiiUlld3ahOiaMYSU77uLFBRWv5sQqfewLuFvlzHn/2ZSt7 +TcipT4f67b18W8iuLJELEs57 -----END PRIVATE KEY----- -----BEGIN CERTIFICATE----- -MIIDozCCAougAwIBAgIUfuExHknCb9E7HvWowDBr/pvOcx8wDQYJKoZIhvcNAQEL +MIIDuTCCAqGgAwIBAgIUUNvkPwe0W8C2I0+KnLpMaQ+S+vowDQYJKoZIhvcNAQEL BQAwYTELMAkGA1UEBhMCRlIxETAPBgNVBAgMCEJyZXRhZ25lMQ8wDQYDVQQHDAZS ZW5uZXMxGjAYBgNVBAoMEVNwaGlueCB0ZXN0IHN1aXRlMRIwEAYDVQQDDAlsb2Nh -bGhvc3QwHhcNMjAxMTE0MTE0MzQ3WhcNMzAxMTEyMTE0MzQ3WjBhMQswCQYDVQQG +bGhvc3QwHhcNMjAxMTE1MTcyNDExWhcNMzAxMTEzMTcyNDExWjBhMQswCQYDVQQG EwJGUjERMA8GA1UECAwIQnJldGFnbmUxDzANBgNVBAcMBlJlbm5lczEaMBgGA1UE CgwRU3BoaW54IHRlc3Qgc3VpdGUxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJ -KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMttPysQIw62bsforAWcKJLaRSNkLL/f -Ikakh4ahMtc/43xrUtRQhfcwnyq7W+w8rpjPWH/5eK/vfHJaPAroOLXurflXJhVi -JGO6EiyIfV3DBh80T+E7dCEs4S0Erv4FNZRDfor7BEUF/9hXKceHsS8prAJZKZpl -XKqOPZdLzeanUDwm3QxMDgDhHKFXhpLXpW+JJ5F/c5xV3r2q29/cwKWsMVHIWcIg -TpqN+nWGtnnALsW/l3QDR0onYwZihuVQ9P+qr5vyOfWfE++5IejheGNZer6TTmJ0 -iXuYuTvC6uaAeOayUHT9l0TjA1rqgA0t68Xi/GIIYnpz2B/xDnqBpRkCAwEAAaNT -MFEwHQYDVR0OBBYEFNkkFFS7VnquYF3ncePQNfFzpU+hMB8GA1UdIwQYMBaAFNkk -FFS7VnquYF3ncePQNfFzpU+hMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL -BQADggEBAGgfOaXba4tnb+DORKUWm0WQ5umV8LKDr3BMjh8BZ2m/GWpFoL19o6LB -7S0oPdWnuRSy92SqAODt10e/PhepSpBrRa12g92AgAicgfnEutlY4zB8amCJBN/m -TemAAjMrBwxd6gD8tadu06Ye5ml+eMZ3S2JFPjQQgij//sDv89nl0JP+jOiwbDGn -cPmQSdP+6EH5ng5uKb6FXGKt29g3ZSfAzHS3+eCY0rkQnZWye9ZS2AaBTW/IKd2N -0VcJWXcWLx0XRi5zWVp7TkwGdiZRmB7NxXDbGuHSfq8n42FiKQnyIyNQyTmMeOgw -WHqLRjX6vLio3RwWAWleprLfHq6Jyf8= +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAL1/McYE81pk1w2cv/We87bElH8Tri8W +5rdB89BDkemKCOsr8dFSVEbO1Eon4ISMJc1g19Dh1RxXjmiTm48fdTUKQDDZ39Z7 +6RmcAeQ27DiZELSiF3OlQHLIM4Dla1RY+f1LCFcJXPp8zt4jnZq6/OlQTqyhUMze +e5yepkMndRr0qAHTYWupfXdLvpSr65/xDaGlOu47+cHMHinrEXAknW1pcQRaLyzC +Doc7z/7THQB6uKlXomJ41Sm0QW5X2Qu9HP2oaDQI7Z/sB0iM14UMauA7uon5XFdX +0aduLqmHi+xTC0lkHemFEYaiZ2CJF3o9KZrzWe9N7XPr3H+D7w/T3WkCAwEAAaNp +MGcwHQYDVR0OBBYEFN1iHZj88N6eI2FlRzza52xzOU5EMB8GA1UdIwQYMBaAFN1i +HZj88N6eI2FlRzza52xzOU5EMA8GA1UdEwEB/wQFMAMBAf8wFAYDVR0RBA0wC4IJ +bG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQBVUZm1iw7N7uZu/SF3hailxS+1 +3KChItWu3ZOIjlmDIkaJ9kWqP2ficUg3tBUx6/UOjHQAwRC4rj87BoSV2mEy+0OX +fyy+ER/BeHYly5v+hpjVojVKeqysk5CKttZM+cOibT2SzLLYf0InNqZRQRJco+nL +QNR0hVo/Lz6Mf1gF2ywf9bXSF3+XECU4K6sVm4QpFbJNm+fHqJBuh1LXHRrcTAsP +LM6PBnd3P5QTcr/G0s/tYMPmero9YHZUO8FMvMVoI2K8k6/duG/EbBaNzriRI1OM +PpZGCWxbJfyApnzc5lGAG4zJnV/wpOyNhKJuW9N1fr2oEwPpJlS3VzrgeKcY -----END CERTIFICATE----- diff --git a/tests/roots/test-ext-autodoc/target/annotations.py b/tests/roots/test-ext-autodoc/target/annotations.py index 691176b08..e9ff2f604 100644 --- a/tests/roots/test-ext-autodoc/target/annotations.py +++ b/tests/roots/test-ext-autodoc/target/annotations.py @@ -4,6 +4,9 @@ from typing import overload myint = int +#: docstring +variable: myint + def sum(x: myint, y: myint) -> myint: """docstring""" @@ -23,3 +26,10 @@ def mult(x: float, y: float) -> float: def mult(x, y): """docstring""" return x, y + + +class Foo: + """docstring""" + + #: docstring + attr: myint diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index ebaeecef2..864f8b37c 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -20,7 +20,7 @@ from test_build_html import ENV_WARNINGS from sphinx.builders.latex import default_latex_documents from sphinx.config import Config -from sphinx.errors import SphinxError, ThemeError +from sphinx.errors import SphinxError from sphinx.testing.util import strip_escseq from sphinx.util import docutils from sphinx.util.osutil import cd, ensuredir @@ -763,7 +763,7 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): assert ('\\caption{This is the figure caption with a footnote to ' '\\sphinxfootnotemark[7].}\\label{\\detokenize{index:id29}}\\end{figure}\n' '%\n\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' - 'Footnote in caption\n%\n\\end{footnotetext}')in result + 'Footnote in caption\n%\n\\end{footnotetext}') in result assert ('\\sphinxcaption{footnote \\sphinxfootnotemark[8] in ' 'caption of normal table}\\label{\\detokenize{index:id30}}') in result assert ('\\caption{footnote \\sphinxfootnotemark[9] ' diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py index 9149e8121..965a8576d 100644 --- a/tests/test_build_linkcheck.py +++ b/tests/test_build_linkcheck.py @@ -10,15 +10,12 @@ import http.server import json -import os -import re import textwrap -from unittest import mock import pytest import requests -from utils import CERT_FILE, http_server, https_server +from utils import CERT_FILE, http_server, https_server, modify_env @pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True) @@ -60,7 +57,7 @@ def test_defaults_json(app): assert len(rows) == 10 # the output order of the rows is not stable # due to possible variance in network latency - rowsby = {row["uri"]:row for row in rows} + rowsby = {row["uri"]: row for row in rows} assert rowsby["https://www.google.com#!bar"] == { 'filename': 'links.txt', 'lineno': 10, @@ -113,6 +110,7 @@ def test_anchors_ignored(app): # expect all ok when excluding #top assert not content + @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-anchor', freshenv=True) def test_raises_for_invalid_status(app): class InternalServerErrorHandler(http.server.BaseHTTPRequestHandler): @@ -138,6 +136,7 @@ class HeadersDumperHandler(http.server.BaseHTTPRequestHandler): self.end_headers() print(self.headers.as_string()) + @pytest.mark.sphinx( 'linkcheck', testroot='linkcheck-localserver', freshenv=True, confoverrides={'linkcheck_auth': [ @@ -213,6 +212,7 @@ def test_linkcheck_request_headers_default(app, capsys): assert "Accepts: application/json\n" not in stdout assert "X-Secret: open sesami\n" in stdout + def make_redirect_handler(*, support_head): class RedirectOnceHandler(http.server.BaseHTTPRequestHandler): def do_HEAD(self): @@ -236,6 +236,7 @@ def make_redirect_handler(*, support_head): return RedirectOnceHandler + @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_follows_redirects_on_HEAD(app, capsys): with http_server(make_redirect_handler(support_head=True)): @@ -253,6 +254,7 @@ def test_follows_redirects_on_HEAD(app, capsys): """ ) + @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_follows_redirects_on_GET(app, capsys): with http_server(make_redirect_handler(support_head=False)): @@ -281,6 +283,7 @@ class OKHandler(http.server.BaseHTTPRequestHandler): self.do_HEAD() self.wfile.write(b"ok\n") + @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) def test_invalid_ssl(app): # Link indicates SSL should be used (https) but the server does not handle it. @@ -295,6 +298,7 @@ def test_invalid_ssl(app): assert content["uri"] == "https://localhost:7777/" assert "SSLError" in content["info"] + @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) def test_connect_to_selfsigned_fails(app): with https_server(OKHandler): @@ -308,6 +312,7 @@ def test_connect_to_selfsigned_fails(app): assert content["uri"] == "https://localhost:7777/" 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 @@ -325,6 +330,7 @@ def test_connect_to_selfsigned_with_tls_verify_false(app): "info": "", } + @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 @@ -342,10 +348,10 @@ def test_connect_to_selfsigned_with_tls_cacerts(app): "info": "", } + @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) def test_connect_to_selfsigned_with_requests_env_var(app): - os.environ["REQUESTS_CA_BUNDLE"] = CERT_FILE - with https_server(OKHandler): + with modify_env(REQUESTS_CA_BUNDLE=CERT_FILE), https_server(OKHandler): app.builder.build_all() with open(app.outdir / 'output.json') as fp: @@ -358,3 +364,21 @@ def test_connect_to_selfsigned_with_requests_env_var(app): "uri": "https://localhost:7777/", "info": "", } + + +@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" + with https_server(OKHandler): + app.builder.build_all() + + with open(app.outdir / 'output.json') as fp: + content = json.load(fp) + assert content == { + "code": 0, + "status": "broken", + "filename": "index.rst", + "lineno": 1, + "uri": "https://localhost:7777/", + "info": "Could not find a suitable TLS CA certificate bundle, invalid path: does/not/exist", + } diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py index 57a7c49e6..28e74e7bf 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domain_c.py @@ -75,12 +75,12 @@ def _check(name, input, idDict, output, key, asTextOutput): idExpected.append(idExpected[i - 1]) idActual = [None] for i in range(1, _max_id + 1): - #try: + # try: id = ast.get_id(version=i) assert id is not None idActual.append(id[len(_id_prefix[i]):]) - #except NoOldIdError: - # idActual.append(None) + # except NoOldIdError: + # idActual.append(None) res = [True] for i in range(1, _max_id + 1): @@ -94,7 +94,7 @@ def _check(name, input, idDict, output, key, asTextOutput): print("Error in id version %d." % i) print("result: %s" % idActual[i]) print("expected: %s" % idExpected[i]) - #print(rootSymbol.dump(0)) + # print(rootSymbol.dump(0)) raise DefinitionError("") @@ -106,7 +106,7 @@ def check(name, input, idDict, output=None, key=None, asTextOutput=None): if name != 'macro': # Second, check with semicolon _check(name, input + ' ;', idDict, output + ';', key, - asTextOutput + ';' if asTextOutput is not None else None) + asTextOutput + ';' if asTextOutput is not None else None) def test_expressions(): @@ -422,7 +422,7 @@ def test_nested_name(): check('function', 'void f(.A.B a)', {1: "f"}) -def test_union_definitions(): +def test_struct_definitions(): check('struct', '{key}A', {1: 'A'}) @@ -482,7 +482,7 @@ def test_attributes(): # style: user-defined paren check('member', 'paren_attr() int f', {1: 'f'}) check('member', 'paren_attr(a) int f', {1: 'f'}) - check('member', 'paren_attr("") int f',{1: 'f'}) + check('member', 'paren_attr("") int f', {1: 'f'}) check('member', 'paren_attr(()[{}][]{}) int f', {1: 'f'}) with pytest.raises(DefinitionError): parse('member', 'paren_attr(() int f') @@ -521,7 +521,7 @@ def test_attributes(): def filter_warnings(warning, file): - lines = warning.getvalue().split("\n"); + lines = warning.getvalue().split("\n") res = [l for l in lines if "domain-c" in l and "{}.rst".format(file) in l and "WARNING: document isn't included in any toctree" not in l] print("Filtered warnings for file '{}':".format(file)) @@ -602,6 +602,7 @@ def _get_obj(app, queryName): return (docname, anchor, objectType) return (queryName, "not", "found") + def test_cfunction(app): text = (".. c:function:: PyObject* " "PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)") diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 2c8a43071..ff2a015ee 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -182,9 +182,9 @@ def test_expressions(): expr = i + l + u exprCheck(expr, 'L' + expr + 'E') decimalFloats = ['5e42', '5e+42', '5e-42', - '5.', '5.e42', '5.e+42', '5.e-42', - '.5', '.5e42', '.5e+42', '.5e-42', - '5.0', '5.0e42', '5.0e+42', '5.0e-42'] + '5.', '5.e42', '5.e+42', '5.e-42', + '.5', '.5e42', '.5e+42', '.5e-42', + '5.0', '5.0e42', '5.0e+42', '5.0e-42'] hexFloats = ['ApF', 'Ap+F', 'Ap-F', 'A.', 'A.pF', 'A.p+F', 'A.p-F', '.A', '.ApF', '.Ap+F', '.Ap-F', @@ -425,9 +425,9 @@ def test_member_definitions(): check('member', 'int b : 8 = 42', {1: 'b__i', 2: '1b'}) check('member', 'int b : 8{42}', {1: 'b__i', 2: '1b'}) # TODO: enable once the ternary operator is supported - #check('member', 'int b : true ? 8 : a = 42', {1: 'b__i', 2: '1b'}) + # check('member', 'int b : true ? 8 : a = 42', {1: 'b__i', 2: '1b'}) # TODO: enable once the ternary operator is supported - #check('member', 'int b : (true ? 8 : a) = 42', {1: 'b__i', 2: '1b'}) + # check('member', 'int b : (true ? 8 : a) = 42', {1: 'b__i', 2: '1b'}) check('member', 'int b : 1 || new int{0}', {1: 'b__i', 2: '1b'}) @@ -537,8 +537,8 @@ def test_function_definitions(): check('function', 'int foo(const A*...)', {1: "foo__ACPDp", 2: "3fooDpPK1A"}) check('function', 'int foo(const int A::*... a)', {2: "3fooDpM1AKi"}) check('function', 'int foo(const int A::*...)', {2: "3fooDpM1AKi"}) - #check('function', 'int foo(int (*a)(A)...)', {1: "foo__ACRDp", 2: "3fooDpPK1A"}) - #check('function', 'int foo(int (*)(A)...)', {1: "foo__ACRDp", 2: "3fooDpPK1A"}) + # check('function', 'int foo(int (*a)(A)...)', {1: "foo__ACRDp", 2: "3fooDpPK1A"}) + # check('function', 'int foo(int (*)(A)...)', {1: "foo__ACRDp", 2: "3fooDpPK1A"}) check('function', 'virtual void f()', {1: "f", 2: "1fv"}) # test for ::nestedName, from issue 1738 check("function", "result(int val, ::std::error_category const &cat)", @@ -707,7 +707,6 @@ def test_class_definitions(): check('class', 'template {key}has_var>', {2: 'I0E7has_varI1TNSt6void_tIDTadN1T3varEEEEE'}) - check('class', 'template {key}T', {2: 'IDpE1TIJPFi2TsEEE'}) check('class', 'template {key}T<(Is)...>', @@ -1001,7 +1000,7 @@ def test_build_domain_cpp_warn_template_param_qualified_name(app, status, warnin @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) -def test_build_domain_cpp_backslash_ok(app, status, warning): +def test_build_domain_cpp_backslash_ok_true(app, status, warning): app.builder.build_all() ws = filter_warnings(warning, "backslash") assert len(ws) == 0 @@ -1016,7 +1015,7 @@ def test_build_domain_cpp_semicolon(app, status, warning): @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True, 'strip_signature_backslash': True}) -def test_build_domain_cpp_backslash_ok(app, status, warning): +def test_build_domain_cpp_backslash_ok_false(app, status, warning): app.builder.build_all() ws = filter_warnings(warning, "backslash") assert len(ws) == 1 @@ -1250,4 +1249,4 @@ def test_mix_decl_duplicate(app, warning): assert "Declaration is '.. cpp:function:: void A()'." in ws[1] assert "index.rst:3: WARNING: Duplicate C++ declaration, also defined at index:1." in ws[2] assert "Declaration is '.. cpp:struct:: A'." in ws[3] - assert ws[4] == "" \ No newline at end of file + assert ws[4] == "" diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py index 92876fa6a..7ee1d6c07 100644 --- a/tests/test_domain_std.py +++ b/tests/test_domain_std.py @@ -333,7 +333,7 @@ def test_multiple_cmdoptions(app): def test_productionlist(app, status, warning): app.builder.build_all() - warnings = warning.getvalue().split("\n"); + warnings = warning.getvalue().split("\n") assert len(warnings) == 2 assert warnings[-1] == '' assert "Dup2.rst:4: WARNING: duplicate token description of Dup, other instance in Dup1" in warnings[0] diff --git a/tests/test_environment_indexentries.py b/tests/test_environment_indexentries.py index c15226d85..075022be5 100644 --- a/tests/test_environment_indexentries.py +++ b/tests/test_environment_indexentries.py @@ -38,8 +38,9 @@ def test_create_single_index(app): ('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])]) + assert index[4] == ('Е', + [('ёлка', [[('', '#index-6')], [], None]), + ('Ель', [[('', '#index-5')], [], None])]) assert index[5] == ('ת', [('‏תירבע‎', [[('', '#index-7')], [], None])]) @@ -69,8 +70,9 @@ def test_create_pair_index(app): ('ёлка', [('', '#index-5')]), ('Ель', [('', '#index-4')])], None])]) - assert index[6] == ('Е', [('ёлка', [[], [('Sphinx', [('', '#index-5')])], None]), - ('Ель', [[], [('Sphinx', [('', '#index-4')])], None])]) + assert index[6] == ('Е', + [('ёлка', [[], [('Sphinx', [('', '#index-5')])], None]), + ('Ель', [[], [('Sphinx', [('', '#index-4')])], None])]) @pytest.mark.sphinx('dummy', freshenv=True) diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 140c9351e..d8e1f730e 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -177,7 +177,6 @@ def test_format_signature(app): for C in (D, E): assert formatsig('class', 'D', C, None, None) == '()' - class SomeMeta(type): def __call__(cls, a, b=None): return type.__call__(cls, a, b) @@ -209,7 +208,6 @@ def test_format_signature(app): assert formatsig('class', 'C', C, None, None) == '(a, b=None)' assert formatsig('class', 'C', D, 'a, b', 'X') == '(a, b) -> X' - class ListSubclass(list): pass @@ -219,7 +217,6 @@ def test_format_signature(app): else: assert formatsig('class', 'C', ListSubclass, None, None) == '' - class ExceptionSubclass(Exception): pass @@ -227,7 +224,6 @@ def test_format_signature(app): if getattr(Exception, '__text_signature__', None) is None: assert formatsig('class', 'C', ExceptionSubclass, None, None) == '' - # __init__ have signature at first line of docstring directive.env.config.autoclass_content = 'both' diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 3d1005ac0..4dff7c3db 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -700,6 +700,19 @@ def test_autodoc_type_aliases(app): '.. py:module:: target.annotations', '', '', + '.. py:class:: Foo()', + ' :module: target.annotations', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr', + ' :module: target.annotations', + ' :type: int', + '', + ' docstring', + '', + '', '.. py:function:: mult(x: int, y: int) -> int', ' mult(x: float, y: float) -> float', ' :module: target.annotations', @@ -712,6 +725,13 @@ def test_autodoc_type_aliases(app): '', ' docstring', '', + '', + '.. py:data:: variable', + ' :module: target.annotations', + ' :type: int', + '', + ' docstring', + '', ] # define aliases @@ -722,6 +742,19 @@ def test_autodoc_type_aliases(app): '.. py:module:: target.annotations', '', '', + '.. py:class:: Foo()', + ' :module: target.annotations', + '', + ' docstring', + '', + '', + ' .. py:attribute:: Foo.attr', + ' :module: target.annotations', + ' :type: myint', + '', + ' docstring', + '', + '', '.. py:function:: mult(x: myint, y: myint) -> myint', ' mult(x: float, y: float) -> float', ' :module: target.annotations', @@ -734,6 +767,13 @@ def test_autodoc_type_aliases(app): '', ' docstring', '', + '', + '.. py:data:: variable', + ' :module: target.annotations', + ' :type: myint', + '', + ' docstring', + '', ] diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index ef621e2b6..63a62aeab 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -11,7 +11,6 @@ import http.server import os import unittest -from io import BytesIO from unittest import mock import pytest diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 0d2f8727e..220a394d4 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -1070,7 +1070,7 @@ Methods: description -""" +""" # NOQA config = Config() actual = str(GoogleDocstring(docstring, config=config, app=None, what='module', options={'noindex': True})) @@ -2222,7 +2222,7 @@ definition_after_normal_text : int ["{", "'F'", ", ", "'C'", ", ", "'N or C'", "}", ", ", "default", " ", "'F'"], ["str", ", ", "default", ": ", "'F or C'"], ["int", ", ", "default", ": ", "None"], - ["int", ", " , "default", " ", "None"], + ["int", ", ", "default", " ", "None"], ["int", ", ", "default", " ", ":obj:`None`"], ['"ma{icious"'], [r"'with \'quotes\''"], diff --git a/tests/test_intl.py b/tests/test_intl.py index de242be72..d96733e4a 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -89,15 +89,6 @@ def assert_count(expected_expr, result, count): assert len(re.findall(*find_pair)) == count, find_pair -@sphinx_intl -@pytest.mark.sphinx('text') -@pytest.mark.test_params(shared_result='test_intl_basic') -def test_text_toctree(app): - app.build() - result = (app.outdir / 'index.txt').read_text() - assert_startswith(result, "CONTENTS\n********\n\nTABLE OF CONTENTS\n") - - @sphinx_intl @pytest.mark.sphinx('text') @pytest.mark.test_params(shared_result='test_intl_basic') @@ -436,11 +427,16 @@ def test_text_admonitions(app): @pytest.mark.test_params(shared_result='test_intl_gettext') def test_gettext_toctree(app): app.build() - # --- toctree + # --- toctree (index.rst) expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'index.po') actual = read_po(app.outdir / 'index.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] + # --- toctree (toctree.rst) + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po') + actual = read_po(app.outdir / 'toctree.pot') + for expect_msg in [m for m in expect if m.id]: + assert expect_msg.id in [m.id for m in actual if m.id] @sphinx_intl @@ -467,24 +463,17 @@ def test_text_table(app): assert expect_msg.string in result -@sphinx_intl -@pytest.mark.sphinx('gettext') -@pytest.mark.test_params(shared_result='test_intl_gettext') -def test_gettext_toctree(app): - app.build() - # --- toctree - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po') - actual = read_po(app.outdir / 'toctree.pot') - for expect_msg in [m for m in expect if m.id]: - assert expect_msg.id in [m.id for m in actual if m.id] - - @sphinx_intl @pytest.mark.sphinx('text') @pytest.mark.test_params(shared_result='test_intl_basic') def test_text_toctree(app): app.build() - # --- toctree + # --- toctree (index.rst) + # Note: index.rst contains contents that is not shown in text. + result = (app.outdir / 'index.txt').read_text() + assert 'CONTENTS' in result + assert 'TABLE OF CONTENTS' in result + # --- toctree (toctree.rst) result = (app.outdir / 'toctree.txt').read_text() expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po') for expect_msg in [m for m in expect if m.id]: diff --git a/tests/test_markup.py b/tests/test_markup.py index e7d855c36..a2bcb2dc1 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -17,7 +17,6 @@ from docutils.parsers.rst import Parser as RstParser from sphinx import addnodes from sphinx.builders.html.transforms import KeyboardTransform from sphinx.builders.latex import LaTeXBuilder -from sphinx.builders.latex.theming import ThemeFactory from sphinx.roles import XRefRole from sphinx.testing.util import Struct, assert_node from sphinx.transforms import SphinxSmartQuotes diff --git a/tests/test_project.py b/tests/test_project.py index 50b06f7b8..906a52bfe 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -13,7 +13,6 @@ from collections import OrderedDict import pytest from sphinx.project import Project -from sphinx.testing.comparer import PathComparer def test_project_discover(rootdir): diff --git a/tests/test_pycode.py b/tests/test_pycode.py index ac3b34c9f..3d69e53cb 100644 --- a/tests/test_pycode.py +++ b/tests/test_pycode.py @@ -19,6 +19,7 @@ from sphinx.pycode import ModuleAnalyzer SPHINX_MODULE_PATH = os.path.splitext(sphinx.__file__)[0] + '.py' + def test_ModuleAnalyzer_get_module_source(): assert ModuleAnalyzer.get_module_source('sphinx') == (sphinx.__file__, sphinx.__loader__.get_source('sphinx')) diff --git a/tests/test_util.py b/tests/test_util.py index c58931bb4..2d03ed89a 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -14,8 +14,7 @@ from unittest.mock import patch import pytest -import sphinx -from sphinx.errors import ExtensionError, PycodeError +from sphinx.errors import ExtensionError from sphinx.testing.util import strip_escseq from sphinx.util import (SkipProgressMessage, display_chunk, encode_uri, ensuredir, import_object, logging, parselinenos, progress_message, diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 7b51cb7d1..9243c030d 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -19,7 +19,7 @@ import _testcapi import pytest from sphinx.util import inspect -from sphinx.util.inspect import is_builtin_class_method, stringify_signature +from sphinx.util.inspect import stringify_signature def test_signature(): @@ -493,6 +493,28 @@ def test_dict_customtype(): assert ": 2" in description +def test_getslots(): + class Foo: + pass + + class Bar: + __slots__ = ['attr'] + + class Baz: + __slots__ = {'attr': 'docstring'} + + class Qux: + __slots__ = 'attr' + + assert inspect.getslots(Foo) is None + assert inspect.getslots(Bar) == {'attr': None} + assert inspect.getslots(Baz) == {'attr': 'docstring'} + assert inspect.getslots(Qux) == {'attr': None} + + with pytest.raises(TypeError): + inspect.getslots(Bar()) + + @pytest.mark.sphinx(testroot='ext-autodoc') def test_isclassmethod(app): from target.methods import Base, Inherited diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 354db1567..73b4aca53 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -10,8 +10,7 @@ import sys from numbers import Integral -from typing import (Any, Callable, Dict, Generator, Generic, List, Optional, Tuple, TypeVar, - Union) +from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, TypeVar, Union import pytest @@ -25,8 +24,10 @@ class MyClass1: class MyClass2(MyClass1): __qualname__ = '' + T = TypeVar('T') + class MyList(List[T]): pass @@ -132,8 +133,8 @@ def test_stringify_type_hints_containers(): @pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') def test_stringify_Annotated(): - from typing import Annotated - assert stringify(Annotated[str, "foo", "bar"]) == "str" + from typing import Annotated # type: ignore + assert stringify(Annotated[str, "foo", "bar"]) == "str" # NOQA def test_stringify_type_hints_string(): diff --git a/tests/typing_test_data.py b/tests/typing_test_data.py index 8b30c843f..c2db7d95b 100644 --- a/tests/typing_test_data.py +++ b/tests/typing_test_data.py @@ -77,7 +77,7 @@ def f14() -> Any: pass -def f15(x: "Unknown", y: "int") -> Any: +def f15(x: "Unknown", y: "int") -> Any: # type: ignore # NOQA pass diff --git a/tests/utils.py b/tests/utils.py index 043d64c06..eb2c40c52 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,11 +1,13 @@ import contextlib import http.server +import os import pathlib import ssl import threading # Generated with: -# $ openssl req -new -x509 -days 3650 -nodes -out cert.pem -keyout cert.pem +# $ openssl req -new -x509 -days 3650 -nodes -out cert.pem \ +# -keyout cert.pem -addext "subjectAltName = DNS:localhost" CERT_FILE = str(pathlib.Path(__file__).parent / "certs" / "cert.pem") @@ -46,3 +48,18 @@ def create_server(thread_class): http_server = create_server(HttpServerThread) https_server = create_server(HttpsServerThread) + + +@contextlib.contextmanager +def modify_env(**env): + original_env = os.environ.copy() + for k, v in env.items(): + os.environ[k] = v + try: + yield + finally: + for k in env: + try: + os.environ[k] = original_env[k] + except KeyError: + os.unsetenv(k)