Skip to content

Commit d7eb392

Browse files
committed
Merge branch 'cleanup/util-2b-typing-union-bugfix' into cleanup/util-2c-typing-style
# Conflicts: # sphinx/util/typing.py
2 parents 6905fe9 + dfab997 commit d7eb392

File tree

5 files changed

+69
-52
lines changed

5 files changed

+69
-52
lines changed

.ruff.toml

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,23 @@ exclude = [
1515
"doc/usage/extensions/example*.py",
1616
]
1717
ignore = [
18+
# flake8-annotations
1819
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `{name}`
20+
# pycodestyle
1921
"E741", # Ambiguous variable name: `{name}`
22+
# pyflakes
2023
"F841", # Local variable `{name}` is assigned to but never used
24+
# refurb
2125
"FURB101", # `open` and `read` should be replaced by `Path(...).read_text(...)`
2226
"FURB103", # `open` and `write` should be replaced by `Path(...).write_text(...)`
27+
# pylint
2328
"PLC1901", # simplify truthy/falsey string comparisons
29+
# flake8-simplify
2430
"SIM102", # Use a single `if` statement instead of nested `if` statements
2531
"SIM108", # Use ternary operator `{contents}` instead of `if`-`else`-block
32+
# pyupgrade
2633
"UP031", # Use format specifiers instead of percent format
2734
"UP032", # Use f-string instead of `format` call
28-
2935
]
3036
external = [ # Whitelist for RUF100 unknown code warnings
3137
"E704",
@@ -36,7 +42,8 @@ select = [
3642
# NOT YET USED
3743
# airflow ('AIR')
3844
# Airflow is not used in Sphinx
39-
"ANN", # flake8-annotations ('ANN')
45+
# flake8-annotations ('ANN')
46+
"ANN",
4047
# flake8-unused-arguments ('ARG')
4148
"ARG004", # Unused static method argument: `{name}`
4249
# flake8-async ('ASYNC')
@@ -124,7 +131,8 @@ select = [
124131
# NOT YET USED
125132
# flynt ('FLY')
126133
# NOT YET USED
127-
"FURB", # refurb
134+
# refurb ('FURB')
135+
"FURB",
128136
# flake8-logging-format ('G')
129137
"G001", # Logging statement uses `str.format`
130138
# "G002", # Logging statement uses `%`
@@ -136,6 +144,7 @@ select = [
136144
"G202", # Logging statement has redundant `exc_info`
137145
# isort ('I')
138146
"I",
147+
# flake8-import-conventions ('ICN')
139148
"ICN", # flake8-import-conventions
140149
# flake8-no-pep420 ('INP')
141150
"INP",
@@ -327,16 +336,19 @@ select = [
327336
"S612", # Use of insecure `logging.config.listen` detected
328337
# "S701", # Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
329338
# "S702", # Mako templates allow HTML and JavaScript rendering by default and are inherently open to XSS attacks
339+
# flake8-simplify ('SIM')
330340
"SIM", # flake8-simplify
331341
# flake8-self ('SLF')
332342
# NOT YET USED
333-
"SLOT", # flake8-slots
343+
# flake8-slots ('SLOT')
344+
"SLOT",
334345
# flake8-debugger ('T10')
335346
"T100", # Trace found: `{name}` used
336347
# flake8-print ('T20')
337348
"T201", # `print` found
338349
"T203", # `pprint` found
339-
"TCH", # flake8-type-checking
350+
# flake8-type-checking ('TCH')
351+
"TCH",
340352
# flake8-todos ('TD')
341353
# "TD001", # Invalid TODO tag: `{tag}`
342354
# "TD003", # Missing issue link on the line following this TODO
@@ -352,7 +364,8 @@ select = [
352364
# Trio is not used in Sphinx
353365
# tryceratops ('TRY')
354366
# NOT YET USED
355-
"UP001", # pyupgrade
367+
# pyupgrade ('UP')
368+
"UP",
356369
# pycodestyle ('W')
357370
"W191", # Indentation contains tabs
358371
# "W291", # Trailing whitespace

sphinx/util/inspect.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,10 @@ def isclassmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool:
219219
return True
220220
if cls and name:
221221
# trace __mro__ if the method is defined in parent class
222+
sentinel = object()
222223
for basecls in getmro(cls):
223-
meth = basecls.__dict__.get(name)
224-
if meth is not None:
224+
meth = basecls.__dict__.get(name, sentinel)
225+
if meth is not sentinel:
225226
return isclassmethod(meth)
226227
return False
227228

@@ -232,9 +233,10 @@ def isstaticmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool:
232233
return True
233234
if cls and name:
234235
# trace __mro__ if the method is defined in parent class
236+
sentinel = object()
235237
for basecls in getattr(cls, '__mro__', [cls]):
236-
meth = basecls.__dict__.get(name)
237-
if meth is not None:
238+
meth = basecls.__dict__.get(name, sentinel)
239+
if meth is not sentinel:
238240
return isinstance(meth, staticmethod)
239241
return False
240242

sphinx/util/typing.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -213,24 +213,8 @@ def restify(cls: type | None, mode: str = 'fully-qualified-except-typing') -> st
213213
and cls.__module__ == 'typing'
214214
and cls.__origin__ is Union # type: ignore[attr-defined]
215215
):
216-
# *cls* is defined in ``typing``, and thus ``__args__`` must exist;
217-
if NoneType in (__args__ := cls.__args__): # type: ignore[attr-defined]
218-
# Shape: Union[T_1, ..., T_k, None, T_{k+1}, ..., T_n]
219-
#
220-
# Note that we keep Literal[None] in their rightful place
221-
# since we want to distinguish the following semantics:
222-
#
223-
# - ``Union[int, None]`` is "an optional integer" and is
224-
# natively represented by ``Optional[int]``.
225-
# - ``Uniont[int, Literal["None"]]`` is "an integer or
226-
# the literal ``None``", and is natively kept as is.
227-
non_none = [a for a in __args__ if a is not NoneType]
228-
if len(non_none) == 1:
229-
return rf':py:obj:`~typing.Optional`\ [{restify(non_none[0], mode)}]'
230-
args = ', '.join(restify(a, mode) for a in non_none)
231-
return rf':py:obj:`~typing.Optional`\ [:obj:`~typing.Union`\ [{args}]]'
232-
args = ', '.join(restify(a, mode) for a in __args__)
233-
return rf':py:obj:`~typing.Union`\ [{args}]'
216+
# *cls* is defined in ``typing``, and thus ``__args__`` must exist
217+
return ' | '.join(restify(a, mode) for a in cls.__args__) # type: ignore[attr-defined]
234218
elif inspect.isgenericalias(cls):
235219
if isinstance(cls.__origin__, typing._SpecialForm): # type: ignore[attr-defined]
236220
text = restify(cls.__origin__, mode) # type: ignore[attr-defined,arg-type]

tests/test_extensions/test_ext_autodoc_autoclass.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,7 @@ def test_show_inheritance_for_subclass_of_generic_type(app):
276276
'.. py:class:: Quux(iterable=(), /)',
277277
' :module: target.classes',
278278
'',
279-
' Bases: :py:class:`~typing.List`\\ '
280-
'[:py:obj:`~typing.Union`\\ [:py:class:`int`, :py:class:`float`]]',
279+
' Bases: :py:class:`~typing.List`\\ [:py:class:`int` | :py:class:`float`]',
281280
'',
282281
' A subclass of List[Union[int, float]]',
283282
'',

tests/test_util/test_util_typing.py

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -190,25 +190,44 @@ def test_restify_type_hints_Callable():
190190

191191

192192
def test_restify_type_hints_Union():
193-
assert restify(Optional[int]) == ":py:obj:`~typing.Optional`\\ [:py:class:`int`]"
194-
assert restify(Union[str, None]) == ":py:obj:`~typing.Optional`\\ [:py:class:`str`]"
195-
assert restify(Union[None, str]) == ":py:obj:`~typing.Optional`\\ [:py:class:`str`]"
196-
assert restify(Union[int, str]) == (":py:obj:`~typing.Union`\\ "
197-
"[:py:class:`int`, :py:class:`str`]")
198-
assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ "
199-
"[:py:class:`int`, :py:class:`numbers.Integral`]")
200-
assert restify(Union[int, Integral], "smart") == (":py:obj:`~typing.Union`\\ "
201-
"[:py:class:`int`,"
202-
" :py:class:`~numbers.Integral`]")
193+
assert restify(Union[int]) == ":py:class:`int`"
194+
assert restify(Union[int, str]) == ":py:class:`int` | :py:class:`str`"
195+
assert restify(Optional[int]) == ":py:class:`int` | :py:obj:`None`"
196+
197+
assert restify(Union[str, None]) == ":py:class:`str` | :py:obj:`None`"
198+
assert restify(Union[None, str]) == ":py:obj:`None` | :py:class:`str`"
199+
assert restify(Optional[str]) == ":py:class:`str` | :py:obj:`None`"
200+
201+
assert restify(Union[int, str, None]) == (
202+
":py:class:`int` | :py:class:`str` | :py:obj:`None`"
203+
)
204+
assert restify(Optional[Union[int, str]]) in {
205+
":py:class:`str` | :py:class:`int` | :py:obj:`None`",
206+
":py:class:`int` | :py:class:`str` | :py:obj:`None`",
207+
}
208+
209+
assert restify(Union[int, Integral]) == (
210+
":py:class:`int` | :py:class:`numbers.Integral`"
211+
)
212+
assert restify(Union[int, Integral], "smart") == (
213+
":py:class:`int` | :py:class:`~numbers.Integral`"
214+
)
203215

204216
assert (restify(Union[MyClass1, MyClass2]) ==
205-
(":py:obj:`~typing.Union`\\ "
206-
"[:py:class:`tests.test_util.test_util_typing.MyClass1`, "
207-
":py:class:`tests.test_util.test_util_typing.<MyClass2>`]"))
217+
(":py:class:`tests.test_util.test_util_typing.MyClass1`"
218+
" | :py:class:`tests.test_util.test_util_typing.<MyClass2>`"))
208219
assert (restify(Union[MyClass1, MyClass2], "smart") ==
209-
(":py:obj:`~typing.Union`\\ "
210-
"[:py:class:`~tests.test_util.test_util_typing.MyClass1`,"
211-
" :py:class:`~tests.test_util.test_util_typing.<MyClass2>`]"))
220+
(":py:class:`~tests.test_util.test_util_typing.MyClass1`"
221+
" | :py:class:`~tests.test_util.test_util_typing.<MyClass2>`"))
222+
223+
assert (restify(Optional[Union[MyClass1, MyClass2]]) ==
224+
(":py:class:`tests.test_util.test_util_typing.MyClass1`"
225+
" | :py:class:`tests.test_util.test_util_typing.<MyClass2>`"
226+
" | :py:obj:`None`"))
227+
assert (restify(Optional[Union[MyClass1, MyClass2]], "smart") ==
228+
(":py:class:`~tests.test_util.test_util_typing.MyClass1`"
229+
" | :py:class:`~tests.test_util.test_util_typing.<MyClass2>`"
230+
" | :py:obj:`None`"))
212231

213232

214233
def test_restify_type_hints_typevars():
@@ -488,12 +507,12 @@ def test_stringify_type_hints_Union():
488507
assert stringify_annotation(Optional[int], "fully-qualified") == "int | None"
489508
assert stringify_annotation(Optional[int], "smart") == "int | None"
490509

491-
assert stringify_annotation(Union[str, None], 'fully-qualified-except-typing') == "str | None"
492-
assert stringify_annotation(Union[None, str], 'fully-qualified-except-typing') == "None | str"
493-
assert stringify_annotation(Union[str, None], "fully-qualified") == "str | None"
494-
assert stringify_annotation(Union[None, str], "fully-qualified") == "None | str"
495-
assert stringify_annotation(Union[str, None], "smart") == "str | None"
496-
assert stringify_annotation(Union[None, str], "smart") == "None | str"
510+
assert stringify_annotation(Union[int, None], 'fully-qualified-except-typing') == "int | None"
511+
assert stringify_annotation(Union[None, int], 'fully-qualified-except-typing') == "None | int"
512+
assert stringify_annotation(Union[int, None], "fully-qualified") == "int | None"
513+
assert stringify_annotation(Union[None, int], "fully-qualified") == "None | int"
514+
assert stringify_annotation(Union[int, None], "smart") == "int | None"
515+
assert stringify_annotation(Union[None, int], "smart") == "None | int"
497516

498517
assert stringify_annotation(Union[int, str], 'fully-qualified-except-typing') == "int | str"
499518
assert stringify_annotation(Union[int, str], "fully-qualified") == "int | str"

0 commit comments

Comments
 (0)