|
29 | 29 | import sys
|
30 | 30 | import time
|
31 | 31 | from os import path
|
32 |
| -from typing import IO, Any, Dict, List, Tuple |
| 32 | +from typing import IO, Any, Dict, List, Optional, Tuple |
33 | 33 | from urllib.parse import urlsplit, urlunsplit
|
34 | 34 |
|
35 | 35 | from docutils import nodes
|
36 |
| -from docutils.nodes import TextElement |
| 36 | +from docutils.nodes import Element, TextElement |
37 | 37 | from docutils.utils import relative_path
|
38 | 38 |
|
39 | 39 | import sphinx
|
40 | 40 | from sphinx.addnodes import pending_xref
|
41 | 41 | from sphinx.application import Sphinx
|
42 | 42 | from sphinx.builders.html import INVENTORY_FILENAME
|
43 | 43 | from sphinx.config import Config
|
| 44 | +from sphinx.domains import Domain |
44 | 45 | from sphinx.environment import BuildEnvironment
|
45 | 46 | from sphinx.locale import _, __
|
46 | 47 | from sphinx.util import logging, requests
|
47 | 48 | from sphinx.util.inventory import InventoryFile
|
48 |
| -from sphinx.util.typing import Inventory |
| 49 | +from sphinx.util.typing import Inventory, InventoryItem |
49 | 50 |
|
50 | 51 | logger = logging.getLogger(__name__)
|
51 | 52 |
|
@@ -258,105 +259,211 @@ def load_mappings(app: Sphinx) -> None:
|
258 | 259 | inventories.main_inventory.setdefault(type, {}).update(objects)
|
259 | 260 |
|
260 | 261 |
|
261 |
| -def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, |
262 |
| - contnode: TextElement) -> nodes.reference: |
263 |
| - """Attempt to resolve a missing reference via intersphinx references.""" |
264 |
| - target = node['reftarget'] |
265 |
| - inventories = InventoryAdapter(env) |
266 |
| - objtypes: List[str] = None |
267 |
| - if node['reftype'] == 'any': |
268 |
| - # we search anything! |
269 |
| - objtypes = ['%s:%s' % (domain.name, objtype) |
270 |
| - for domain in env.domains.values() |
271 |
| - for objtype in domain.object_types] |
272 |
| - domain = None |
| 262 | +def _create_element_from_result(domain: Domain, inv_name: Optional[str], |
| 263 | + data: InventoryItem, |
| 264 | + node: pending_xref, contnode: TextElement) -> Element: |
| 265 | + proj, version, uri, dispname = data |
| 266 | + if '://' not in uri and node.get('refdoc'): |
| 267 | + # get correct path in case of subdirectories |
| 268 | + uri = path.join(relative_path(node['refdoc'], '.'), uri) |
| 269 | + if version: |
| 270 | + reftitle = _('(in %s v%s)') % (proj, version) |
273 | 271 | else:
|
274 |
| - domain = node.get('refdomain') |
275 |
| - if not domain: |
| 272 | + reftitle = _('(in %s)') % (proj,) |
| 273 | + newnode = nodes.reference('', '', internal=False, refuri=uri, reftitle=reftitle) |
| 274 | + if node.get('refexplicit'): |
| 275 | + # use whatever title was given |
| 276 | + newnode.append(contnode) |
| 277 | + elif dispname == '-' or \ |
| 278 | + (domain.name == 'std' and node['reftype'] == 'keyword'): |
| 279 | + # use whatever title was given, but strip prefix |
| 280 | + title = contnode.astext() |
| 281 | + if inv_name is not None and title.startswith(inv_name + ':'): |
| 282 | + newnode.append(contnode.__class__(title[len(inv_name) + 1:], |
| 283 | + title[len(inv_name) + 1:])) |
| 284 | + else: |
| 285 | + newnode.append(contnode) |
| 286 | + else: |
| 287 | + # else use the given display name (used for :ref:) |
| 288 | + newnode.append(contnode.__class__(dispname, dispname)) |
| 289 | + return newnode |
| 290 | + |
| 291 | + |
| 292 | +def _resolve_reference_in_domain_by_target( |
| 293 | + inv_name: Optional[str], inventory: Inventory, |
| 294 | + domain: Domain, objtypes: List[str], |
| 295 | + target: str, |
| 296 | + node: pending_xref, contnode: TextElement) -> Optional[Element]: |
| 297 | + for objtype in objtypes: |
| 298 | + if objtype not in inventory: |
| 299 | + # Continue if there's nothing of this kind in the inventory |
| 300 | + continue |
| 301 | + |
| 302 | + if target in inventory[objtype]: |
| 303 | + # Case sensitive match, use it |
| 304 | + data = inventory[objtype][target] |
| 305 | + elif objtype == 'std:term': |
| 306 | + # Check for potential case insensitive matches for terms only |
| 307 | + target_lower = target.lower() |
| 308 | + insensitive_matches = list(filter(lambda k: k.lower() == target_lower, |
| 309 | + inventory[objtype].keys())) |
| 310 | + if insensitive_matches: |
| 311 | + data = inventory[objtype][insensitive_matches[0]] |
| 312 | + else: |
| 313 | + # No case insensitive match either, continue to the next candidate |
| 314 | + continue |
| 315 | + else: |
| 316 | + # Could reach here if we're not a term but have a case insensitive match. |
| 317 | + # This is a fix for terms specifically, but potentially should apply to |
| 318 | + # other types. |
| 319 | + continue |
| 320 | + return _create_element_from_result(domain, inv_name, data, node, contnode) |
| 321 | + return None |
| 322 | + |
| 323 | + |
| 324 | +def _resolve_reference_in_domain(env: BuildEnvironment, |
| 325 | + inv_name: Optional[str], inventory: Inventory, |
| 326 | + honor_disabled_refs: bool, |
| 327 | + domain: Domain, objtypes: List[str], |
| 328 | + node: pending_xref, contnode: TextElement |
| 329 | + ) -> Optional[Element]: |
| 330 | + # we adjust the object types for backwards compatibility |
| 331 | + if domain.name == 'std' and 'cmdoption' in objtypes: |
| 332 | + # until Sphinx-1.6, cmdoptions are stored as std:option |
| 333 | + objtypes.append('option') |
| 334 | + if domain.name == 'py' and 'attribute' in objtypes: |
| 335 | + # Since Sphinx-2.1, properties are stored as py:method |
| 336 | + objtypes.append('method') |
| 337 | + |
| 338 | + # the inventory contains domain:type as objtype |
| 339 | + objtypes = ["{}:{}".format(domain.name, t) for t in objtypes] |
| 340 | + |
| 341 | + # now that the objtypes list is complete we can remove the disabled ones |
| 342 | + if honor_disabled_refs: |
| 343 | + disabled = env.config.intersphinx_disabled_reftypes |
| 344 | + objtypes = [o for o in objtypes if o not in disabled] |
| 345 | + |
| 346 | + # without qualification |
| 347 | + res = _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes, |
| 348 | + node['reftarget'], node, contnode) |
| 349 | + if res is not None: |
| 350 | + return res |
| 351 | + |
| 352 | + # try with qualification of the current scope instead |
| 353 | + full_qualified_name = domain.get_full_qualified_name(node) |
| 354 | + if full_qualified_name is None: |
| 355 | + return None |
| 356 | + return _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes, |
| 357 | + full_qualified_name, node, contnode) |
| 358 | + |
| 359 | + |
| 360 | +def _resolve_reference(env: BuildEnvironment, inv_name: Optional[str], inventory: Inventory, |
| 361 | + honor_disabled_refs: bool, |
| 362 | + node: pending_xref, contnode: TextElement) -> Optional[Element]: |
| 363 | + # disabling should only be done if no inventory is given |
| 364 | + honor_disabled_refs = honor_disabled_refs and inv_name is None |
| 365 | + |
| 366 | + if honor_disabled_refs and '*' in env.config.intersphinx_disabled_reftypes: |
| 367 | + return None |
| 368 | + |
| 369 | + typ = node['reftype'] |
| 370 | + if typ == 'any': |
| 371 | + for domain_name, domain in env.domains.items(): |
| 372 | + if honor_disabled_refs \ |
| 373 | + and (domain_name + ":*") in env.config.intersphinx_disabled_reftypes: |
| 374 | + continue |
| 375 | + objtypes = list(domain.object_types) |
| 376 | + res = _resolve_reference_in_domain(env, inv_name, inventory, |
| 377 | + honor_disabled_refs, |
| 378 | + domain, objtypes, |
| 379 | + node, contnode) |
| 380 | + if res is not None: |
| 381 | + return res |
| 382 | + return None |
| 383 | + else: |
| 384 | + domain_name = node.get('refdomain') |
| 385 | + if not domain_name: |
276 | 386 | # only objects in domains are in the inventory
|
277 | 387 | return None
|
278 |
| - objtypes = env.get_domain(domain).objtypes_for_role(node['reftype']) |
| 388 | + if honor_disabled_refs \ |
| 389 | + and (domain_name + ":*") in env.config.intersphinx_disabled_reftypes: |
| 390 | + return None |
| 391 | + domain = env.get_domain(domain_name) |
| 392 | + objtypes = domain.objtypes_for_role(typ) |
279 | 393 | if not objtypes:
|
280 | 394 | return None
|
281 |
| - objtypes = ['%s:%s' % (domain, objtype) for objtype in objtypes] |
282 |
| - if 'std:cmdoption' in objtypes: |
283 |
| - # until Sphinx-1.6, cmdoptions are stored as std:option |
284 |
| - objtypes.append('std:option') |
285 |
| - if 'py:attribute' in objtypes: |
286 |
| - # Since Sphinx-2.1, properties are stored as py:method |
287 |
| - objtypes.append('py:method') |
288 |
| - |
289 |
| - to_try = [(inventories.main_inventory, target)] |
290 |
| - if domain: |
291 |
| - full_qualified_name = env.get_domain(domain).get_full_qualified_name(node) |
292 |
| - if full_qualified_name: |
293 |
| - to_try.append((inventories.main_inventory, full_qualified_name)) |
294 |
| - in_set = None |
295 |
| - if ':' in target: |
296 |
| - # first part may be the foreign doc set name |
297 |
| - setname, newtarget = target.split(':', 1) |
298 |
| - if setname in inventories.named_inventory: |
299 |
| - in_set = setname |
300 |
| - to_try.append((inventories.named_inventory[setname], newtarget)) |
301 |
| - if domain: |
302 |
| - node['reftarget'] = newtarget |
303 |
| - full_qualified_name = env.get_domain(domain).get_full_qualified_name(node) |
304 |
| - if full_qualified_name: |
305 |
| - to_try.append((inventories.named_inventory[setname], full_qualified_name)) |
306 |
| - for inventory, target in to_try: |
307 |
| - for objtype in objtypes: |
308 |
| - if objtype not in inventory: |
309 |
| - # Continue if there's nothing of this kind in the inventory |
310 |
| - continue |
311 |
| - if target in inventory[objtype]: |
312 |
| - # Case sensitive match, use it |
313 |
| - proj, version, uri, dispname = inventory[objtype][target] |
314 |
| - elif objtype == 'std:term': |
315 |
| - # Check for potential case insensitive matches for terms only |
316 |
| - target_lower = target.lower() |
317 |
| - insensitive_matches = list(filter(lambda k: k.lower() == target_lower, |
318 |
| - inventory[objtype].keys())) |
319 |
| - if insensitive_matches: |
320 |
| - proj, version, uri, dispname = inventory[objtype][insensitive_matches[0]] |
321 |
| - else: |
322 |
| - # No case insensitive match either, continue to the next candidate |
323 |
| - continue |
324 |
| - else: |
325 |
| - # Could reach here if we're not a term but have a case insensitive match. |
326 |
| - # This is a fix for terms specifically, but potentially should apply to |
327 |
| - # other types. |
328 |
| - continue |
| 395 | + return _resolve_reference_in_domain(env, inv_name, inventory, |
| 396 | + honor_disabled_refs, |
| 397 | + domain, objtypes, |
| 398 | + node, contnode) |
329 | 399 |
|
330 |
| - if '://' not in uri and node.get('refdoc'): |
331 |
| - # get correct path in case of subdirectories |
332 |
| - uri = path.join(relative_path(node['refdoc'], '.'), uri) |
333 |
| - if version: |
334 |
| - reftitle = _('(in %s v%s)') % (proj, version) |
335 |
| - else: |
336 |
| - reftitle = _('(in %s)') % (proj,) |
337 |
| - newnode = nodes.reference('', '', internal=False, refuri=uri, reftitle=reftitle) |
338 |
| - if node.get('refexplicit'): |
339 |
| - # use whatever title was given |
340 |
| - newnode.append(contnode) |
341 |
| - elif dispname == '-' or \ |
342 |
| - (domain == 'std' and node['reftype'] == 'keyword'): |
343 |
| - # use whatever title was given, but strip prefix |
344 |
| - title = contnode.astext() |
345 |
| - if in_set and title.startswith(in_set + ':'): |
346 |
| - newnode.append(contnode.__class__(title[len(in_set) + 1:], |
347 |
| - title[len(in_set) + 1:])) |
348 |
| - else: |
349 |
| - newnode.append(contnode) |
350 |
| - else: |
351 |
| - # else use the given display name (used for :ref:) |
352 |
| - newnode.append(contnode.__class__(dispname, dispname)) |
353 |
| - return newnode |
354 |
| - # at least get rid of the ':' in the target if no explicit title given |
355 |
| - if in_set is not None and not node.get('refexplicit', True): |
356 |
| - if len(contnode) and isinstance(contnode[0], nodes.Text): |
357 |
| - contnode[0] = nodes.Text(newtarget, contnode[0].rawsource) |
358 | 400 |
|
359 |
| - return None |
| 401 | +def inventory_exists(env: BuildEnvironment, inv_name: str) -> bool: |
| 402 | + return inv_name in InventoryAdapter(env).named_inventory |
| 403 | + |
| 404 | + |
| 405 | +def resolve_reference_in_inventory(env: BuildEnvironment, |
| 406 | + inv_name: str, |
| 407 | + node: pending_xref, contnode: TextElement |
| 408 | + ) -> Optional[Element]: |
| 409 | + """Attempt to resolve a missing reference via intersphinx references. |
| 410 | +
|
| 411 | + Resolution is tried in the given inventory with the target as is. |
| 412 | +
|
| 413 | + Requires ``inventory_exists(env, inv_name)``. |
| 414 | + """ |
| 415 | + assert inventory_exists(env, inv_name) |
| 416 | + return _resolve_reference(env, inv_name, InventoryAdapter(env).named_inventory[inv_name], |
| 417 | + False, node, contnode) |
| 418 | + |
| 419 | + |
| 420 | +def resolve_reference_any_inventory(env: BuildEnvironment, |
| 421 | + honor_disabled_refs: bool, |
| 422 | + node: pending_xref, contnode: TextElement |
| 423 | + ) -> Optional[Element]: |
| 424 | + """Attempt to resolve a missing reference via intersphinx references. |
| 425 | +
|
| 426 | + Resolution is tried with the target as is in any inventory. |
| 427 | + """ |
| 428 | + return _resolve_reference(env, None, InventoryAdapter(env).main_inventory, |
| 429 | + honor_disabled_refs, |
| 430 | + node, contnode) |
| 431 | + |
| 432 | + |
| 433 | +def resolve_reference_detect_inventory(env: BuildEnvironment, |
| 434 | + node: pending_xref, contnode: TextElement |
| 435 | + ) -> Optional[Element]: |
| 436 | + """Attempt to resolve a missing reference via intersphinx references. |
| 437 | +
|
| 438 | + Resolution is tried first with the target as is in any inventory. |
| 439 | + If this does not succeed, then the target is split by the first ``:``, |
| 440 | + to form ``inv_name:newtarget``. If ``inv_name`` is a named inventory, then resolution |
| 441 | + is tried in that inventory with the new target. |
| 442 | + """ |
| 443 | + |
| 444 | + # ordinary direct lookup, use data as is |
| 445 | + res = resolve_reference_any_inventory(env, True, node, contnode) |
| 446 | + if res is not None: |
| 447 | + return res |
| 448 | + |
| 449 | + # try splitting the target into 'inv_name:target' |
| 450 | + target = node['reftarget'] |
| 451 | + if ':' not in target: |
| 452 | + return None |
| 453 | + inv_name, newtarget = target.split(':', 1) |
| 454 | + if not inventory_exists(env, inv_name): |
| 455 | + return None |
| 456 | + node['reftarget'] = newtarget |
| 457 | + res_inv = resolve_reference_in_inventory(env, inv_name, node, contnode) |
| 458 | + node['reftarget'] = target |
| 459 | + return res_inv |
| 460 | + |
| 461 | + |
| 462 | +def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, |
| 463 | + contnode: TextElement) -> Optional[Element]: |
| 464 | + """Attempt to resolve a missing reference via intersphinx references.""" |
| 465 | + |
| 466 | + return resolve_reference_detect_inventory(env, node, contnode) |
360 | 467 |
|
361 | 468 |
|
362 | 469 | def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None:
|
@@ -387,6 +494,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
|
387 | 494 | app.add_config_value('intersphinx_mapping', {}, True)
|
388 | 495 | app.add_config_value('intersphinx_cache_limit', 5, False)
|
389 | 496 | app.add_config_value('intersphinx_timeout', None, False)
|
| 497 | + app.add_config_value('intersphinx_disabled_reftypes', [], True) |
390 | 498 | app.connect('config-inited', normalize_intersphinx_mapping, priority=800)
|
391 | 499 | app.connect('builder-inited', load_mappings)
|
392 | 500 | app.connect('missing-reference', missing_reference)
|
|
0 commit comments