|
25 | 25 | from sphinx.util.typing import Inventory |
26 | 26 |
|
27 | 27 |
|
28 | | -def _strip_basic_auth(url: str) -> str: |
29 | | - """Returns *url* with basic auth credentials removed. Also returns the |
30 | | - basic auth username and password if they're present in *url*. |
31 | | -
|
32 | | - E.g.: https://user:[email protected] => https://example.com |
33 | | -
|
34 | | - *url* need not include basic auth credentials. |
35 | | -
|
36 | | - :param url: url which may or may not contain basic auth credentials |
37 | | - :type url: ``str`` |
38 | | -
|
39 | | - :return: *url* with any basic auth creds removed |
40 | | - :rtype: ``str`` |
41 | | - """ |
42 | | - frags = list(urlsplit(url)) |
43 | | - # swap out "user[:pass]@hostname" for "hostname" |
44 | | - if '@' in frags[1]: |
45 | | - frags[1] = frags[1].split('@')[1] |
46 | | - return urlunsplit(frags) |
47 | | - |
48 | | - |
49 | | -def _read_from_url(url: str, *, config: Config) -> IO: |
50 | | - """Reads data from *url* with an HTTP *GET*. |
51 | | -
|
52 | | - This function supports fetching from resources which use basic HTTP auth as |
53 | | - laid out by RFC1738 § 3.1. See § 5 for grammar definitions for URLs. |
54 | | -
|
55 | | - .. seealso: |
| 28 | +def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None: |
| 29 | + for key, value in config.intersphinx_mapping.copy().items(): |
| 30 | + try: |
| 31 | + if isinstance(value, (list, tuple)): |
| 32 | + # new format |
| 33 | + name, (uri, inv) = key, value |
| 34 | + if not isinstance(name, str): |
| 35 | + LOGGER.warning(__('intersphinx identifier %r is not string. Ignored'), |
| 36 | + name) |
| 37 | + config.intersphinx_mapping.pop(key) |
| 38 | + continue |
| 39 | + else: |
| 40 | + # old format, no name |
| 41 | + # xref RemovedInSphinx80Warning |
| 42 | + name, uri, inv = None, key, value |
| 43 | + msg = ( |
| 44 | + "The pre-Sphinx 1.0 'intersphinx_mapping' format is " |
| 45 | + "deprecated and will be removed in Sphinx 8. Update to the " |
| 46 | + "current format as described in the documentation. " |
| 47 | + f"Hint: \"intersphinx_mapping = {{'<name>': {(uri, inv)!r}}}\"." |
| 48 | + "https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping" # NoQA: E501 |
| 49 | + ) |
| 50 | + LOGGER.warning(msg) |
56 | 51 |
|
57 | | - https://www.ietf.org/rfc/rfc1738.txt |
| 52 | + if not isinstance(inv, tuple): |
| 53 | + config.intersphinx_mapping[key] = (name, (uri, (inv,))) |
| 54 | + else: |
| 55 | + config.intersphinx_mapping[key] = (name, (uri, inv)) |
| 56 | + except Exception as exc: |
| 57 | + LOGGER.warning(__('Failed to read intersphinx_mapping[%s], ignored: %r'), key, exc) |
| 58 | + config.intersphinx_mapping.pop(key) |
58 | 59 |
|
59 | | - :param url: URL of an HTTP resource |
60 | | - :type url: ``str`` |
61 | 60 |
|
62 | | - :return: data read from resource described by *url* |
63 | | - :rtype: ``file``-like object |
64 | | - """ |
65 | | - r = requests.get(url, stream=True, timeout=config.intersphinx_timeout, |
66 | | - _user_agent=config.user_agent, |
67 | | - _tls_info=(config.tls_verify, config.tls_cacerts)) |
68 | | - r.raise_for_status() |
69 | | - r.raw.url = r.url |
70 | | - # decode content-body based on the header. |
71 | | - # ref: https://github.com/psf/requests/issues/2155 |
72 | | - r.raw.read = functools.partial(r.raw.read, decode_content=True) |
73 | | - return r.raw |
| 61 | +def load_mappings(app: Sphinx) -> None: |
| 62 | + """Load all intersphinx mappings into the environment.""" |
| 63 | + now = int(time.time()) |
| 64 | + inventories = InventoryAdapter(app.builder.env) |
| 65 | + intersphinx_cache: dict[str, InventoryCacheEntry] = inventories.cache |
74 | 66 |
|
| 67 | + with concurrent.futures.ThreadPoolExecutor() as pool: |
| 68 | + futures = [] |
| 69 | + name: str | None |
| 70 | + uri: str |
| 71 | + invs: tuple[str | None, ...] |
| 72 | + for name, (uri, invs) in app.config.intersphinx_mapping.values(): |
| 73 | + futures.append(pool.submit( |
| 74 | + fetch_inventory_group, name, uri, invs, intersphinx_cache, app, now, |
| 75 | + )) |
| 76 | + updated = [f.result() for f in concurrent.futures.as_completed(futures)] |
75 | 77 |
|
76 | | -def _get_safe_url(url: str) -> str: |
77 | | - """Gets version of *url* with basic auth passwords obscured. This function |
78 | | - returns results suitable for printing and logging. |
| 78 | + if any(updated): |
| 79 | + inventories.clear() |
79 | 80 |
|
80 | | - |
| 81 | + # Duplicate values in different inventories will shadow each |
| 82 | + # other; which one will override which can vary between builds |
| 83 | + # since they are specified using an unordered dict. To make |
| 84 | + # it more consistent, we sort the named inventories and then |
| 85 | + # add the unnamed inventories last. This means that the |
| 86 | + # unnamed inventories will shadow the named ones but the named |
| 87 | + # ones can still be accessed when the name is specified. |
| 88 | + named_vals = [] |
| 89 | + unnamed_vals = [] |
| 90 | + for name, _expiry, invdata in intersphinx_cache.values(): |
| 91 | + if name: |
| 92 | + named_vals.append((name, invdata)) |
| 93 | + else: |
| 94 | + unnamed_vals.append((name, invdata)) |
| 95 | + for name, invdata in sorted(named_vals) + unnamed_vals: |
| 96 | + if name: |
| 97 | + inventories.named_inventory[name] = invdata |
| 98 | + for type, objects in invdata.items(): |
| 99 | + inventories.main_inventory.setdefault(type, {}).update(objects) |
81 | 100 |
|
82 | | - :param url: a url |
83 | | - :type url: ``str`` |
84 | 101 |
|
85 | | - :return: *url* with password removed |
86 | | - :rtype: ``str`` |
87 | | - """ |
88 | | - parts = urlsplit(url) |
89 | | - if parts.username is None: |
90 | | - return url |
91 | | - else: |
92 | | - frags = list(parts) |
93 | | - if parts.port: |
94 | | - frags[1] = f'{parts.username}@{parts.hostname}:{parts.port}' |
| 102 | +def fetch_inventory_group( |
| 103 | + name: str | None, |
| 104 | + uri: str, |
| 105 | + invs: tuple[str | None, ...], |
| 106 | + cache: dict[str, InventoryCacheEntry], |
| 107 | + app: Sphinx, |
| 108 | + now: int, |
| 109 | +) -> bool: |
| 110 | + cache_time = now - app.config.intersphinx_cache_limit * 86400 |
| 111 | + failures = [] |
| 112 | + try: |
| 113 | + for inv in invs: |
| 114 | + if not inv: |
| 115 | + inv = posixpath.join(uri, INVENTORY_FILENAME) |
| 116 | + # decide whether the inventory must be read: always read local |
| 117 | + # files; remote ones only if the cache time is expired |
| 118 | + if '://' not in inv or uri not in cache or cache[uri][1] < cache_time: |
| 119 | + safe_inv_url = _get_safe_url(inv) |
| 120 | + LOGGER.info(__('loading intersphinx inventory from %s...'), safe_inv_url) |
| 121 | + try: |
| 122 | + invdata = fetch_inventory(app, uri, inv) |
| 123 | + except Exception as err: |
| 124 | + failures.append(err.args) |
| 125 | + continue |
| 126 | + if invdata: |
| 127 | + cache[uri] = name, now, invdata |
| 128 | + return True |
| 129 | + return False |
| 130 | + finally: |
| 131 | + if failures == []: |
| 132 | + pass |
| 133 | + elif len(failures) < len(invs): |
| 134 | + LOGGER.info(__("encountered some issues with some of the inventories," |
| 135 | + " but they had working alternatives:")) |
| 136 | + for fail in failures: |
| 137 | + LOGGER.info(*fail) |
95 | 138 | else: |
96 | | - frags[1] = f'{parts.username}@{parts.hostname}' |
97 | | - |
98 | | - return urlunsplit(frags) |
| 139 | + issues = '\n'.join(f[0] % f[1:] for f in failures) |
| 140 | + LOGGER.warning(__("failed to reach any of the inventories " |
| 141 | + "with the following issues:") + "\n" + issues) |
99 | 142 |
|
100 | 143 |
|
101 | 144 | def fetch_inventory(app: Sphinx, uri: str, inv: str) -> Inventory: |
@@ -135,117 +178,74 @@ def fetch_inventory(app: Sphinx, uri: str, inv: str) -> Inventory: |
135 | 178 | return invdata |
136 | 179 |
|
137 | 180 |
|
138 | | -def fetch_inventory_group( |
139 | | - name: str | None, |
140 | | - uri: str, |
141 | | - invs: tuple[str | None, ...], |
142 | | - cache: dict[str, InventoryCacheEntry], |
143 | | - app: Sphinx, |
144 | | - now: int, |
145 | | -) -> bool: |
146 | | - cache_time = now - app.config.intersphinx_cache_limit * 86400 |
147 | | - failures = [] |
148 | | - try: |
149 | | - for inv in invs: |
150 | | - if not inv: |
151 | | - inv = posixpath.join(uri, INVENTORY_FILENAME) |
152 | | - # decide whether the inventory must be read: always read local |
153 | | - # files; remote ones only if the cache time is expired |
154 | | - if '://' not in inv or uri not in cache or cache[uri][1] < cache_time: |
155 | | - safe_inv_url = _get_safe_url(inv) |
156 | | - LOGGER.info(__('loading intersphinx inventory from %s...'), safe_inv_url) |
157 | | - try: |
158 | | - invdata = fetch_inventory(app, uri, inv) |
159 | | - except Exception as err: |
160 | | - failures.append(err.args) |
161 | | - continue |
162 | | - if invdata: |
163 | | - cache[uri] = name, now, invdata |
164 | | - return True |
165 | | - return False |
166 | | - finally: |
167 | | - if failures == []: |
168 | | - pass |
169 | | - elif len(failures) < len(invs): |
170 | | - LOGGER.info(__("encountered some issues with some of the inventories," |
171 | | - " but they had working alternatives:")) |
172 | | - for fail in failures: |
173 | | - LOGGER.info(*fail) |
| 181 | +def _get_safe_url(url: str) -> str: |
| 182 | + """Gets version of *url* with basic auth passwords obscured. This function |
| 183 | + returns results suitable for printing and logging. |
| 184 | +
|
| 185 | + |
| 186 | +
|
| 187 | + :param url: a url |
| 188 | + :type url: ``str`` |
| 189 | +
|
| 190 | + :return: *url* with password removed |
| 191 | + :rtype: ``str`` |
| 192 | + """ |
| 193 | + parts = urlsplit(url) |
| 194 | + if parts.username is None: |
| 195 | + return url |
| 196 | + else: |
| 197 | + frags = list(parts) |
| 198 | + if parts.port: |
| 199 | + frags[1] = f'{parts.username}@{parts.hostname}:{parts.port}' |
174 | 200 | else: |
175 | | - issues = '\n'.join(f[0] % f[1:] for f in failures) |
176 | | - LOGGER.warning(__("failed to reach any of the inventories " |
177 | | - "with the following issues:") + "\n" + issues) |
| 201 | + frags[1] = f'{parts.username}@{parts.hostname}' |
178 | 202 |
|
| 203 | + return urlunsplit(frags) |
179 | 204 |
|
180 | | -def load_mappings(app: Sphinx) -> None: |
181 | | - """Load all intersphinx mappings into the environment.""" |
182 | | - now = int(time.time()) |
183 | | - inventories = InventoryAdapter(app.builder.env) |
184 | | - intersphinx_cache: dict[str, InventoryCacheEntry] = inventories.cache |
185 | 205 |
|
186 | | - with concurrent.futures.ThreadPoolExecutor() as pool: |
187 | | - futures = [] |
188 | | - name: str | None |
189 | | - uri: str |
190 | | - invs: tuple[str | None, ...] |
191 | | - for name, (uri, invs) in app.config.intersphinx_mapping.values(): |
192 | | - futures.append(pool.submit( |
193 | | - fetch_inventory_group, name, uri, invs, intersphinx_cache, app, now, |
194 | | - )) |
195 | | - updated = [f.result() for f in concurrent.futures.as_completed(futures)] |
| 206 | +def _strip_basic_auth(url: str) -> str: |
| 207 | + """Returns *url* with basic auth credentials removed. Also returns the |
| 208 | + basic auth username and password if they're present in *url*. |
196 | 209 |
|
197 | | - if any(updated): |
198 | | - inventories.clear() |
| 210 | + E.g.: https://user:[email protected] => https://example.com |
199 | 211 |
|
200 | | - # Duplicate values in different inventories will shadow each |
201 | | - # other; which one will override which can vary between builds |
202 | | - # since they are specified using an unordered dict. To make |
203 | | - # it more consistent, we sort the named inventories and then |
204 | | - # add the unnamed inventories last. This means that the |
205 | | - # unnamed inventories will shadow the named ones but the named |
206 | | - # ones can still be accessed when the name is specified. |
207 | | - named_vals = [] |
208 | | - unnamed_vals = [] |
209 | | - for name, _expiry, invdata in intersphinx_cache.values(): |
210 | | - if name: |
211 | | - named_vals.append((name, invdata)) |
212 | | - else: |
213 | | - unnamed_vals.append((name, invdata)) |
214 | | - for name, invdata in sorted(named_vals) + unnamed_vals: |
215 | | - if name: |
216 | | - inventories.named_inventory[name] = invdata |
217 | | - for type, objects in invdata.items(): |
218 | | - inventories.main_inventory.setdefault(type, {}).update(objects) |
| 212 | + *url* need not include basic auth credentials. |
219 | 213 |
|
| 214 | + :param url: url which may or may not contain basic auth credentials |
| 215 | + :type url: ``str`` |
220 | 216 |
|
221 | | -def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None: |
222 | | - for key, value in config.intersphinx_mapping.copy().items(): |
223 | | - try: |
224 | | - if isinstance(value, (list, tuple)): |
225 | | - # new format |
226 | | - name, (uri, inv) = key, value |
227 | | - if not isinstance(name, str): |
228 | | - LOGGER.warning(__('intersphinx identifier %r is not string. Ignored'), |
229 | | - name) |
230 | | - config.intersphinx_mapping.pop(key) |
231 | | - continue |
232 | | - else: |
233 | | - # old format, no name |
234 | | - # xref RemovedInSphinx80Warning |
235 | | - name, uri, inv = None, key, value |
236 | | - msg = ( |
237 | | - "The pre-Sphinx 1.0 'intersphinx_mapping' format is " |
238 | | - "deprecated and will be removed in Sphinx 8. Update to the " |
239 | | - "current format as described in the documentation. " |
240 | | - f"Hint: \"intersphinx_mapping = {{'<name>': {(uri, inv)!r}}}\"." |
241 | | - "https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping" # NoQA: E501 |
242 | | - ) |
243 | | - LOGGER.warning(msg) |
| 217 | + :return: *url* with any basic auth creds removed |
| 218 | + :rtype: ``str`` |
| 219 | + """ |
| 220 | + frags = list(urlsplit(url)) |
| 221 | + # swap out "user[:pass]@hostname" for "hostname" |
| 222 | + if '@' in frags[1]: |
| 223 | + frags[1] = frags[1].split('@')[1] |
| 224 | + return urlunsplit(frags) |
244 | 225 |
|
245 | | - if not isinstance(inv, tuple): |
246 | | - config.intersphinx_mapping[key] = (name, (uri, (inv,))) |
247 | | - else: |
248 | | - config.intersphinx_mapping[key] = (name, (uri, inv)) |
249 | | - except Exception as exc: |
250 | | - LOGGER.warning(__('Failed to read intersphinx_mapping[%s], ignored: %r'), key, exc) |
251 | | - config.intersphinx_mapping.pop(key) |
| 226 | + |
| 227 | +def _read_from_url(url: str, *, config: Config) -> IO: |
| 228 | + """Reads data from *url* with an HTTP *GET*. |
| 229 | +
|
| 230 | + This function supports fetching from resources which use basic HTTP auth as |
| 231 | + laid out by RFC1738 § 3.1. See § 5 for grammar definitions for URLs. |
| 232 | +
|
| 233 | + .. seealso: |
| 234 | +
|
| 235 | + https://www.ietf.org/rfc/rfc1738.txt |
| 236 | +
|
| 237 | + :param url: URL of an HTTP resource |
| 238 | + :type url: ``str`` |
| 239 | +
|
| 240 | + :return: data read from resource described by *url* |
| 241 | + :rtype: ``file``-like object |
| 242 | + """ |
| 243 | + r = requests.get(url, stream=True, timeout=config.intersphinx_timeout, |
| 244 | + _user_agent=config.user_agent, |
| 245 | + _tls_info=(config.tls_verify, config.tls_cacerts)) |
| 246 | + r.raise_for_status() |
| 247 | + r.raw.url = r.url |
| 248 | + # decode content-body based on the header. |
| 249 | + # ref: https://github.com/psf/requests/issues/2155 |
| 250 | + r.raw.read = functools.partial(r.raw.read, decode_content=True) |
| 251 | + return r.raw |
0 commit comments