Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,10 +1162,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
try:
# In some cases fetching a signature is not possible.
# But, we surely should not fail in this case.
text_sig = str(inspect.signature(cls)).replace(' -> None', '')
doc = '\n'.join(cls.__name__ + str(sig).replace(' -> None', '')
for sig in inspect.signatures(cls))
except (TypeError, ValueError):
text_sig = ''
cls.__doc__ = (cls.__name__ + text_sig)
doc = cls.__name__
cls.__doc__ = doc

if match_args:
# I could probably compute this once.
Expand Down
17 changes: 11 additions & 6 deletions Lib/idlelib/calltip.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,20 +170,25 @@ def get_argspec(ob):

# Initialize argspec and wrap it to get lines.
try:
argspec = str(inspect.signature(fob))
signatures = inspect.signatures(fob)
except Exception as err:
msg = str(err)
if msg.startswith(_invalid_method):
return _invalid_method
else:
argspec = ''
signatures = []

if isinstance(fob, type) and argspec == '()':
lines = []
for sig in signatures:
line = str(sig)
if len(line) > _MAX_COLS:
lines.extend(textwrap.wrap(line, _MAX_COLS, subsequent_indent=_INDENT))
else:
lines.append(line)
if isinstance(fob, type) and lines == ['()']:
# If fob has no argument, use default callable argspec.
argspec = _default_callable_argspec
lines = [_default_callable_argspec]

lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT)
if len(argspec) > _MAX_COLS else [argspec] if argspec else [])

# Augment lines from docstring, if any, and join to get argspec.
doc = inspect.getdoc(ob)
Expand Down
32 changes: 32 additions & 0 deletions Lib/idlelib/idle_test/test_calltip.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from idlelib import calltip
import unittest
from unittest.mock import Mock
import builtins
import textwrap
import types
import re
Expand Down Expand Up @@ -151,17 +152,48 @@ def f(): pass
"Signature information for builtins requires docstrings")
def test_multiline_docstring(self):
# Test fewer lines than max.
def range():
"""range(stop) -> range object
range(start, stop[, step]) -> range object

Return an object that produces a sequence of integers from start
(inclusive) to stop (exclusive) by step.
"""
range.__text_signature__ = '<no signature>'
self.assertEqual(get_spec(range),
"range(stop) -> range object\n"
"range(start, stop[, step]) -> range object")
self.assertEqual(get_spec(builtins.range),
"(stop, /)\n"
"(start, stop, step=1, /)\n"
"Create a range object.")

# Test max lines
def bytes():
"""bytes(iterable_of_ints) -> bytes
bytes(string, encoding[, errors]) -> bytes
bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
bytes(int) -> bytes object of size given by the parameter initialized with null bytes
bytes() -> empty bytes object

Construct an immutable array of bytes from:
- an iterable yielding integers in range(256)
- a text string encoded using the specified encoding
- any object implementing the buffer API.
- an integer"""
bytes.__text_signature__ = '<no signature>'
self.assertEqual(get_spec(bytes), '''\
bytes(iterable_of_ints) -> bytes
bytes(string, encoding[, errors]) -> bytes
bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
bytes(int) -> bytes object of size given by the parameter initialized with null bytes
bytes() -> empty bytes object''')
self.assertEqual(get_spec(builtins.bytes), '''\
()
(source)
(source, encoding)
(source, encoding, errors)
Construct an immutable array of bytes.''')

def test_multiline_docstring_2(self):
# Test more than max lines
Expand Down
151 changes: 129 additions & 22 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"GEN_CREATED",
"GEN_RUNNING",
"GEN_SUSPENDED",
"MultiSignature",
"Parameter",
"Signature",
"TPFLAGS_IS_ABSTRACT",
Expand Down Expand Up @@ -134,6 +135,7 @@
"istraceback",
"markcoroutinefunction",
"signature",
"signatures",
"stack",
"trace",
"unwrap",
Expand Down Expand Up @@ -2189,19 +2191,20 @@ def _signature_is_functionlike(obj):
(isinstance(annotations, (dict)) or annotations is None) )


def _signature_strip_non_python_syntax(signature):
def _signature_split(signature):
"""
Private helper function. Takes a signature in Argument Clinic's
extended signature format.

Returns a tuple of two things:
Yields pairs:
* that signature re-rendered in standard Python syntax, and
* the index of the "self" parameter (generally 0), or None if
the function does not have a "self" parameter.
"""

if not signature:
return signature, None
yield (signature, None)
return

self_parameter = None

Expand All @@ -2214,38 +2217,47 @@ def _signature_strip_non_python_syntax(signature):

current_parameter = 0
OP = token.OP
ERRORTOKEN = token.ERRORTOKEN
NEWLINE = token.NEWLINE
NL = token.NL

# token stream always starts with ENCODING token, skip it
t = next(token_stream)
assert t.type == tokenize.ENCODING

for t in token_stream:
type, string = t.type, t.string

if type == OP:
if type == NEWLINE:
yield (''.join(text), self_parameter)
text.clear()
self_parameter = None
current_parameter = 0
continue
elif type == NL:
continue
elif type == OP:
if string == ',':
current_parameter += 1

if (type == OP) and (string == '$'):
assert self_parameter is None
self_parameter = current_parameter
continue

string = ', '
elif string == '$' and self_parameter is None:
self_parameter = current_parameter
continue
add(string)
if (string == ','):
add(' ')
clean_signature = ''.join(text).strip().replace("\n", "")
return clean_signature, self_parameter


