Skip to content

Commit

Permalink
Merge branch 'cleanup/util-2e-typing-refactor' into fix/util-3-unpack
Browse files Browse the repository at this point in the history
  • Loading branch information
picnixz committed Apr 15, 2024
2 parents 1d52e28 + e99f070 commit 77de209
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 60 deletions.
25 changes: 19 additions & 6 deletions .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,23 @@ exclude = [
"doc/usage/extensions/example*.py",
]
ignore = [
# flake8-annotations
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `{name}`
# pycodestyle
"E741", # Ambiguous variable name: `{name}`
# pyflakes
"F841", # Local variable `{name}` is assigned to but never used
# refurb
"FURB101", # `open` and `read` should be replaced by `Path(...).read_text(...)`
"FURB103", # `open` and `write` should be replaced by `Path(...).write_text(...)`
# pylint
"PLC1901", # simplify truthy/falsey string comparisons
# flake8-simplify
"SIM102", # Use a single `if` statement instead of nested `if` statements
"SIM108", # Use ternary operator `{contents}` instead of `if`-`else`-block
# pyupgrade
"UP031", # Use format specifiers instead of percent format
"UP032", # Use f-string instead of `format` call

]
external = [ # Whitelist for RUF100 unknown code warnings
"E704",
Expand All @@ -36,7 +42,8 @@ select = [
# NOT YET USED
# airflow ('AIR')
# Airflow is not used in Sphinx
"ANN", # flake8-annotations ('ANN')
# flake8-annotations ('ANN')
"ANN",
# flake8-unused-arguments ('ARG')
"ARG004", # Unused static method argument: `{name}`
# flake8-async ('ASYNC')
Expand Down Expand Up @@ -124,7 +131,8 @@ select = [
# NOT YET USED
# flynt ('FLY')
# NOT YET USED
"FURB", # refurb
# refurb ('FURB')
"FURB",
# flake8-logging-format ('G')
"G001", # Logging statement uses `str.format`
# "G002", # Logging statement uses `%`
Expand All @@ -136,6 +144,7 @@ select = [
"G202", # Logging statement has redundant `exc_info`
# isort ('I')
"I",
# flake8-import-conventions ('ICN')
"ICN", # flake8-import-conventions
# flake8-no-pep420 ('INP')
"INP",
Expand Down Expand Up @@ -327,16 +336,19 @@ select = [
"S612", # Use of insecure `logging.config.listen` detected
# "S701", # Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function.
# "S702", # Mako templates allow HTML and JavaScript rendering by default and are inherently open to XSS attacks
# flake8-simplify ('SIM')
"SIM", # flake8-simplify
# flake8-self ('SLF')
# NOT YET USED
"SLOT", # flake8-slots
# flake8-slots ('SLOT')
"SLOT",
# flake8-debugger ('T10')
"T100", # Trace found: `{name}` used
# flake8-print ('T20')
"T201", # `print` found
"T203", # `pprint` found
"TCH", # flake8-type-checking
# flake8-type-checking ('TCH')
"TCH",
# flake8-todos ('TD')
# "TD001", # Invalid TODO tag: `{tag}`
# "TD003", # Missing issue link on the line following this TODO
Expand All @@ -352,7 +364,8 @@ select = [
# Trio is not used in Sphinx
# tryceratops ('TRY')
# NOT YET USED
"UP001", # pyupgrade
# pyupgrade ('UP')
"UP",
# pycodestyle ('W')
"W191", # Indentation contains tabs
# "W291", # Trailing whitespace
Expand Down
10 changes: 6 additions & 4 deletions sphinx/util/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,10 @@ def isclassmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool:
return True
if cls and name:
# trace __mro__ if the method is defined in parent class
sentinel = object()
for basecls in getmro(cls):
meth = basecls.__dict__.get(name)
if meth is not None:
meth = basecls.__dict__.get(name, sentinel)
if meth is not sentinel:
return isclassmethod(meth)
return False

Expand All @@ -268,9 +269,10 @@ def isstaticmethod(obj: Any, cls: Any = None, name: str | None = None) -> bool:
return True
if cls and name:
# trace __mro__ if the method is defined in parent class
sentinel = object()
for basecls in getattr(cls, '__mro__', [cls]):
meth = basecls.__dict__.get(name)
if meth is not None:
meth = basecls.__dict__.get(name, sentinel)
if meth is not sentinel:
return isinstance(meth, staticmethod)
return False

Expand Down
35 changes: 9 additions & 26 deletions sphinx/util/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,6 @@ def restify(cls: Any, mode: object = 'fully-qualified-except-typing') -> str:
from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading
from sphinx.util import inspect # lazy loading

if cls in {None, NoneType}:
return ':py:obj:`None`'
elif cls is Ellipsis:
return '...'
elif isinstance(cls, str):
return cls

mode = _validate_restify_mode(mode)
# With an if-else block, mypy infers 'mode' to be a 'str'
# instead of a literal string (and we don't want to cast).
Expand All @@ -239,7 +232,13 @@ def restify(cls: Any, mode: object = 'fully-qualified-except-typing') -> str:
# the least precise case.

try:
if ismockmodule(cls):
if cls in {None, NoneType}:
return ':py:obj:`None`'
elif cls is Ellipsis:
return '...'
elif isinstance(cls, str):
return cls
elif ismockmodule(cls):
return f':py:class:`{module_prefix}{cls.__name__}`'
elif ismock(cls):
return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`'
Expand Down Expand Up @@ -271,24 +270,8 @@ def restify(cls: Any, mode: object = 'fully-qualified-except-typing') -> str:
and cls.__module__ == 'typing'
and cls.__origin__ is Union
):
# *cls* is defined in ``typing``, thus ``__args__`` should exist
if NoneType in (__args__ := cls.__args__):
# Shape: Union[T_1, ..., T_k, None, T_{k+1}, ..., T_n]
#
# Note that we keep Literal[None] in their rightful place
# since we want to distinguish the following semantics:
#
# - ``Union[int, None]`` is "an optional integer" and is
# natively represented by ``Optional[int]``.
# - ``Uniont[int, Literal["None"]]`` is "an integer or
# the literal ``None``", and is natively kept as is.
non_none = [a for a in __args__ if a is not NoneType]
if len(non_none) == 1:
return rf':py:obj:`~typing.Optional`\ [{restify(non_none[0], mode)}]'
args = ', '.join(restify(a, mode) for a in non_none)
return rf':py:obj:`~typing.Optional`\ [:obj:`~typing.Union`\ [{args}]]'
args = ', '.join(restify(a, mode) for a in __args__)
return rf':py:obj:`~typing.Union`\ [{args}]'
# *cls* is defined in ``typing``, and thus ``__args__`` must exist
return ' | '.join(restify(a, mode) for a in cls.__args__)
elif inspect.isgenericalias(cls):
__origin__ = cls.__origin__
if _is_annotated_form(__origin__):
Expand Down
3 changes: 1 addition & 2 deletions tests/test_extensions/test_ext_autodoc_autoclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,7 @@ def test_show_inheritance_for_subclass_of_generic_type(app):
'.. py:class:: Quux(iterable=(), /)',
' :module: target.classes',
'',
' Bases: :py:class:`~typing.List`\\ '
'[:py:obj:`~typing.Union`\\ [:py:class:`int`, :py:class:`float`]]',
' Bases: :py:class:`~typing.List`\\ [:py:class:`int` | :py:class:`float`]',
'',
' A subclass of List[Union[int, float]]',
'',
Expand Down
63 changes: 41 additions & 22 deletions tests/test_util/test_util_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,25 +190,44 @@ def test_restify_type_hints_Callable():


def test_restify_type_hints_Union():
assert restify(Optional[int]) == ":py:obj:`~typing.Optional`\\ [:py:class:`int`]"
assert restify(Union[str, None]) == ":py:obj:`~typing.Optional`\\ [:py:class:`str`]"
assert restify(Union[None, str]) == ":py:obj:`~typing.Optional`\\ [:py:class:`str`]"
assert restify(Union[int, str]) == (":py:obj:`~typing.Union`\\ "
"[:py:class:`int`, :py:class:`str`]")
assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ "
"[:py:class:`int`, :py:class:`numbers.Integral`]")
assert restify(Union[int, Integral], "smart") == (":py:obj:`~typing.Union`\\ "
"[:py:class:`int`,"
" :py:class:`~numbers.Integral`]")
assert restify(Union[int]) == ":py:class:`int`"
assert restify(Union[int, str]) == ":py:class:`int` | :py:class:`str`"
assert restify(Optional[int]) == ":py:class:`int` | :py:obj:`None`"

assert restify(Union[str, None]) == ":py:class:`str` | :py:obj:`None`"
assert restify(Union[None, str]) == ":py:obj:`None` | :py:class:`str`"
assert restify(Optional[str]) == ":py:class:`str` | :py:obj:`None`"

assert restify(Union[int, str, None]) == (
":py:class:`int` | :py:class:`str` | :py:obj:`None`"
)
assert restify(Optional[Union[int, str]]) in {
":py:class:`str` | :py:class:`int` | :py:obj:`None`",
":py:class:`int` | :py:class:`str` | :py:obj:`None`",
}

assert restify(Union[int, Integral]) == (
":py:class:`int` | :py:class:`numbers.Integral`"
)
assert restify(Union[int, Integral], "smart") == (
":py:class:`int` | :py:class:`~numbers.Integral`"
)

assert (restify(Union[MyClass1, MyClass2]) ==
(":py:obj:`~typing.Union`\\ "
"[:py:class:`tests.test_util.test_util_typing.MyClass1`, "
":py:class:`tests.test_util.test_util_typing.<MyClass2>`]"))
(":py:class:`tests.test_util.test_util_typing.MyClass1`"
" | :py:class:`tests.test_util.test_util_typing.<MyClass2>`"))
assert (restify(Union[MyClass1, MyClass2], "smart") ==
(":py:obj:`~typing.Union`\\ "
"[:py:class:`~tests.test_util.test_util_typing.MyClass1`,"
" :py:class:`~tests.test_util.test_util_typing.<MyClass2>`]"))
(":py:class:`~tests.test_util.test_util_typing.MyClass1`"
" | :py:class:`~tests.test_util.test_util_typing.<MyClass2>`"))

assert (restify(Optional[Union[MyClass1, MyClass2]]) ==
(":py:class:`tests.test_util.test_util_typing.MyClass1`"
" | :py:class:`tests.test_util.test_util_typing.<MyClass2>`"
" | :py:obj:`None`"))
assert (restify(Optional[Union[MyClass1, MyClass2]], "smart") ==
(":py:class:`~tests.test_util.test_util_typing.MyClass1`"
" | :py:class:`~tests.test_util.test_util_typing.<MyClass2>`"
" | :py:obj:`None`"))


def test_restify_type_hints_typevars():
Expand Down Expand Up @@ -534,12 +553,12 @@ def test_stringify_type_hints_Union():
assert stringify_annotation(Optional[int], "fully-qualified") == "int | None"
assert stringify_annotation(Optional[int], "smart") == "int | None"

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

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

0 comments on commit 77de209

Please sign in to comment.