inspect.signature: Use '/' to separate positional-only parameters from

the rest in Signature.__str__. #20356
This commit is contained in:
Yury Selivanov 2014-01-27 15:07:58 -05:00
parent ea2d66e68a
commit 2393dca472
4 changed files with 63 additions and 42 deletions

View File

@ -510,9 +510,8 @@ function.
.. attribute:: Parameter.name .. attribute:: Parameter.name
The name of the parameter as a string. Must be a valid python identifier The name of the parameter as a string. The name must be a valid
name (with the exception of ``POSITIONAL_ONLY`` parameters, which can have Python identifier.
it set to ``None``).
.. attribute:: Parameter.default .. attribute:: Parameter.default
@ -596,6 +595,10 @@ function.
>>> str(param.replace(default=Parameter.empty, annotation='spam')) >>> str(param.replace(default=Parameter.empty, annotation='spam'))
"foo:'spam'" "foo:'spam'"
.. versionchanged:: 3.4
In Python 3.3 Parameter objects were allowed to have ``name`` set
to ``None`` if their ``kind`` was set to ``POSITIONAL_ONLY``.
This is no longer permitted.
.. class:: BoundArguments .. class:: BoundArguments

View File

@ -1488,6 +1488,9 @@ removed:
* Support for loading the deprecated ``TYPE_INT64`` has been removed from * Support for loading the deprecated ``TYPE_INT64`` has been removed from
:mod:`marshal`. (Contributed by Dan Riti in :issue:`15480`.) :mod:`marshal`. (Contributed by Dan Riti in :issue:`15480`.)
* :class:`inspect.Signature`: positional-only parameters are now required
to have a valid name.
Code Cleanups Code Cleanups
------------- -------------

View File