def _signature_fromstr(cls, obj, s, skip_bound_arg=True):
"""Private helper to parse content of '__text_signature__'
and return a Signature based on it.
"""
Parameter = cls._parameter_cls
signatures = [_signature_fromstr1(cls, obj,
clean_signature, self_parameter,
skip_bound_arg)
for clean_signature, self_parameter in _signature_split(s)]
if len(signatures) == 1:
return signatures[0]
else:
return MultiSignature(signatures)

clean_signature, self_parameter = _signature_strip_non_python_syntax(s)
def _signature_fromstr1(cls, obj, clean_signature, self_parameter, skip_bound_arg):
Parameter = cls._parameter_cls

program = "def foo" + clean_signature + ": pass"

Expand Down Expand Up @@ -2830,6 +2842,8 @@ def replace(self, *, name=_void, kind=_void,

return type(self)(name, kind, default=default, annotation=annotation)

__replace__ = replace

def __str__(self):
kind = self.kind
formatted = self._name
Expand All @@ -2852,8 +2866,6 @@ def __str__(self):

return formatted

__replace__ = replace

def __repr__(self):
return '<{} "{}">'.format(self.__class__.__name__, self)

Expand Down Expand Up @@ -3133,7 +3145,7 @@ def __hash__(self):
def __eq__(self, other):
if self is other:
return True
if not isinstance(other, Signature):
if not isinstance(other, Signature) or isinstance(other, MultiSignature):
return NotImplemented
return self._hash_basis() == other._hash_basis()

Expand Down Expand Up @@ -3355,11 +3367,106 @@ def format(self, *, max_width=None):
return rendered


class MultiSignature(Signature):
__slots__ = ('_signatures',)

def __init__(self, signatures):
signatures = tuple(signatures)
if not signatures:
raise ValueError('No signatures')
self._signatures = signatures

@staticmethod
def from_callable(obj, *,
follow_wrapped=True, globals=None, locals=None, eval_str=False):
"""Constructs MultiSignature for the given callable object."""
signature = Signature.from_callable(obj, follow_wrapped=follow_wrapped,
globals=globals, locals=locals,
eval_str=eval_str)
if not isinstance(signature, MultiSignature):
signature = MultiSignature((signature,))
return signature

@property
def parameters(self):
try:
return self._parameters
except AttributeError:
pass
params = {}
for s in self._signatures:
params.update(s.parameters)
self._parameters = types.MappingProxyType(params)
return self._parameters

@property
def return_annotation(self):
try:
return self._return_annotation
except AttributeError:
pass
self._return_annotation = types.UnionType(tuple(s.return_annotation
for s in self._signatures
if s.return_annotation != _empty))
return self._return_annotation

def replace(self):
raise NotImplementedError

__replace__ = replace

def __iter__(self):
return iter(self._signatures)

def __hash__(self):
if len(self._signatures) == 1:
return hash(self._signatures[0])
return hash(self._signatures)

def __eq__(self, other):
if self is other:
return True
if isinstance(other, MultiSignature):
return self._signatures == other._signatures
if len(self._signatures) == 1 and isinstance(other, Signature):
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be

Suggested change
if len(self._signatures) == 1 and isinstance(other, Signature):
if isinstance(other, Signature):
return len(self._signatures) == 1

and the addition to Signature's eq should be removed, above.

Copy link
Member Author

Choose a reason for hiding this comment

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

No, it is not correct, it would make a 1-element MultiSignature equal to any Signature.

You perhaps mistyped, I wrote more correct fix.

Copy link
Contributor

Choose a reason for hiding this comment

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

I did. That's good. (I would suggest removing the line about MultiSignature in Signature's eq as I believe it's dead code now, but up to you).

Copy link
Member Author

Choose a reason for hiding this comment

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

It is not dead. It is called in SignatureSubclass() == MultiSignature(). Although it is not covered by tests yet.

return self._signatures[0] == other
return NotImplemented

def _bind(self, args, kwargs, *, partial=False):
"""Private method. Don't use directly."""
for i, s in enumerate(self._signatures):
try:
return s._bind(args, kwargs, partial=partial)
except TypeError:
if i == len(self._signatures) - 1:
raise

def __reduce__(self):
return type(self), (self._signatures,)

def __repr__(self):
return '<%s %s>' % (self.__class__.__name__,
'|'.join(map(str, self._signatures)))

def __str__(self):
return '\n'.join(map(str, self._signatures))

def format(self, *, max_width=None):
return '\n'.join(sig.format(max_width=max_width)
for sig in self._signatures)


def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False):
"""Get a signature object for the passed callable."""
return Signature.from_callable(obj, follow_wrapped=follow_wrapped,
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that the original Signature.from_callable is not changed and does the exact same call to _from_callable, that means both signature and Signature.from_callable may return a MultiSignature object, right ?

Strictly speaking this is not a violation of backwards compatibility as MultiSignature is a subclass of Signature, but I find it weird and it may break some strict-type behaviors. Is there a way to have those only return pure Signature objects, without breaking everything this update brings to the table ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it is. The idea was that inspect.signature() will start to support previously not supported cases and return a MultiSignature which should be compatible with Signature. But now I am no longer sure. They are not so well compatible as I expected, and in most cases I needed to rewrite the code to explicitly handle MultiSignature. This is why I added inspect.signatures(). Now the code is in the intermediate state -- either MultiSignature will be made more compatible with Signature and inspect.signatures() may disappear, or they will became completely different things.

globals=globals, locals=locals, eval_str=eval_str)

def signatures(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False):
"""Get a multi-signature object for the passed callable."""
return MultiSignature.from_callable(obj, follow_wrapped=follow_wrapped,
globals=globals, locals=locals,
eval_str=eval_str)


class BufferFlags(enum.IntFlag):
SIMPLE = 0x0
Expand Down
Loading