|
34 | 34 | import sphinx |
35 | 35 | from sphinx.addnodes import pending_xref |
36 | 36 | from sphinx.builders.html import INVENTORY_FILENAME |
| 37 | +from sphinx.deprecation import _deprecation_warning |
37 | 38 | from sphinx.errors import ExtensionError |
38 | 39 | from sphinx.locale import _, __ |
39 | 40 | from sphinx.transforms.post_transforms import ReferencesResolver |
@@ -533,17 +534,90 @@ def run(self) -> tuple[list[Node], list[system_message]]: |
533 | 534 | assert self.name == self.orig_name.lower() |
534 | 535 | inventory, name_suffix = self.get_inventory_and_name_suffix(self.orig_name) |
535 | 536 | if inventory and not inventory_exists(self.env, inventory): |
536 | | - logger.warning(__('inventory for external cross-reference not found: %s'), |
537 | | - inventory, location=(self.env.docname, self.lineno)) |
| 537 | + self._emit_warning( |
| 538 | + __('inventory for external cross-reference not found: %r'), inventory |
| 539 | + ) |
538 | 540 | return [], [] |
539 | 541 |
|
540 | | - role_name = self.get_role_name(name_suffix) |
| 542 | + domain_name, role_name = self._get_domain_role(name_suffix) |
| 543 | + |
541 | 544 | if role_name is None: |
542 | | - logger.warning(__('role for external cross-reference not found: %s'), name_suffix, |
543 | | - location=(self.env.docname, self.lineno)) |
| 545 | + self._emit_warning( |
| 546 | + __('invalid external cross-reference suffix: %r'), name_suffix |
| 547 | + ) |
544 | 548 | return [], [] |
545 | 549 |
|
546 | | - result, messages = self.invoke_role(role_name) |
| 550 | + # attempt to find a matching role function |
| 551 | + role_func: RoleFunction | None |
| 552 | + |
| 553 | + if domain_name is not None: |
| 554 | + # the user specified a domain, so we only check that |
| 555 | + if (domain := self.env.domains.get(domain_name)) is None: |
| 556 | + self._emit_warning( |
| 557 | + __('domain for external cross-reference not found: %r'), domain_name |
| 558 | + ) |
| 559 | + return [], [] |
| 560 | + if (role_func := domain.roles.get(role_name)) is None: |
| 561 | + msg = 'role for external cross-reference not found in domain %r: %r' |
| 562 | + if ( |
| 563 | + object_types := domain.object_types.get(role_name) |
| 564 | + ) is not None and object_types.roles: |
| 565 | + self._emit_warning( |
| 566 | + __(f'{msg} (perhaps you meant one of: %s)'), |
| 567 | + domain_name, |
| 568 | + role_name, |
| 569 | + self._concat_strings(object_types.roles), |
| 570 | + ) |
| 571 | + else: |
| 572 | + self._emit_warning(__(msg), domain_name, role_name) |
| 573 | + return [], [] |
| 574 | + |
| 575 | + else: |
| 576 | + # the user did not specify a domain, |
| 577 | + # so we check first the default (if available) then standard domains |
| 578 | + domains: list[Domain] = [] |
| 579 | + if default_domain := self.env.temp_data.get('default_domain'): |
| 580 | + domains.append(default_domain) |
| 581 | + if ( |
| 582 | + std_domain := self.env.domains.get('std') |
| 583 | + ) is not None and std_domain not in domains: |
| 584 | + domains.append(std_domain) |
| 585 | + |
| 586 | + role_func = None |
| 587 | + for domain in domains: |
| 588 | + if (role_func := domain.roles.get(role_name)) is not None: |
| 589 | + domain_name = domain.name |
| 590 | + break |
| 591 | + |
| 592 | + if role_func is None or domain_name is None: |
| 593 | + domains_str = self._concat_strings(d.name for d in domains) |
| 594 | + msg = 'role for external cross-reference not found in domains %s: %r' |
| 595 | + possible_roles: set[str] = set() |
| 596 | + for d in domains: |
| 597 | + if o := d.object_types.get(role_name): |
| 598 | + possible_roles.update(f'{d.name}:{r}' for r in o.roles) |
| 599 | + if possible_roles: |
| 600 | + msg = f'{msg} (perhaps you meant one of: %s)' |
| 601 | + self._emit_warning( |
| 602 | + __(msg), |
| 603 | + domains_str, |
| 604 | + role_name, |
| 605 | + self._concat_strings(possible_roles), |
| 606 | + ) |
| 607 | + else: |
| 608 | + self._emit_warning(__(msg), domains_str, role_name) |
| 609 | + return [], [] |
| 610 | + |
| 611 | + result, messages = role_func( |
| 612 | + f'{domain_name}:{role_name}', |
| 613 | + self.rawtext, |
| 614 | + self.text, |
| 615 | + self.lineno, |
| 616 | + self.inliner, |
| 617 | + self.options, |
| 618 | + self.content, |
| 619 | + ) |
| 620 | + |
547 | 621 | for node in result: |
548 | 622 | if isinstance(node, pending_xref): |
549 | 623 | node['intersphinx'] = True |
@@ -573,57 +647,74 @@ def get_inventory_and_name_suffix(self, name: str) -> tuple[str | None, str]: |
573 | 647 | msg = f'Malformed :external: role name: {name}' |
574 | 648 | raise ValueError(msg) |
575 | 649 |
|
576 | | - def get_role_name(self, name: str) -> tuple[str, str] | None: |
577 | | - """Find (if any) the corresponding ``(domain, role name)`` for *name*. |
578 | | -
|
579 | | - The *name* can be either a role name (e.g., ``py:function`` or ``function``) |
580 | | - given as ``domain:role`` or ``role``, or its corresponding object name |
581 | | - (in this case, ``py:func`` or ``func``) given as ``domain:objname`` or ``objname``. |
| 650 | + def _get_domain_role(self, name: str) -> tuple[str | None, str | None]: |
| 651 | + """Convert the *name* string into a domain and a role name. |
582 | 652 |
|
583 | | - If no domain is given, or the object/role name is not found for the requested domain, |
584 | | - the 'std' domain is used. |
| 653 | + - If *name* contains no ``:``, return ``(None, name)``. |
| 654 | + - If *name* contains a single ``:``, the domain/role is split on this. |
| 655 | + - If *name* contains multiple ``:``, return ``(None, None)``. |
585 | 656 | """ |
586 | 657 | names = name.split(':') |
587 | 658 | if len(names) == 1: |
| 659 | + return None, names[0] |
| 660 | + elif len(names) == 2: |
| 661 | + return names[0], names[1] |
| 662 | + else: |
| 663 | + return None, None |
| 664 | + |
| 665 | + def _emit_warning(self, msg: str, /, *args: Any) -> None: |
| 666 | + logger.warning( |
| 667 | + msg, |
| 668 | + *args, |
| 669 | + type='intersphinx', |
| 670 | + subtype='external', |
| 671 | + location=(self.env.docname, self.lineno), |
| 672 | + ) |
| 673 | + |
| 674 | + def _concat_strings(self, strings: Iterable[str]) -> str: |
| 675 | + return ', '.join(f'{s!r}' for s in sorted(strings)) |
| 676 | + |
| 677 | + # deprecated methods |
| 678 | + |
| 679 | + def get_role_name(self, name: str) -> tuple[str, str] | None: |
| 680 | + _deprecation_warning( |
| 681 | + __name__, f'{self.__class__.__name__}.get_role_name', '', remove=(9, 0) |
| 682 | + ) |
| 683 | + names = name.split(':') |
| 684 | + if len(names) == 1: |
| 685 | + # role |
588 | 686 | default_domain = self.env.temp_data.get('default_domain') |
589 | 687 | domain = default_domain.name if default_domain else None |
590 | | - name = names[0] |
| 688 | + role = names[0] |
591 | 689 | elif len(names) == 2: |
| 690 | + # domain:role: |
592 | 691 | domain = names[0] |
593 | | - name = names[1] |
| 692 | + role = names[1] |
594 | 693 | else: |
595 | 694 | return None |
596 | 695 |
|
597 | | - if domain and (role := self.get_role_name_from_domain(domain, name)): |
| 696 | + if domain and self.is_existent_role(domain, role): |
598 | 697 | return (domain, role) |
599 | | - elif (role := self.get_role_name_from_domain('std', name)): |
| 698 | + elif self.is_existent_role('std', role): |
600 | 699 | return ('std', role) |
601 | 700 | else: |
602 | 701 | return None |
603 | 702 |
|
604 | | - def is_existent_role(self, domain_name: str, role_or_obj_name: str) -> bool: |
605 | | - """Check if the given role or object exists in the given domain.""" |
606 | | - return self.get_role_name_from_domain(domain_name, role_or_obj_name) is not None |
607 | | - |
608 | | - def get_role_name_from_domain(self, domain_name: str, role_or_obj_name: str) -> str | None: |
609 | | - """Check if the given role or object exists in the given domain, |
610 | | - and return the related role name if it exists, otherwise return None. |
611 | | - """ |
| 703 | + def is_existent_role(self, domain_name: str, role_name: str) -> bool: |
| 704 | + _deprecation_warning( |
| 705 | + __name__, f'{self.__class__.__name__}.is_existent_role', '', remove=(9, 0) |
| 706 | + ) |
612 | 707 | try: |
613 | 708 | domain = self.env.get_domain(domain_name) |
| 709 | + return role_name in domain.roles |
614 | 710 | except ExtensionError: |
615 | | - return None |
616 | | - if role_or_obj_name in domain.roles: |
617 | | - return role_or_obj_name |
618 | | - if ( |
619 | | - (role_name := domain.role_for_objtype(role_or_obj_name)) |
620 | | - and role_name in domain.roles |
621 | | - ): |
622 | | - return role_name |
623 | | - return None |
| 711 | + return False |
624 | 712 |
|
625 | 713 | def invoke_role(self, role: tuple[str, str]) -> tuple[list[Node], list[system_message]]: |
626 | 714 | """Invoke the role described by a ``(domain, role name)`` pair.""" |
| 715 | + _deprecation_warning( |
| 716 | + __name__, f'{self.__class__.__name__}.invoke_role', '', remove=(9, 0) |
| 717 | + ) |
627 | 718 | domain = self.env.get_domain(role[0]) |
628 | 719 | if domain: |
629 | 720 | role_func = domain.role(role[1]) |
|
0 commit comments