Skip to content

Fix #10899: Restore None hyperlink in py domain#14318

Open
japinderofficial-hub wants to merge 3 commits intosphinx-doc:masterfrom
japinderofficial-hub:Sphinx
Open

Fix #10899: Restore None hyperlink in py domain#14318
japinderofficial-hub wants to merge 3 commits intosphinx-doc:masterfrom
japinderofficial-hub:Sphinx

Conversation

@japinderofficial-hub
Copy link
Copy Markdown

Description

Since Sphinx 4.4.0, the None singleton is no longer hyperlinked in type annotations.

This occurs because None is currently treated using the class role instead of the obj role during cross-reference generation.

However, None is not a type — it is a singleton object. Therefore, it should be referenced using the :obj: role to generate correct cross-references.

Changes

  • Override the make_xref() method in PyField and PyTypedField
  • Detect when rolename == "class" and target == "None"
  • Switch the role from "class" to "obj" to ensure proper linking
  • Add test coverage to verify correct cross-referencing of None

Related Issues

Closes #10899
Closes collective/icalendar#1152

Fix Agent added 2 commits February 23, 2026 21:15
Since Sphinx 4.4.0, the None type is no longer hyperlinked in type annotations.
This was caused by None being treated with the 'class' role instead of the 'obj' role.

None is not a type but an object/singleton, so it should be referenced with the
:obj: role to generate proper cross-references. This fix overrides the make_xref
method in PyField and PyTypedField to detect when rolename is 'class' and
target is 'None', and changes it to use the 'obj' role instead.

Fixes: sphinx-doc#10899
Also resolves: collective/icalendar#1152
Test ensures that None type in function signatures and type hints
are properly linked with the obj role.
Copilot AI review requested due to automatic review settings February 23, 2026 16:42
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request fixes a regression introduced in Sphinx 4.4.0 where the None singleton was no longer hyperlinked in type annotations within docstring fields. The issue occurred because None was being referenced with the :class: role instead of the more appropriate :obj: role in field type annotations (e.g., :param, :type:, :rtype:).

Changes:

  • Override make_xref() method in PyField and PyTypedField classes to intercept type references to None
  • Convert the role from 'class' to 'obj' when referencing None in field types
  • Add test coverage to verify proper cross-referencing of None

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
sphinx/domains/python/_object.py Adds make_xref() override to PyField and PyTypedField to change role from 'class' to 'obj' for None
tests/test_domains/test_domain_py_fields.py Adds test to verify None is properly cross-referenced

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +165 to +167
if rolename == 'class' and target == 'None':
# None is not a type, so use obj role instead.
rolename = 'obj'
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition target == 'None' doesn't handle edge cases like '.None' or '~None' which are supported by parse_reftarget in _annotations.py. For consistency with parse_reftarget's logic (lines 35-51 in _annotations.py), consider using: if rolename == 'class' and target.lstrip('.~') == 'None': or extract and reuse the logic from parse_reftarget.

Copilot uses AI. Check for mistakes.
Comment on lines +190 to +192
if rolename == 'class' and target == 'None':
# None is not a type, so use obj role instead.
rolename = 'obj'
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition target == 'None' doesn't handle edge cases like '.None' or '~None' which are supported by parse_reftarget in _annotations.py. For consistency with parse_reftarget's logic (lines 35-51 in _annotations.py), consider using: if rolename == 'class' and target.lstrip('.~') == 'None': or extract and reuse the logic from parse_reftarget.

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +196
def make_xref(
self,
rolename: str,
domain: str,
target: str,
innernode: type[TextlikeNode] = nodes.emphasis,
contnode: Node | None = None,
env: BuildEnvironment | None = None,
inliner: Inliner | None = None,
location: Node | None = None,
) -> Node:
if rolename == 'class' and target == 'None':
# None is not a type, so use obj role instead.
rolename = 'obj'

return super().make_xref(
rolename, domain, target, innernode, contnode, env, inliner, location
)


class PyGroupedField(PyXrefMixin, GroupedField):
pass


class PyTypedField(PyXrefMixin, TypedField):
pass
def make_xref(
self,
rolename: str,
domain: str,
target: str,
innernode: type[TextlikeNode] = nodes.emphasis,
contnode: Node | None = None,
env: BuildEnvironment | None = None,
inliner: Inliner | None = None,
location: Node | None = None,
) -> Node:
if rolename == 'class' and target == 'None':
# None is not a type, so use obj role instead.
rolename = 'obj'

return super().make_xref(
rolename, domain, target, innernode, contnode, env, inliner, location
)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The identical make_xref implementation is duplicated in both PyField and PyTypedField classes. Since both inherit from PyXrefMixin, consider moving this None-handling logic into PyXrefMixin.make_xref instead. This would eliminate the code duplication and ensure consistent behavior across all Python domain field types. The fix could be placed at the beginning of PyXrefMixin.make_xref before calling super().make_xref().

Copilot uses AI. Check for mistakes.
# Verify that None xrefs have the correct role
for none_xref in none_xrefs:
# The reftype should be 'obj' not 'class' for None
assert none_xref.get('reftype') in ['obj', 'class'] or 'reftype' not in none_xref
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test assertion on line 637 is too permissive. It allows both 'obj' and 'class' as valid reftypes, which means the test would pass even if the fix doesn't work. The assertion should specifically verify that the reftype is 'obj' for None, not 'class'. Consider changing this to: assert none_xref.get('reftype') == 'obj', f"Expected reftype 'obj' but got '{none_xref.get('reftype')}' for None"

Suggested change
assert none_xref.get('reftype') in ['obj', 'class'] or 'reftype' not in none_xref
assert none_xref.get('reftype') == 'obj', (
f"Expected reftype 'obj' but got '{none_xref.get('reftype')}' for None"
)

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@niccokunzmann
Copy link
Copy Markdown

@japinderofficial-hub From my view, the comments by copilot are valid - especially, I do not like code duplication as a maintainer. The edge cases need considering and also testing - I would value it if you could add tests for them if I was the maintainer.
When the tests pass, you can let us know over at icalendar and we can give it a go, build the documentation and see if the changes address the problem for us.

Copy link
Copy Markdown

@niccokunzmann niccokunzmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, I left some comments on the code :)
I am not familiar with sphinx but with pytest and code quality.

inliner: Inliner | None = None,
location: Node | None = None,
) -> Node:
if rolename == 'class' and target == 'None':
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I would move the if check into is_none(rolename, target) and test that independently and remove the duplication.

Same goes for the rolename = 'obj'

Something like rolename = self.choose_rolename(...)

That can be documented, then.

@pytest.mark.sphinx('html', testroot='_blank')
def test_none_type_xref(app):
"""Test that None type is properly linked with the obj role."""
text = (
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you use pytest, please use pytest.mark.parametrize
to generate a separate test case for each special case. That makes it much easier for me to understand if they are all tested properly.

https://docs.pytest.org/en/stable/example/parametrize.html#different-options-for-test-ids

Especially

assert len(none_xrefs) >= 1,

does not tell me wether you really test all cases.
With parametrization, it should be changed to

assert len(none_xrefs) == 1

I would say that the test then actually catches what it should. One xref per example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

None type does not get linked in Sphinx py domain: None is no longer hyperlinked after 4.4.0

3 participants