mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Fix #8219: autodoc: Parameters for generic base class are not shown
This commit is contained in:
parent
5337e3848c
commit
e2bf9166da
3
CHANGES
3
CHANGES
@ -54,6 +54,9 @@ Features added
|
|||||||
Bugs fixed
|
Bugs fixed
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
* #8219: autodoc: Parameters for generic class are not shown when super class is
|
||||||
|
a generic class and show-inheritance option is given (in Python 3.7 or above)
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ from sphinx.util.docstrings import extract_metadata, prepare_docstring
|
|||||||
from sphinx.util.inspect import (
|
from sphinx.util.inspect import (
|
||||||
evaluate_signature, getdoc, object_description, safe_getattr, stringify_signature
|
evaluate_signature, getdoc, object_description, safe_getattr, stringify_signature
|
||||||
)
|
)
|
||||||
from sphinx.util.typing import stringify as stringify_typehint
|
from sphinx.util.typing import restify, stringify as stringify_typehint
|
||||||
|
|
||||||
if False:
|
if False:
|
||||||
# For type annotation
|
# For type annotation
|
||||||
@ -1574,13 +1574,16 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
|||||||
if not self.doc_as_attr and self.options.show_inheritance:
|
if not self.doc_as_attr and self.options.show_inheritance:
|
||||||
sourcename = self.get_sourcename()
|
sourcename = self.get_sourcename()
|
||||||
self.add_line('', sourcename)
|
self.add_line('', sourcename)
|
||||||
if hasattr(self.object, '__bases__') and len(self.object.__bases__):
|
|
||||||
bases = [':class:`%s`' % b.__name__
|
if hasattr(self.object, '__orig_bases__') and len(self.object.__orig_bases__):
|
||||||
if b.__module__ in ('__builtin__', 'builtins')
|
# A subclass of generic types
|
||||||
else ':class:`%s.%s`' % (b.__module__, b.__qualname__)
|
# refs: PEP-560 <https://www.python.org/dev/peps/pep-0560/>
|
||||||
for b in self.object.__bases__]
|
bases = [restify(cls) for cls in self.object.__orig_bases__]
|
||||||
self.add_line(' ' + _('Bases: %s') % ', '.join(bases),
|
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
|
||||||
sourcename)
|
elif hasattr(self.object, '__bases__') and len(self.object.__bases__):
|
||||||
|
# A normal class
|
||||||
|
bases = [restify(cls) for cls in self.object.__bases__]
|
||||||
|
self.add_line(' ' + _('Bases: %s') % ', '.join(bases), sourcename)
|
||||||
|
|
||||||
def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
|
def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
|
||||||
if encoding is not None:
|
if encoding is not None:
|
||||||
|
@ -312,6 +312,9 @@ def isgenericalias(obj: Any) -> bool:
|
|||||||
elif (hasattr(types, 'GenericAlias') and # only for py39+
|
elif (hasattr(types, 'GenericAlias') and # only for py39+
|
||||||
isinstance(obj, types.GenericAlias)): # type: ignore
|
isinstance(obj, types.GenericAlias)): # type: ignore
|
||||||
return True
|
return True
|
||||||
|
elif (hasattr(typing, '_SpecialGenericAlias') and # for py39+
|
||||||
|
isinstance(obj, typing._SpecialGenericAlias)): # type: ignore
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
from typing import Any, Callable, Dict, Generator, List, Tuple, TypeVar, Union
|
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, TypeVar, Union
|
||||||
|
|
||||||
from docutils import nodes
|
from docutils import nodes
|
||||||
from docutils.parsers.rst.states import Inliner
|
from docutils.parsers.rst.states import Inliner
|
||||||
@ -30,6 +30,10 @@ else:
|
|||||||
ref = _ForwardRef(self.arg)
|
ref = _ForwardRef(self.arg)
|
||||||
return ref._eval_type(globalns, localns)
|
return ref._eval_type(globalns, localns)
|
||||||
|
|
||||||
|
if False:
|
||||||
|
# For type annotation
|
||||||
|
from typing import Type # NOQA # for python3.5.1
|
||||||
|
|
||||||
|
|
||||||
# An entry of Directive.option_spec
|
# An entry of Directive.option_spec
|
||||||
DirectiveOption = Callable[[str], Any]
|
DirectiveOption = Callable[[str], Any]
|
||||||
@ -60,6 +64,195 @@ def is_system_TypeVar(typ: Any) -> bool:
|
|||||||
return modname == 'typing' and isinstance(typ, TypeVar)
|
return modname == 'typing' and isinstance(typ, TypeVar)
|
||||||
|
|
||||||
|
|
||||||
|
def restify(cls: Optional["Type"]) -> str:
|
||||||
|
"""Convert python class to a reST reference."""
|
||||||
|
if cls is None or cls is NoneType:
|
||||||
|
return ':obj:`None`'
|
||||||
|
elif cls is Ellipsis:
|
||||||
|
return '...'
|
||||||
|
elif cls.__module__ in ('__builtin__', 'builtins'):
|
||||||
|
return ':class:`%s`' % cls.__name__
|
||||||
|
else:
|
||||||
|
if sys.version_info >= (3, 7): # py37+
|
||||||
|
return _restify_py37(cls)
|
||||||
|
else:
|
||||||
|
return _restify_py36(cls)
|
||||||
|
|
||||||
|
|
||||||
|
def _restify_py37(cls: Optional["Type"]) -> str:
|
||||||
|
"""Convert python class to a reST reference."""
|
||||||
|
from sphinx.util import inspect # lazy loading
|
||||||
|
|
||||||
|
if (inspect.isgenericalias(cls) and
|
||||||
|
cls.__module__ == 'typing' and cls.__origin__ is Union):
|
||||||
|
# Union
|
||||||
|
if len(cls.__args__) > 1 and cls.__args__[-1] is NoneType:
|
||||||
|
if len(cls.__args__) > 2:
|
||||||
|
args = ', '.join(restify(a) for a in cls.__args__[:-1])
|
||||||
|
return ':obj:`Optional`\\ [:obj:`Union`\\ [%s]]' % args
|
||||||
|
else:
|
||||||
|
return ':obj:`Optional`\\ [%s]' % restify(cls.__args__[0])
|
||||||
|
else:
|
||||||
|
args = ', '.join(restify(a) for a in cls.__args__)
|
||||||
|
return ':obj:`Union`\\ [%s]' % args
|
||||||
|
elif inspect.isgenericalias(cls):
|
||||||
|
if getattr(cls, '_name', None):
|
||||||
|
if cls.__module__ == 'typing':
|
||||||
|
text = ':class:`%s`' % cls._name
|
||||||
|
else:
|
||||||
|
text = ':class:`%s.%s`' % (cls.__module__, cls._name)
|
||||||
|
else:
|
||||||
|
text = restify(cls.__origin__)
|
||||||
|
|
||||||
|
if not hasattr(cls, '__args__'):
|
||||||
|
pass
|
||||||
|
elif all(is_system_TypeVar(a) for a in cls.__args__):
|
||||||
|
# Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT])
|
||||||
|
pass
|
||||||
|
elif cls.__module__ == 'typing' and cls._name == 'Callable':
|
||||||
|
args = ', '.join(restify(a) for a in cls.__args__[:-1])
|
||||||
|
text += r"\ [[%s], %s]" % (args, restify(cls.__args__[-1]))
|
||||||
|
elif cls.__args__:
|
||||||
|
text += r"\ [%s]" % ", ".join(restify(a) for a in cls.__args__)
|
||||||
|
|
||||||
|
return text
|
||||||
|
elif hasattr(cls, '__qualname__'):
|
||||||
|
if cls.__module__ == 'typing':
|
||||||
|
return ':class:`%s`' % cls.__qualname__
|
||||||
|
else:
|
||||||
|
return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__)
|
||||||
|
elif hasattr(cls, '_name'):
|
||||||
|
# SpecialForm
|
||||||
|
if cls.__module__ == 'typing':
|
||||||
|
return ':obj:`%s`' % cls._name
|
||||||
|
else:
|
||||||
|
return ':obj:`%s.%s`' % (cls.__module__, cls._name)
|
||||||
|
else:
|
||||||
|
# not a class (ex. TypeVar)
|
||||||
|
return ':obj:`%s.%s`' % (cls.__module__, cls.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _restify_py36(cls: Optional["Type"]) -> str:
|
||||||
|
module = getattr(cls, '__module__', None)
|
||||||
|
if module == 'typing':
|
||||||
|
if getattr(cls, '_name', None):
|
||||||
|
qualname = cls._name
|
||||||
|
elif getattr(cls, '__qualname__', None):
|
||||||
|
qualname = cls.__qualname__
|
||||||
|
elif getattr(cls, '__forward_arg__', None):
|
||||||
|
qualname = cls.__forward_arg__
|
||||||
|
elif getattr(cls, '__origin__', None):
|
||||||
|
qualname = stringify(cls.__origin__) # ex. Union
|
||||||
|
else:
|
||||||
|
qualname = repr(cls).replace('typing.', '')
|
||||||
|
elif hasattr(cls, '__qualname__'):
|
||||||
|
qualname = '%s.%s' % (module, cls.__qualname__)
|
||||||
|
else:
|
||||||
|
qualname = repr(cls)
|
||||||
|
|
||||||
|
if (isinstance(cls, typing.TupleMeta) and # type: ignore
|
||||||
|
not hasattr(cls, '__tuple_params__')): # for Python 3.6
|
||||||
|
params = cls.__args__
|
||||||
|
if params:
|
||||||
|
param_str = ', '.join(restify(p) for p in params)
|
||||||
|
return ':class:`%s`\\ [%s]' % (qualname, param_str)
|
||||||
|
else:
|
||||||
|
return ':class:`%s`' % qualname
|
||||||
|
elif isinstance(cls, typing.GenericMeta):
|
||||||
|
params = None
|
||||||
|
if hasattr(cls, '__args__'):
|
||||||
|
# for Python 3.5.2+
|
||||||
|
if cls.__args__ is None or len(cls.__args__) <= 2: # type: ignore # NOQA
|
||||||
|
params = cls.__args__ # type: ignore
|
||||||
|
elif cls.__origin__ == Generator: # type: ignore
|
||||||
|
params = cls.__args__ # type: ignore
|
||||||
|
else: # typing.Callable
|
||||||
|
args = ', '.join(restify(arg) for arg in cls.__args__[:-1]) # type: ignore
|
||||||
|
result = restify(cls.__args__[-1]) # type: ignore
|
||||||
|
return ':class:`%s`\\ [[%s], %s]' % (qualname, args, result)
|
||||||
|
elif hasattr(cls, '__parameters__'):
|
||||||
|
# for Python 3.5.0 and 3.5.1
|
||||||
|
params = cls.__parameters__ # type: ignore
|
||||||
|
|
||||||
|
if params:
|
||||||
|
param_str = ', '.join(restify(p) for p in params)
|
||||||
|
return ':class:`%s`\\ [%s]' % (qualname, param_str)
|
||||||
|
else:
|
||||||
|
return ':class:`%s`' % qualname
|
||||||
|
elif (hasattr(typing, 'UnionMeta') and
|
||||||
|
isinstance(cls, typing.UnionMeta) and # type: ignore
|
||||||
|
hasattr(cls, '__union_params__')): # for Python 3.5
|
||||||
|
params = cls.__union_params__
|
||||||
|
if params is not None:
|
||||||
|
if len(params) == 2 and params[1] is NoneType:
|
||||||
|
return ':obj:`Optional`\\ [%s]' % restify(params[0])
|
||||||
|
else:
|
||||||
|
param_str = ', '.join(restify(p) for p in params)
|
||||||
|
return ':obj:`%s`\\ [%s]' % (qualname, param_str)
|
||||||
|
else:
|
||||||
|
return ':obj:`%s`' % qualname
|
||||||
|
elif (hasattr(cls, '__origin__') and
|
||||||
|
cls.__origin__ is typing.Union): # for Python 3.5.2+
|
||||||
|
params = cls.__args__
|
||||||
|
if params is not None:
|
||||||
|
if len(params) > 1 and params[-1] is NoneType:
|
||||||
|
if len(params) > 2:
|
||||||
|
param_str = ", ".join(restify(p) for p in params[:-1])
|
||||||
|
return ':obj:`Optional`\\ [:obj:`Union`\\ [%s]]' % param_str
|
||||||
|
else:
|
||||||
|
return ':obj:`Optional`\\ [%s]' % restify(params[0])
|
||||||
|
else:
|
||||||
|
param_str = ', '.join(restify(p) for p in params)
|
||||||
|
return ':obj:`Union`\\ [%s]' % param_str
|
||||||
|
else:
|
||||||
|
return ':obj:`Union`'
|
||||||
|
elif (isinstance(cls, typing.CallableMeta) and # type: ignore
|
||||||
|
getattr(cls, '__args__', None) is not None and
|
||||||
|
hasattr(cls, '__result__')): # for Python 3.5
|
||||||
|
# Skipped in the case of plain typing.Callable
|
||||||
|
args = cls.__args__
|
||||||
|
if args is None:
|
||||||
|
return qualname
|
||||||
|
elif args is Ellipsis:
|
||||||
|
args_str = '...'
|
||||||
|
else:
|
||||||
|
formatted_args = (restify(a) for a in args) # type: ignore
|
||||||
|
args_str = '[%s]' % ', '.join(formatted_args)
|
||||||
|
|
||||||
|
return ':class:`%s`\\ [%s, %s]' % (qualname, args_str, stringify(cls.__result__))
|
||||||
|
elif (isinstance(cls, typing.TupleMeta) and # type: ignore
|
||||||
|
hasattr(cls, '__tuple_params__') and
|
||||||
|
hasattr(cls, '__tuple_use_ellipsis__')): # for Python 3.5
|
||||||
|
params = cls.__tuple_params__
|
||||||
|
if params is not None:
|
||||||
|
param_strings = [restify(p) for p in params]
|
||||||
|
if cls.__tuple_use_ellipsis__:
|
||||||
|
param_strings.append('...')
|
||||||
|
return ':class:`%s`\\ [%s]' % (qualname, ', '.join(param_strings))
|
||||||
|
else:
|
||||||
|
return ':class:`%s`' % qualname
|
||||||
|
elif hasattr(cls, '__qualname__'):
|
||||||
|
if cls.__module__ == 'typing':
|
||||||
|
return ':class:`%s`' % cls.__qualname__
|
||||||
|
else:
|
||||||
|
return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__)
|
||||||
|
elif hasattr(cls, '_name'):
|
||||||
|
# SpecialForm
|
||||||
|
if cls.__module__ == 'typing':
|
||||||
|
return ':obj:`%s`' % cls._name
|
||||||
|
else:
|
||||||
|
return ':obj:`%s.%s`' % (cls.__module__, cls._name)
|
||||||
|
elif hasattr(cls, '__name__'):
|
||||||
|
# not a class (ex. TypeVar)
|
||||||
|
return ':obj:`%s.%s`' % (cls.__module__, cls.__name__)
|
||||||
|
else:
|
||||||
|
# others (ex. Any)
|
||||||
|
if cls.__module__ == 'typing':
|
||||||
|
return ':obj:`%s`' % qualname
|
||||||
|
else:
|
||||||
|
return ':obj:`%s.%s`' % (cls.__module__, qualname)
|
||||||
|
|
||||||
|
|
||||||
def stringify(annotation: Any) -> str:
|
def stringify(annotation: Any) -> str:
|
||||||
"""Stringify type annotation object."""
|
"""Stringify type annotation object."""
|
||||||
if isinstance(annotation, str):
|
if isinstance(annotation, str):
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from inspect import Parameter, Signature
|
from inspect import Parameter, Signature
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
|
||||||
class Foo:
|
class Foo:
|
||||||
@ -21,3 +22,8 @@ class Qux:
|
|||||||
|
|
||||||
def __init__(self, x, y):
|
def __init__(self, x, y):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Quux(List[Union[int, float]]):
|
||||||
|
"""A subclass of List[Union[int, float]]"""
|
||||||
|
pass
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
:license: BSD, see LICENSE for details.
|
:license: BSD, see LICENSE for details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from test_ext_autodoc import do_autodoc
|
from test_ext_autodoc import do_autodoc
|
||||||
@ -73,3 +75,20 @@ def test_decorators(app):
|
|||||||
' :module: target.decorator',
|
' :module: target.decorator',
|
||||||
'',
|
'',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
|
||||||
|
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
||||||
|
def test_show_inheritance_for_subclass_of_generic_type(app):
|
||||||
|
options = {'show-inheritance': True}
|
||||||
|
actual = do_autodoc(app, 'class', 'target.classes.Quux', options)
|
||||||
|
assert list(actual) == [
|
||||||
|
'',
|
||||||
|
'.. py:class:: Quux(iterable=(), /)',
|
||||||
|
' :module: target.classes',
|
||||||
|
'',
|
||||||
|
' Bases: :class:`List`\\ [:obj:`Union`\\ [:class:`int`, :class:`float`]]',
|
||||||
|
'',
|
||||||
|
' A subclass of List[Union[int, float]]',
|
||||||
|
'',
|
||||||
|
]
|
||||||
|
@ -16,7 +16,7 @@ from typing import (
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sphinx.util.typing import stringify
|
from sphinx.util.typing import restify, stringify
|
||||||
|
|
||||||
|
|
||||||
class MyClass1:
|
class MyClass1:
|
||||||
@ -36,6 +36,80 @@ class BrokenType:
|
|||||||
__args__ = int
|
__args__ = int
|
||||||
|
|
||||||
|
|
||||||
|
def test_restify():
|
||||||
|
assert restify(int) == ":class:`int`"
|
||||||
|
assert restify(str) == ":class:`str`"
|
||||||
|
assert restify(None) == ":obj:`None`"
|
||||||
|
assert restify(Integral) == ":class:`numbers.Integral`"
|
||||||
|
assert restify(Any) == ":obj:`Any`"
|
||||||
|
|
||||||
|
|
||||||
|
def test_restify_type_hints_containers():
|
||||||
|
assert restify(List) == ":class:`List`"
|
||||||
|
assert restify(Dict) == ":class:`Dict`"
|
||||||
|
assert restify(List[int]) == ":class:`List`\\ [:class:`int`]"
|
||||||
|
assert restify(List[str]) == ":class:`List`\\ [:class:`str`]"
|
||||||
|
assert restify(Dict[str, float]) == ":class:`Dict`\\ [:class:`str`, :class:`float`]"
|
||||||
|
assert restify(Tuple[str, str, str]) == ":class:`Tuple`\\ [:class:`str`, :class:`str`, :class:`str`]"
|
||||||
|
assert restify(Tuple[str, ...]) == ":class:`Tuple`\\ [:class:`str`, ...]"
|
||||||
|
assert restify(List[Dict[str, Tuple]]) == ":class:`List`\\ [:class:`Dict`\\ [:class:`str`, :class:`Tuple`]]"
|
||||||
|
assert restify(MyList[Tuple[int, int]]) == ":class:`test_util_typing.MyList`\\ [:class:`Tuple`\\ [:class:`int`, :class:`int`]]"
|
||||||
|
assert restify(Generator[None, None, None]) == ":class:`Generator`\\ [:obj:`None`, :obj:`None`, :obj:`None`]"
|
||||||
|
|
||||||
|
|
||||||
|
def test_restify_type_hints_Callable():
|
||||||
|
assert restify(Callable) == ":class:`Callable`"
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 7):
|
||||||
|
assert restify(Callable[[str], int]) == ":class:`Callable`\\ [[:class:`str`], :class:`int`]"
|
||||||
|
assert restify(Callable[..., int]) == ":class:`Callable`\\ [[...], :class:`int`]"
|
||||||
|
else:
|
||||||
|
assert restify(Callable[[str], int]) == ":class:`Callable`\\ [:class:`str`, :class:`int`]"
|
||||||
|
assert restify(Callable[..., int]) == ":class:`Callable`\\ [..., :class:`int`]"
|
||||||
|
|
||||||
|
|
||||||
|
def test_restify_type_hints_Union():
|
||||||
|
assert restify(Optional[int]) == ":obj:`Optional`\\ [:class:`int`]"
|
||||||
|
assert restify(Union[str, None]) == ":obj:`Optional`\\ [:class:`str`]"
|
||||||
|
assert restify(Union[int, str]) == ":obj:`Union`\\ [:class:`int`, :class:`str`]"
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 7):
|
||||||
|
assert restify(Union[int, Integral]) == ":obj:`Union`\\ [:class:`int`, :class:`numbers.Integral`]"
|
||||||
|
assert (restify(Union[MyClass1, MyClass2]) ==
|
||||||
|
":obj:`Union`\\ [:class:`test_util_typing.MyClass1`, :class:`test_util_typing.<MyClass2>`]")
|
||||||
|
else:
|
||||||
|
assert restify(Union[int, Integral]) == ":class:`numbers.Integral`"
|
||||||
|
assert restify(Union[MyClass1, MyClass2]) == ":class:`test_util_typing.MyClass1`"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
|
||||||
|
def test_restify_type_hints_typevars():
|
||||||
|
T = TypeVar('T')
|
||||||
|
T_co = TypeVar('T_co', covariant=True)
|
||||||
|
T_contra = TypeVar('T_contra', contravariant=True)
|
||||||
|
|
||||||
|
assert restify(T) == ":obj:`test_util_typing.T`"
|
||||||
|
assert restify(T_co) == ":obj:`test_util_typing.T_co`"
|
||||||
|
assert restify(T_contra) == ":obj:`test_util_typing.T_contra`"
|
||||||
|
assert restify(List[T]) == ":class:`List`\\ [:obj:`test_util_typing.T`]"
|
||||||
|
|
||||||
|
|
||||||
|
def test_restify_type_hints_custom_class():
|
||||||
|
assert restify(MyClass1) == ":class:`test_util_typing.MyClass1`"
|
||||||
|
assert restify(MyClass2) == ":class:`test_util_typing.<MyClass2>`"
|
||||||
|
|
||||||
|
|
||||||
|
def test_restify_type_hints_alias():
|
||||||
|
MyStr = str
|
||||||
|
MyTuple = Tuple[str, str]
|
||||||
|
assert restify(MyStr) == ":class:`str`"
|
||||||
|
assert restify(MyTuple) == ":class:`Tuple`\\ [:class:`str`, :class:`str`]" # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_restify_broken_type_hints():
|
||||||
|
assert restify(BrokenType) == ':class:`test_util_typing.BrokenType`'
|
||||||
|
|
||||||
|
|
||||||
def test_stringify():
|
def test_stringify():
|
||||||
assert stringify(int) == "int"
|
assert stringify(int) == "int"
|
||||||
assert stringify(str) == "str"
|
assert stringify(str) == "str"
|
||||||
|
Loading…
Reference in New Issue
Block a user