3939import re
4040import sys
4141import tokenize
42+ import token
4243import types
4344import warnings
4445import functools
@@ -1648,25 +1649,88 @@ def _signature_get_bound_param(spec):
16481649 return spec [2 :pos ]
16491650
16501651
1652+ def _signature_strip_non_python_syntax (signature ):
1653+ """
1654+ Takes a signature in Argument Clinic's extended signature format.
1655+ Returns a tuple of three things:
1656+ * that signature re-rendered in standard Python syntax,
1657+ * the index of the "self" parameter (generally 0), or None if
1658+ the function does not have a "self" parameter, and
1659+ * the index of the last "positional only" parameter,
1660+ or None if the signature has no positional-only parameters.
1661+ """
1662+
1663+ if not signature :
1664+ return signature , None , None
1665+
1666+ self_parameter = None
1667+ last_positional_only = None
1668+
1669+ lines = [l .encode ('ascii' ) for l in signature .split ('\n ' )]
1670+ generator = iter (lines ).__next__
1671+ token_stream = tokenize .tokenize (generator )
1672+
1673+ delayed_comma = False
1674+ skip_next_comma = False
1675+ text = []
1676+ add = text .append
1677+
1678+ current_parameter = 0
1679+ OP = token .OP
1680+ ERRORTOKEN = token .ERRORTOKEN
1681+
1682+ # token stream always starts with ENCODING token, skip it
1683+ t = next (token_stream )
1684+ assert t .type == tokenize .ENCODING
1685+
1686+ for t in token_stream :
1687+ type , string = t .type , t .string
1688+
1689+ if type == OP :
1690+ if string == ',' :
1691+ if skip_next_comma :
1692+ skip_next_comma = False
1693+ else :
1694+ assert not delayed_comma
1695+ delayed_comma = True
1696+ current_parameter += 1
1697+ continue
1698+
1699+ if string == '/' :
1700+ assert not skip_next_comma
1701+ assert last_positional_only is None
1702+ skip_next_comma = True
1703+ last_positional_only = current_parameter - 1
1704+ continue
1705+
1706+ if (type == ERRORTOKEN ) and (string == '$' ):
1707+ assert self_parameter is None
1708+ self_parameter = current_parameter
1709+ continue
1710+
1711+ if delayed_comma :
1712+ delayed_comma = False
1713+ if not ((type == OP ) and (string == ')' )):
1714+ add (', ' )
1715+ add (string )
1716+ if (string == ',' ):
1717+ add (' ' )
1718+ clean_signature = '' .join (text )
1719+ return clean_signature , self_parameter , last_positional_only
1720+
1721+
16511722def _signature_fromstr (cls , obj , s ):
16521723 # Internal helper to parse content of '__text_signature__'
16531724 # and return a Signature based on it
16541725 Parameter = cls ._parameter_cls
16551726
1656- if s .endswith ("/)" ):
1657- kind = Parameter .POSITIONAL_ONLY
1658- s = s [:- 2 ] + ')'
1659- else :
1660- kind = Parameter .POSITIONAL_OR_KEYWORD
1661-
1662- first_parameter_is_self = s .startswith ("($" )
1663- if first_parameter_is_self :
1664- s = '(' + s [2 :]
1727+ clean_signature , self_parameter , last_positional_only = \
1728+ _signature_strip_non_python_syntax (s )
16651729
1666- s = "def foo" + s + ": pass"
1730+ program = "def foo" + clean_signature + ": pass"
16671731
16681732 try :
1669- module = ast .parse (s )
1733+ module = ast .parse (program )
16701734 except SyntaxError :
16711735 module = None
16721736
@@ -1750,8 +1814,14 @@ def p(name_node, default_node, default=empty):
17501814 args = reversed (f .args .args )
17511815 defaults = reversed (f .args .defaults )
17521816 iter = itertools .zip_longest (args , defaults , fillvalue = None )
1753- for name , default in reversed (list (iter )):
1817+ if last_positional_only is not None :
1818+ kind = Parameter .POSITIONAL_ONLY
1819+ else :
1820+ kind = Parameter .POSITIONAL_OR_KEYWORD
1821+ for i , (name , default ) in enumerate (reversed (list (iter ))):
17541822 p (name , default )
1823+ if i == last_positional_only :
1824+ kind = Parameter .POSITIONAL_OR_KEYWORD
17551825
17561826 # *args
17571827 if f .args .vararg :
@@ -1768,7 +1838,7 @@ def p(name_node, default_node, default=empty):
17681838 kind = Parameter .VAR_KEYWORD
17691839 p (f .args .kwarg , empty )
17701840
1771- if first_parameter_is_self :
1841+ if self_parameter is not None :
17721842 assert parameters
17731843 if getattr (obj , '__self__' , None ):
17741844 # strip off self, it's already been bound
@@ -1861,12 +1931,13 @@ def signature(obj):
18611931 # At this point we know, that `obj` is a class, with no user-
18621932 # defined '__init__', '__new__', or class-level '__call__'
18631933
1864- for base in obj .__mro__ :
1934+ for base in obj .__mro__ [: - 1 ] :
18651935 # Since '__text_signature__' is implemented as a
18661936 # descriptor that extracts text signature from the
18671937 # class docstring, if 'obj' is derived from a builtin
18681938 # class, its own '__text_signature__' may be 'None'.
1869- # Therefore, we go through the MRO to find the first
1939+ # Therefore, we go through the MRO (except the last
1940+ # class in there, which is 'object') to find the first
18701941 # class with non-empty text signature.
18711942 try :
18721943 text_sig = base .__text_signature__
@@ -1881,13 +1952,7 @@ def signature(obj):
18811952 # No '__text_signature__' was found for the 'obj' class.
18821953 # Last option is to check if its '__init__' is
18831954 # object.__init__ or type.__init__.
1884- if type in obj .__mro__ :
1885- # 'obj' is a metaclass without user-defined __init__
1886- # or __new__.
1887- if obj .__init__ is type .__init__ :
1888- # Return a signature of 'type' builtin.
1889- return signature (type )
1890- else :
1955+ if type not in obj .__mro__ :
18911956 # We have a class (not metaclass), but no user-defined
18921957 # __init__ or __new__ for it
18931958 if obj .__init__ is object .__init__ :
@@ -1901,7 +1966,11 @@ def signature(obj):
19011966 # infinite recursion (and even potential segfault)
19021967 call = _signature_get_user_defined_method (type (obj ), '__call__' )
19031968 if call is not None :
1904- sig = signature (call )
1969+ try :
1970+ sig = signature (call )
1971+ except ValueError as ex :
1972+ msg = 'no signature found for {!r}' .format (obj )
1973+ raise ValueError (msg ) from ex
19051974
19061975 if sig is not None :
19071976 # For classes and objects we skip the first parameter of their
0 commit comments