@ -1629,16 +1629,15 @@ class Parameter:
self._default = default self._default = default
self._annotation = annotation self._annotation = annotation
if name is None: if name is _empty:
if kind != _POSITIONAL_ONLY: raise ValueError('name is a required attribute for Parameter')
raise ValueError("None is not a valid name for a "
"non-positional-only parameter") if not isinstance(name, str):
self._name = name raise TypeError("name must be a str, not a {!r}".format(name))
else:
name = str(name) if not name.isidentifier():
if kind != _POSITIONAL_ONLY and not name.isidentifier(): raise ValueError('{!r} is not a valid parameter name'.format(name))
msg = '{!r} is not a valid parameter name'.format(name)
raise ValueError(msg)
self._name = name self._name = name
self._partial_kwarg = _partial_kwarg self._partial_kwarg = _partial_kwarg
@ -1683,12 +1682,7 @@ class Parameter:
def __str__(self): def __str__(self):
kind = self.kind kind = self.kind
formatted = self._name formatted = self._name
if kind == _POSITIONAL_ONLY:
if formatted is None:
formatted = ''
formatted = '<{}>'.format(formatted)
# Add annotation and default value # Add annotation and default value
if self._annotation is not _empty: if self._annotation is not _empty:
@ -1858,21 +1852,19 @@ class Signature:
for idx, param in enumerate(parameters): for idx, param in enumerate(parameters):
kind = param.kind kind = param.kind
name = param.name
if kind < top_kind: if kind < top_kind:
msg = 'wrong parameter order: {} before {}' msg = 'wrong parameter order: {} before {}'
msg = msg.format(top_kind, param.kind) msg = msg.format(top_kind, kind)
raise ValueError(msg) raise ValueError(msg)
else: else:
top_kind = kind top_kind = kind
name = param.name
if name is None:
name = str(idx)
param = param.replace(name=name)
if name in params: if name in params:
msg = 'duplicate parameter name: {!r}'.format(name) msg = 'duplicate parameter name: {!r}'.format(name)
raise ValueError(msg) raise ValueError(msg)
params[name] = param params[name] = param
else: else:
params = OrderedDict(((param.name, param) params = OrderedDict(((param.name, param)
@ -2292,11 +2284,21 @@ class Signature:
def __str__(self): def __str__(self):
result = [] result = []
render_pos_only_separator = False
render_kw_only_separator = True render_kw_only_separator = True
for idx, param in enumerate(self.parameters.values()): for param in self.parameters.values():
formatted = str(param) formatted = str(param)
kind = param.kind kind = param.kind
if kind == _POSITIONAL_ONLY:
render_pos_only_separator = True
elif render_pos_only_separator:
# It's not a positional-only parameter, and the flag
# is set to 'True' (there were pos-only params before.)
result.append('/')
render_pos_only_separator = False
if kind == _VAR_POSITIONAL: if kind == _VAR_POSITIONAL:
# OK, we have an '*args'-like parameter, so we won't need # OK, we have an '*args'-like parameter, so we won't need
# a '*' to separate keyword-only arguments # a '*' to separate keyword-only arguments
@ -2312,6 +2314,11 @@ class Signature:
result.append(formatted) result.append(formatted)
if render_pos_only_separator:
# There were only positional-only parameters, hence the
# flag was not reset to 'False'
result.append('/')
rendered = '({})'.format(', '.join(result)) rendered = '({})'.format(', '.join(result))
if self.return_annotation is not _empty: if self.return_annotation is not _empty:

View File

@ -2122,6 +2122,7 @@ class TestSignatureObject(unittest.TestCase):
def test_signature_str_positional_only(self): def test_signature_str_positional_only(self):
P = inspect.Parameter P = inspect.Parameter
S = inspect.Signature
def test(a_po, *, b, **kwargs): def test(a_po, *, b, **kwargs):
return a_po, kwargs return a_po, kwargs
@ -2132,14 +2133,20 @@ class TestSignatureObject(unittest.TestCase):
test.__signature__ = sig.replace(parameters=new_params) test.__signature__ = sig.replace(parameters=new_params)
self.assertEqual(str(inspect.signature(test)), self.assertEqual(str(inspect.signature(test)),
'(<a_po>, *, b, **kwargs)') '(a_po, /, *, b, **kwargs)')
sig = inspect.signature(test) self.assertEqual(str(S(parameters=[P('foo', P.POSITIONAL_ONLY)])),
new_params = list(sig.parameters.values()) '(foo, /)')
new_params[0] = new_params[0].replace(name=None)
test.__signature__ = sig.replace(parameters=new_params) self.assertEqual(str(S(parameters=[
self.assertEqual(str(inspect.signature(test)), P('foo', P.POSITIONAL_ONLY),
'(<0>, *, b, **kwargs)') P('bar', P.VAR_KEYWORD)])),
'(foo, /, **bar)')
self.assertEqual(str(S(parameters=[
P('foo', P.POSITIONAL_ONLY),
P('bar', P.VAR_POSITIONAL)])),
'(foo, /, *bar)')
def test_signature_replace_anno(self): def test_signature_replace_anno(self):
def test() -> 42: def test() -> 42:
@ -2178,10 +2185,13 @@ class TestParameterObject(unittest.TestCase):
with self.assertRaisesRegex(ValueError, 'not a valid parameter name'): with self.assertRaisesRegex(ValueError, 'not a valid parameter name'):
inspect.Parameter('1', kind=inspect.Parameter.VAR_KEYWORD) inspect.Parameter('1', kind=inspect.Parameter.VAR_KEYWORD)
with self.assertRaisesRegex(ValueError, with self.assertRaisesRegex(TypeError, 'name must be a str'):
'non-positional-only parameter'):
inspect.Parameter(None, kind=inspect.Parameter.VAR_KEYWORD) inspect.Parameter(None, kind=inspect.Parameter.VAR_KEYWORD)
with self.assertRaisesRegex(ValueError,
'is not a valid parameter name'):
inspect.Parameter('$', kind=inspect.Parameter.VAR_KEYWORD)
with self.assertRaisesRegex(ValueError, 'cannot have default values'): with self.assertRaisesRegex(ValueError, 'cannot have default values'):
inspect.Parameter('a', default=42, inspect.Parameter('a', default=42,
kind=inspect.Parameter.VAR_KEYWORD) kind=inspect.Parameter.VAR_KEYWORD)
@ -2230,7 +2240,8 @@ class TestParameterObject(unittest.TestCase):
self.assertEqual(p2.name, 'bar') self.assertEqual(p2.name, 'bar')
self.assertNotEqual(p2, p) self.assertNotEqual(p2, p)
with self.assertRaisesRegex(ValueError, 'not a valid parameter name'): with self.assertRaisesRegex(ValueError,
'name is a required attribute'):
p2 = p2.replace(name=p2.empty) p2 = p2.replace(name=p2.empty)
p2 = p2.replace(name='foo', default=None) p2 = p2.replace(name='foo', default=None)
@ -2252,14 +2263,11 @@ class TestParameterObject(unittest.TestCase):
self.assertEqual(p2, p) self.assertEqual(p2, p)
def test_signature_parameter_positional_only(self): def test_signature_parameter_positional_only(self):
p = inspect.Parameter(None, kind=inspect.Parameter.POSITIONAL_ONLY) with self.assertRaisesRegex(TypeError, 'name must be a str'):
self.assertEqual(str(p), '<>') inspect.Parameter(None, kind=inspect.Parameter.POSITIONAL_ONLY)
p = p.replace(name='1')
self.assertEqual(str(p), '<1>')
def test_signature_parameter_immutability(self): def test_signature_parameter_immutability(self):
p = inspect.Parameter(None, kind=inspect.Parameter.POSITIONAL_ONLY) p = inspect.Parameter('spam', kind=inspect.Parameter.KEYWORD_ONLY)
with self.assertRaises(AttributeError): with self.assertRaises(AttributeError):
p.foo = 'bar' p.foo = 'bar'