|
2 | 2 | """ |
3 | 3 | Sphinx documentation for Salt |
4 | 4 | """ |
5 | | -import logging |
6 | 5 | import os |
7 | 6 | import sys |
8 | 7 | import urllib.parse |
9 | | -import warnings |
10 | 8 |
|
11 | | -# Suppress SyntaxWarnings (invalid escape sequences) which become errors in CI with -W |
12 | | -# We fix these as we find them, but this prevents "flickering" crashes in large-scale builds. |
13 | | -warnings.filterwarnings("ignore", category=SyntaxWarning) |
14 | | - |
15 | | -# Fix urllib.parse.urlsplit/urlparse bug (Invalid IPv6 URL) |
16 | | -# This bug causes Sphinx to crash when encountering documentation examples |
17 | | -# like http://hostname[:port] which are incorrectly parsed as malformed IPv6 URLs. |
18 | | -# We patch it globally to ensure all extensions (like pydata-sphinx-theme) are safe. |
| 9 | +# Sphinx crashes parsing docstring URLs like ``http://hostname[:port]`` because |
| 10 | +# urllib.parse.urlsplit raises ValueError("Invalid IPv6 URL") on the bracketed |
| 11 | +# host placeholder. Return a dummy SplitResult for that one case so the build |
| 12 | +# can proceed. |
19 | 13 | _original_urlsplit = urllib.parse.urlsplit |
20 | | -_original_urlparse = urllib.parse.urlparse |
21 | | -log = logging.getLogger("salt.docs.conf") |
22 | 14 |
|
23 | 15 |
|
24 | 16 | def _safe_urlsplit(url, scheme="", allow_fragments=True): |
25 | 17 | try: |
26 | 18 | return _original_urlsplit(url, scheme, allow_fragments) |
27 | | - except Exception: |
28 | | - # Return a safe dummy SplitResult for ANY error |
29 | | - return urllib.parse.SplitResult(scheme, "invalid-url", str(url), "", "") |
30 | | - |
31 | | - |
32 | | -def _safe_urlparse(url, scheme="", allow_fragments=True): |
33 | | - try: |
34 | | - return _original_urlparse(url, scheme, allow_fragments) |
35 | | - except Exception: |
36 | | - # Return a safe dummy ParseResult for ANY error |
37 | | - return urllib.parse.ParseResult(scheme, "invalid-url", str(url), "", "", "") |
| 19 | + except ValueError as exc: |
| 20 | + if "Invalid IPv6 URL" in str(exc): |
| 21 | + return urllib.parse.SplitResult(scheme, "", url, "", "") |
| 22 | + raise |
38 | 23 |
|
39 | 24 |
|
40 | 25 | urllib.parse.urlsplit = _safe_urlsplit |
41 | | -urllib.parse.urlparse = _safe_urlparse |
42 | | - |
43 | | -# Ensure any modules that already imported urlsplit or urlparse also use the patched versions. |
44 | | -for mod in list(sys.modules.values()): |
45 | | - if mod: |
46 | | - if hasattr(mod, "urlsplit") and mod.urlsplit is _original_urlsplit: |
47 | | - mod.urlsplit = _safe_urlsplit |
48 | | - if hasattr(mod, "urlparse") and mod.urlparse is _original_urlparse: |
49 | | - mod.urlparse = _safe_urlparse |
50 | | - |
51 | | -# Force disable pydata_sphinx_theme link shortener as it's prone to crashing on Salt docs |
52 | | -try: |
53 | | - import pydata_sphinx_theme.short_link |
54 | | - |
55 | | - def _no_op_run(self, **kwargs): |
56 | | - pass |
57 | | - |
58 | | - pydata_sphinx_theme.short_link.ShortenLinkTransform.run = _no_op_run |
59 | | -except ImportError: |
60 | | - pass |
61 | | - |
62 | | -# Manual mock for 'cgi' module which was removed in Python 3.13 |
63 | | -# This is required because some Salt modules import it at the top level, |
64 | | -# and autodoc_mock_imports is sometimes too late or insufficient. |
65 | | -import unittest.mock |
66 | | - |
67 | | -sys.modules["cgi"] = unittest.mock.MagicMock() |
68 | | - |
69 | | - |
70 | | -# Patch urllib3 if it's available, as it has its own unsafe URL parsing |
71 | | -def _patch_urllib3(module): |
72 | | - try: |
73 | | - _original_parse_url = module.util.url.parse_url |
74 | | - |
75 | | - def _safe_parse_url(url): |
76 | | - try: |
77 | | - return _original_parse_url(url) |
78 | | - except (ValueError, AttributeError): |
79 | | - log.debug("urllib3 detected malformed URL: %s", url) |
80 | | - # Return a safe dummy Url object |
81 | | - return module.util.url.Url(path=url) |
82 | | - |
83 | | - module.util.url.parse_url = _safe_parse_url |
84 | | - except (ImportError, AttributeError): |
85 | | - pass |
86 | | - |
87 | | - |
88 | | -try: |
89 | | - import urllib3 |
90 | | - |
91 | | - _patch_urllib3(urllib3) |
92 | | -except ImportError: |
93 | | - pass |
94 | | - |
95 | | -try: |
96 | | - import requests.packages.urllib3 as requests_urllib3 |
97 | | - |
98 | | - _patch_urllib3(requests_urllib3) |
99 | | -except (ImportError, AttributeError): |
100 | | - pass |
101 | 26 |
|
102 | 27 | import pathlib |
103 | 28 | import re |
@@ -310,11 +235,11 @@ def _safe_parse_url(url): |
310 | 235 | ", ".join(_missing_deps), |
311 | 236 | ) |
312 | 237 | else: |
313 | | - # For HTML/full builds, log info that docs may be incomplete |
314 | | - log.info( |
| 238 | + # For HTML/full builds, warn that docs may be incomplete |
| 239 | + log.warning( |
315 | 240 | "\n" |
316 | 241 | "=" * 70 + "\n" |
317 | | - "INFO: Missing optional dependencies for full documentation build:\n" |
| 242 | + "WARNING: Missing dependencies for full documentation build:\n" |
318 | 243 | " %s\n\n" |
319 | 244 | "Autodoc will use mocked modules. Documentation will be generated but\n" |
320 | 245 | "may be incomplete or show incorrect type hints.\n\n" |
@@ -569,15 +494,11 @@ class ReleasesTree(TocTree): |
569 | 494 | option_spec = dict(TocTree.option_spec) |
570 | 495 |
|
571 | 496 | def run(self): |
572 | | - try: |
573 | | - rst = super().run() |
574 | | - if rst and rst[0] and rst[0][0] and "entries" in rst[0][0]: |
575 | | - entries = rst[0][0]["entries"][:] |
576 | | - entries.sort(key=_normalize_version, reverse=True) |
577 | | - rst[0][0]["entries"][:] = entries |
578 | | - return rst |
579 | | - except (AttributeError, KeyError, IndexError, TypeError): |
580 | | - return super().run() |
| 497 | + rst = super().run() |
| 498 | + entries = rst[0][0]["entries"][:] |
| 499 | + entries.sort(key=_normalize_version, reverse=True) |
| 500 | + rst[0][0]["entries"][:] = entries |
| 501 | + return rst |
581 | 502 |
|
582 | 503 |
|
583 | 504 | def copy_release_templates_pre(app): |
|
0 commit comments