diff --git a/CHANGES b/CHANGES index 968e4a24c..9a481b588 100644 --- a/CHANGES +++ b/CHANGES @@ -21,9 +21,7 @@ Deprecated * ``sphinx.roles.Index`` * ``sphinx.util.detect_encoding()`` * ``sphinx.util.get_module_source()`` -* ``sphinx.util.inspect.Signature.format_annotation()`` -* ``sphinx.util.inspect.Signature.format_annotation_new()`` -* ``sphinx.util.inspect.Signature.format_annotation_old()`` +* ``sphinx.util.inspect.Signature`` Features added -------------- @@ -36,6 +34,7 @@ Features added * #6696: html: ``:scale:`` option of image/figure directive not working for SVG images (imagesize-1.2.0 or above is required) * #6994: imgconverter: Support illustrator file (.ai) to .png conversion +* autodoc: Support Positional-Only Argument separator (PEP-570 compliant) * SphinxTranslator now calls visitor/departure method for super node class if visitor/departure method for original node class not found @@ -47,6 +46,7 @@ Bugs fixed * #6961: latex: warning for babel shown twice * #6559: Wrong node-ids are generated in glossary directive * #6986: apidoc: misdetects module name for .so file inside module +* #6999: napoleon: fails to parse tilde in :exc: role Testing -------- diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index ec6db2c16..6c2b05816 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -81,20 +81,11 @@ The following is a list of deprecated interfaces. - 4.0 - N/A - * - ``sphinx.util.inspect.Signature.format_annotation()`` + * - ``sphinx.util.inspect.Signature`` - 2.4 - 4.0 - - ``sphinx.util.typing.stringify()`` - - * - ``sphinx.util.inspect.Signature.format_annotation_new()`` - - 2.4 - - 4.0 - - ``sphinx.util.typing.stringify()`` - - * - ``sphinx.util.inspect.Signature.format_annotation_old()`` - - 2.4 - - 4.0 - - ``sphinx.util.typing.stringify()`` + - ``sphinx.util.inspect.signature`` and + ``sphinx.util.inspect.stringify_signature()`` * - ``sphinx.builders.gettext.POHEADER`` - 2.3 diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 90c03c9a4..c109b763b 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -254,7 +254,7 @@ def make_glossary_term(env: "BuildEnvironment", textnodes: Iterable[Node], index if node_id: # node_id is given from outside (mainly i18n module), use it forcedly - pass + term['ids'].append(node_id) elif document: node_id = make_id(env, document, 'term', termtext) term['ids'].append(node_id) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 8c5ace92a..2124b2d25 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -33,7 +33,7 @@ from sphinx.util import logging from sphinx.util import rpartition from sphinx.util.docstrings import prepare_docstring from sphinx.util.inspect import ( - Signature, getdoc, object_description, safe_getattr, safe_getmembers + getdoc, object_description, safe_getattr, safe_getmembers, stringify_signature ) if False: @@ -983,9 +983,10 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ not inspect.isbuiltin(self.object) and not inspect.isclass(self.object) and hasattr(self.object, '__call__')): - args = Signature(self.object.__call__).format_args(**kwargs) + sig = inspect.signature(self.object.__call__) else: - args = Signature(self.object).format_args(**kwargs) + sig = inspect.signature(self.object) + args = stringify_signature(sig, **kwargs) except TypeError: if (inspect.is_builtin_class_method(self.object, '__new__') and inspect.is_builtin_class_method(self.object, '__init__')): @@ -995,11 +996,11 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ # typing) we try to use the constructor signature as function # signature without the first argument. try: - sig = Signature(self.object.__new__, bound_method=True, has_retval=False) - args = sig.format_args(**kwargs) + sig = inspect.signature(self.object.__new__, bound_method=True) + args = stringify_signature(sig, show_return_annotation=False, **kwargs) except TypeError: - sig = Signature(self.object.__init__, bound_method=True, has_retval=False) - args = sig.format_args(**kwargs) + sig = inspect.signature(self.object.__init__, bound_method=True) + args = stringify_signature(sig, show_return_annotation=False, **kwargs) # escape backslashes for reST args = args.replace('\\', '\\\\') @@ -1080,8 +1081,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)): return None try: - sig = Signature(initmeth, bound_method=True, has_retval=False) - return sig.format_args(**kwargs) + sig = inspect.signature(initmeth, bound_method=True) + return stringify_signature(sig, show_return_annotation=False, **kwargs) except TypeError: # still not possible: happens e.g. for old-style classes # with __init__ in C @@ -1283,9 +1284,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: # can never get arguments of a C function or method return None if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): - args = Signature(self.object, bound_method=False).format_args(**kwargs) + sig = inspect.signature(self.object, bound_method=False) else: - args = Signature(self.object, bound_method=True).format_args(**kwargs) + sig = inspect.signature(self.object, bound_method=True) + args = stringify_signature(sig, **kwargs) + # escape backslashes for reST args = args.replace('\\', '\\\\') return args diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index a06a79cea..81f2496cb 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -101,8 +101,8 @@ class GoogleDocstring: """ - _name_rgx = re.compile(r"^\s*((?::(?P\S+):)?`(?P[a-zA-Z0-9_.-]+)`|" - r" (?P[a-zA-Z0-9_.-]+))\s*", re.X) + _name_rgx = re.compile(r"^\s*((?::(?P\S+):)?`(?P~?[a-zA-Z0-9_.-]+)`|" + r" (?P~?[a-zA-Z0-9_.-]+))\s*", re.X) def __init__(self, docstring: Union[str, List[str]], config: SphinxConfig = None, app: Sphinx = None, what: str = '', name: str = '', diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 967a10d51..8d3392500 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -315,6 +315,108 @@ def is_builtin_class_method(obj: Any, attr_name: str) -> bool: return getattr(builtins, safe_getattr(cls, '__name__', '')) is cls +def signature(subject: Callable, bound_method: bool = False) -> inspect.Signature: + """Return a Signature object for the given *subject*. + + :param bound_method: Specify *subject* is a bound method or not + """ + # check subject is not a built-in class (ex. int, str) + if (isinstance(subject, type) and + is_builtin_class_method(subject, "__new__") and + is_builtin_class_method(subject, "__init__")): + raise TypeError("can't compute signature for built-in type {}".format(subject)) + + try: + signature = inspect.signature(subject) + parameters = list(signature.parameters.values()) + return_annotation = signature.return_annotation + except IndexError: + # Until python 3.6.4, cpython has been crashed on inspection for + # partialmethods not having any arguments. + # https://bugs.python.org/issue33009 + if hasattr(subject, '_partialmethod'): + parameters = [] + return_annotation = inspect.Parameter.empty + else: + raise + + try: + # Update unresolved annotations using ``get_type_hints()``. + annotations = typing.get_type_hints(subject) + for i, param in enumerate(parameters): + if isinstance(param.annotation, str) and param.name in annotations: + parameters[i] = param.replace(annotation=annotations[param.name]) + if 'return' in annotations: + return_annotation = annotations['return'] + except Exception: + # ``get_type_hints()`` does not support some kind of objects like partial, + # ForwardRef and so on. + pass + + if bound_method: + if inspect.ismethod(subject): + # ``inspect.signature()`` considers the subject is a bound method and removes + # first argument from signature. Therefore no skips are needed here. + pass + else: + if len(parameters) > 0: + parameters.pop(0) + + return inspect.Signature(parameters, return_annotation=return_annotation) + + +def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, + show_return_annotation: bool = True) -> str: + """Stringify a Signature object. + + :param show_annotation: Show annotation in result + """ + args = [] + last_kind = None + for param in sig.parameters.values(): + if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY: + # PEP-570: Separator for Positional Only Parameter: / + args.append('/') + elif param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD, + param.POSITIONAL_ONLY, + None): + # PEP-3102: Separator for Keyword Only Parameter: * + args.append('*') + + arg = StringIO() + if param.kind == param.VAR_POSITIONAL: + arg.write('*' + param.name) + elif param.kind == param.VAR_KEYWORD: + arg.write('**' + param.name) + else: + arg.write(param.name) + + if show_annotation and param.annotation is not param.empty: + arg.write(': ') + arg.write(stringify_annotation(param.annotation)) + if param.default is not param.empty: + if show_annotation and param.annotation is not param.empty: + arg.write(' = ') + else: + arg.write('=') + arg.write(object_description(param.default)) + + args.append(arg.getvalue()) + last_kind = param.kind + + if last_kind == inspect.Parameter.POSITIONAL_ONLY: + # PEP-570: Separator for Positional Only Parameter: / + args.append('/') + + if (sig.return_annotation is inspect.Parameter.empty or + show_annotation is False or + show_return_annotation is False): + return '(%s)' % ', '.join(args) + else: + annotation = stringify_annotation(sig.return_annotation) + return '(%s) -> %s' % (', '.join(args), annotation) + + class Parameter: """Fake parameter class for python2.""" POSITIONAL_ONLY = 0 @@ -342,6 +444,9 @@ class Signature: def __init__(self, subject: Callable, bound_method: bool = False, has_retval: bool = True) -> None: + warnings.warn('sphinx.util.inspect.Signature() is deprecated', + RemovedInSphinx40Warning) + # check subject is not a built-in class (ex. int, str) if (isinstance(subject, type) and is_builtin_class_method(subject, "__new__") and @@ -467,20 +572,14 @@ class Signature: def format_annotation(self, annotation: Any) -> str: """Return formatted representation of a type annotation.""" - warnings.warn('format_annotation() is deprecated', - RemovedInSphinx40Warning) return stringify_annotation(annotation) def format_annotation_new(self, annotation: Any) -> str: """format_annotation() for py37+""" - warnings.warn('format_annotation_new() is deprecated', - RemovedInSphinx40Warning) return stringify_annotation(annotation) def format_annotation_old(self, annotation: Any) -> str: """format_annotation() for py36 or below""" - warnings.warn('format_annotation_old() is deprecated', - RemovedInSphinx40Warning) return stringify_annotation(annotation) diff --git a/tests/roots/test-ext-autodoc/target/pep570.py b/tests/roots/test-ext-autodoc/target/pep570.py new file mode 100644 index 000000000..904692eeb --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/pep570.py @@ -0,0 +1,5 @@ +def foo(a, b, /, c, d): + pass + +def bar(a, b, /): + pass diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 35e0e5367..bd13cf6c2 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1318,13 +1318,13 @@ def test_partialmethod(app): ' refs: https://docs.python.jp/3/library/functools.html#functools.partialmethod', ' ', ' ', - ' .. py:method:: Cell.set_alive() -> None', + ' .. py:method:: Cell.set_alive()', ' :module: target.partialmethod', ' ', ' Make a cell alive.', ' ', ' ', - ' .. py:method:: Cell.set_dead() -> None', + ' .. py:method:: Cell.set_dead()', ' :module: target.partialmethod', ' ', ' Make a cell dead.', @@ -1336,11 +1336,6 @@ def test_partialmethod(app): ' Update state of cell to *state*.', ' ', ] - if (sys.version_info < (3, 5, 4) or - (3, 6, 5) <= sys.version_info < (3, 7) or - (3, 7, 0, 'beta', 3) <= sys.version_info): - # TODO: this condition should be updated after 3.7-final release. - expected = '\n'.join(expected).replace(' -> None', '').split('\n') options = {"members": None} actual = do_autodoc(app, 'class', 'target.partialmethod.Cell', options) diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 9547a9d2b..2ce754eff 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -479,6 +479,8 @@ Raises: If the dimensions couldn't be parsed. `InvalidArgumentsError` If the arguments are invalid. + :exc:`~ValueError` + If the arguments are wrong. """, """ Example Function @@ -488,6 +490,7 @@ Example Function :raises AttributeError: errors for missing attributes. :raises ~InvalidDimensionsError: If the dimensions couldn't be parsed. :raises InvalidArgumentsError: If the arguments are invalid. +:raises ~ValueError: If the arguments are wrong. """), ################################ (""" diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 2f4631965..5e035c6a9 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -17,6 +17,7 @@ import types import pytest from sphinx.util import inspect +from sphinx.util.inspect import stringify_signature def test_getargspec(): @@ -89,39 +90,39 @@ def test_getargspec_bound_methods(): assert expected_bound == inspect.getargspec(wrapped_bound_method) -def test_Signature(): +def test_signature(): # literals with pytest.raises(TypeError): - inspect.Signature(1) + inspect.signature(1) with pytest.raises(TypeError): - inspect.Signature('') + inspect.signature('') # builitin classes with pytest.raises(TypeError): - inspect.Signature(int) + inspect.signature(int) with pytest.raises(TypeError): - inspect.Signature(str) + inspect.signature(str) # normal function def func(a, b, c=1, d=2, *e, **f): pass - sig = inspect.Signature(func).format_args() + sig = inspect.stringify_signature(inspect.signature(func)) assert sig == '(a, b, c=1, d=2, *e, **f)' -def test_Signature_partial(): +def test_signature_partial(): def fun(a, b, c=1, d=2): pass p = functools.partial(fun, 10, c=11) - sig = inspect.Signature(p).format_args() - assert sig == '(b, *, c=11, d=2)' + sig = inspect.signature(p) + assert stringify_signature(sig) == '(b, *, c=11, d=2)' -def test_Signature_methods(): +def test_signature_methods(): class Foo: def meth1(self, arg1, **kwargs): pass @@ -139,36 +140,36 @@ def test_Signature_methods(): pass # unbound method - sig = inspect.Signature(Foo.meth1).format_args() - assert sig == '(self, arg1, **kwargs)' + sig = inspect.signature(Foo.meth1) + assert stringify_signature(sig) == '(self, arg1, **kwargs)' - sig = inspect.Signature(Foo.meth1, bound_method=True).format_args() - assert sig == '(arg1, **kwargs)' + sig = inspect.signature(Foo.meth1, bound_method=True) + assert stringify_signature(sig) == '(arg1, **kwargs)' # bound method - sig = inspect.Signature(Foo().meth1).format_args() - assert sig == '(arg1, **kwargs)' + sig = inspect.signature(Foo().meth1) + assert stringify_signature(sig) == '(arg1, **kwargs)' # class method - sig = inspect.Signature(Foo.meth2).format_args() - assert sig == '(arg1, *args, **kwargs)' + sig = inspect.signature(Foo.meth2) + assert stringify_signature(sig) == '(arg1, *args, **kwargs)' - sig = inspect.Signature(Foo().meth2).format_args() - assert sig == '(arg1, *args, **kwargs)' + sig = inspect.signature(Foo().meth2) + assert stringify_signature(sig) == '(arg1, *args, **kwargs)' # static method - sig = inspect.Signature(Foo.meth3).format_args() - assert sig == '(arg1, *args, **kwargs)' + sig = inspect.signature(Foo.meth3) + assert stringify_signature(sig) == '(arg1, *args, **kwargs)' - sig = inspect.Signature(Foo().meth3).format_args() - assert sig == '(arg1, *args, **kwargs)' + sig = inspect.signature(Foo().meth3) + assert stringify_signature(sig) == '(arg1, *args, **kwargs)' # wrapped bound method - sig = inspect.Signature(wrapped_bound_method).format_args() - assert sig == '(arg1, **kwargs)' + sig = inspect.signature(wrapped_bound_method) + assert stringify_signature(sig) == '(arg1, **kwargs)' -def test_Signature_partialmethod(): +def test_signature_partialmethod(): from functools import partialmethod class Foo: @@ -183,115 +184,129 @@ def test_Signature_partialmethod(): baz = partialmethod(meth2, 1, 2) subject = Foo() - sig = inspect.Signature(subject.foo).format_args() - assert sig == '(arg3=None, arg4=None)' + sig = inspect.signature(subject.foo) + assert stringify_signature(sig) == '(arg3=None, arg4=None)' - sig = inspect.Signature(subject.bar).format_args() - assert sig == '(arg2, *, arg3=3, arg4=None)' + sig = inspect.signature(subject.bar) + assert stringify_signature(sig) == '(arg2, *, arg3=3, arg4=None)' - sig = inspect.Signature(subject.baz).format_args() - assert sig == '()' + sig = inspect.signature(subject.baz) + assert stringify_signature(sig) == '()' -def test_Signature_annotations(): +def test_signature_annotations(): from typing_test_data import (f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, Node) # Class annotations - sig = inspect.Signature(f0).format_args() - assert sig == '(x: int, y: numbers.Integral) -> None' + sig = inspect.signature(f0) + assert stringify_signature(sig) == '(x: int, y: numbers.Integral) -> None' # Generic types with concrete parameters - sig = inspect.Signature(f1).format_args() - assert sig == '(x: List[int]) -> List[int]' + sig = inspect.signature(f1) + assert stringify_signature(sig) == '(x: List[int]) -> List[int]' # TypeVars and generic types with TypeVars - sig = inspect.Signature(f2).format_args() - assert sig == '(x: List[T], y: List[T_co], z: T) -> List[T_contra]' + sig = inspect.signature(f2) + assert stringify_signature(sig) == '(x: List[T], y: List[T_co], z: T) -> List[T_contra]' # Union types - sig = inspect.Signature(f3).format_args() - assert sig == '(x: Union[str, numbers.Integral]) -> None' + sig = inspect.signature(f3) + assert stringify_signature(sig) == '(x: Union[str, numbers.Integral]) -> None' # Quoted annotations - sig = inspect.Signature(f4).format_args() - assert sig == '(x: str, y: str) -> None' + sig = inspect.signature(f4) + assert stringify_signature(sig) == '(x: str, y: str) -> None' # Keyword-only arguments - sig = inspect.Signature(f5).format_args() - assert sig == '(x: int, *, y: str, z: str) -> None' + sig = inspect.signature(f5) + assert stringify_signature(sig) == '(x: int, *, y: str, z: str) -> None' # Keyword-only arguments with varargs - sig = inspect.Signature(f6).format_args() - assert sig == '(x: int, *args, y: str, z: str) -> None' + sig = inspect.signature(f6) + assert stringify_signature(sig) == '(x: int, *args, y: str, z: str) -> None' # Space around '=' for defaults - sig = inspect.Signature(f7).format_args() - assert sig == '(x: int = None, y: dict = {}) -> None' + sig = inspect.signature(f7) + assert stringify_signature(sig) == '(x: int = None, y: dict = {}) -> None' # Callable types - sig = inspect.Signature(f8).format_args() - assert sig == '(x: Callable[[int, str], int]) -> None' + sig = inspect.signature(f8) + assert stringify_signature(sig) == '(x: Callable[[int, str], int]) -> None' - sig = inspect.Signature(f9).format_args() - assert sig == '(x: Callable) -> None' + sig = inspect.signature(f9) + assert stringify_signature(sig) == '(x: Callable) -> None' # Tuple types - sig = inspect.Signature(f10).format_args() - assert sig == '(x: Tuple[int, str], y: Tuple[int, ...]) -> None' + sig = inspect.signature(f10) + assert stringify_signature(sig) == '(x: Tuple[int, str], y: Tuple[int, ...]) -> None' # Instance annotations - sig = inspect.Signature(f11).format_args() - assert sig == '(x: CustomAnnotation, y: 123) -> None' - - # has_retval=False - sig = inspect.Signature(f11, has_retval=False).format_args() - assert sig == '(x: CustomAnnotation, y: 123)' + sig = inspect.signature(f11) + assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None' # tuple with more than two items - sig = inspect.Signature(f12).format_args() - assert sig == '() -> Tuple[int, str, int]' + sig = inspect.signature(f12) + assert stringify_signature(sig) == '() -> Tuple[int, str, int]' # optional - sig = inspect.Signature(f13).format_args() - assert sig == '() -> Optional[str]' + sig = inspect.signature(f13) + assert stringify_signature(sig) == '() -> Optional[str]' # Any - sig = inspect.Signature(f14).format_args() - assert sig == '() -> Any' + sig = inspect.signature(f14) + assert stringify_signature(sig) == '() -> Any' # ForwardRef - sig = inspect.Signature(f15).format_args() - assert sig == '(x: Unknown, y: int) -> Any' + sig = inspect.signature(f15) + assert stringify_signature(sig) == '(x: Unknown, y: int) -> Any' # keyword only arguments (1) - sig = inspect.Signature(f16).format_args() - assert sig == '(arg1, arg2, *, arg3=None, arg4=None)' + sig = inspect.signature(f16) + assert stringify_signature(sig) == '(arg1, arg2, *, arg3=None, arg4=None)' # keyword only arguments (2) - sig = inspect.Signature(f17).format_args() - assert sig == '(*, arg3, arg4)' + sig = inspect.signature(f17) + assert stringify_signature(sig) == '(*, arg3, arg4)' - sig = inspect.Signature(f18).format_args() - assert sig == '(self, arg1: Union[int, Tuple] = 10) -> List[Dict]' + sig = inspect.signature(f18) + assert stringify_signature(sig) == '(self, arg1: Union[int, Tuple] = 10) -> List[Dict]' # annotations for variadic and keyword parameters - sig = inspect.Signature(f19).format_args() - assert sig == '(*args: int, **kwargs: str)' + sig = inspect.signature(f19) + assert stringify_signature(sig) == '(*args: int, **kwargs: str)' # type hints by string - sig = inspect.Signature(Node.children).format_args() + sig = inspect.signature(Node.children) if (3, 5, 0) <= sys.version_info < (3, 5, 3): - assert sig == '(self) -> List[Node]' + assert stringify_signature(sig) == '(self) -> List[Node]' else: - assert sig == '(self) -> List[typing_test_data.Node]' + assert stringify_signature(sig) == '(self) -> List[typing_test_data.Node]' - sig = inspect.Signature(Node.__init__).format_args() - assert sig == '(self, parent: Optional[Node]) -> None' + sig = inspect.signature(Node.__init__) + assert stringify_signature(sig) == '(self, parent: Optional[Node]) -> None' # show_annotation is False - sig = inspect.Signature(f7).format_args(show_annotation=False) - assert sig == '(x=None, y={})' + sig = inspect.signature(f7) + assert stringify_signature(sig, show_annotation=False) == '(x=None, y={})' + + # show_return_annotation is False + sig = inspect.signature(f7) + assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})' + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +@pytest.mark.sphinx(testroot='ext-autodoc') +def test_signature_annotations_py38(app): + from target.pep570 import foo, bar + + # case: separator in the middle + sig = inspect.signature(foo) + assert stringify_signature(sig) == '(a, b, /, c, d)' + + # case: separator at tail + sig = inspect.signature(bar) + assert stringify_signature(sig) == '(a, b, /)' def test_safe_getattr_with_default():