Skip to content

Commit f5f1952

Browse files
committed
Update inspect module and related tests
1 parent 588a3d1 commit f5f1952

File tree

5 files changed

+134
-42
lines changed

5 files changed

+134
-42
lines changed

Lib/inspect.py

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ def iscode(object):
272272
| 16=nested | 32=generator | 64=nofree | 128=coroutine
273273
| 256=iterable_coroutine | 512=async_generator
274274
co_freevars tuple of names of free variables
275+
co_posonlyargcount number of positional only arguments
275276
co_kwonlyargcount number of keyword only arguments (not including ** arg)
276277
co_lnotab encoded mapping of line numbers to bytecode indices
277278
co_name name with which this code object was defined
@@ -1031,8 +1032,8 @@ def getargs(co):
10311032
'args' is the list of argument names. Keyword-only arguments are
10321033
appended. 'varargs' and 'varkw' are the names of the * and **
10331034
arguments or None."""
1034-
args, varargs, kwonlyargs, varkw = _getfullargs(co)
1035-
return Arguments(args + kwonlyargs, varargs, varkw)
1035+
args, varargs, posonlyargs, kwonlyargs, varkw = _getfullargs(co)
1036+
return Arguments(posonlyargs + args + kwonlyargs, varargs, varkw)
10361037

10371038
def _getfullargs(co):
10381039
"""Get information about the arguments accepted by a code object.
@@ -1044,13 +1045,17 @@ def _getfullargs(co):
10441045
if not iscode(co):
10451046
raise TypeError('{!r} is not a code object'.format(co))
10461047

1047-
nargs = co.co_argcount
10481048
names = co.co_varnames
1049+
nargs = co.co_argcount
1050+
nposonlyargs = co.co_posonlyargcount
10491051
nkwargs = co.co_kwonlyargcount
1050-
args = list(names[:nargs])
1051-
kwonlyargs = list(names[nargs:nargs+nkwargs])
1052+
nposargs = nargs + nposonlyargs
1053+
posonlyargs = list(names[:nposonlyargs])
1054+
args = list(names[nposonlyargs:nposonlyargs+nargs])
1055+
kwonlyargs = list(names[nposargs:nposargs+nkwargs])
10521056
step = 0
10531057

1058+
nargs += nposonlyargs
10541059
nargs += nkwargs
10551060
varargs = None
10561061
if co.co_flags & CO_VARARGS:
@@ -1059,7 +1064,7 @@ def _getfullargs(co):
10591064
varkw = None
10601065
if co.co_flags & CO_VARKEYWORDS:
10611066
varkw = co.co_varnames[nargs]
1062-
return args, varargs, kwonlyargs, varkw
1067+
return args, varargs, posonlyargs, kwonlyargs, varkw
10631068

10641069

10651070
ArgSpec = namedtuple('ArgSpec', 'args varargs keywords defaults')
@@ -1087,15 +1092,16 @@ def getargspec(func):
10871092
warnings.warn("inspect.getargspec() is deprecated since Python 3.0, "
10881093
"use inspect.signature() or inspect.getfullargspec()",
10891094
DeprecationWarning, stacklevel=2)
1090-
args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, ann = \
1091-
getfullargspec(func)
1092-
if kwonlyargs or ann:
1093-
raise ValueError("Function has keyword-only parameters or annotations"
1094-
", use getfullargspec() API which can support them")
1095+
args, varargs, varkw, defaults, posonlyargs, kwonlyargs, \
1096+
kwonlydefaults, ann = getfullargspec(func)
1097+
if posonlyargs or kwonlyargs or ann:
1098+
raise ValueError("Function has positional-only, keyword-only parameters"
1099+
" or annotations, use getfullargspec() API which can"
1100+
" support them")
10951101
return ArgSpec(args, varargs, varkw, defaults)
10961102

10971103
FullArgSpec = namedtuple('FullArgSpec',
1098-
'args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations')
1104+
'args, varargs, varkw, defaults, posonlyargs, kwonlyargs, kwonlydefaults, annotations')
10991105

11001106
def getfullargspec(func):
11011107
"""Get the names and default values of a callable object's parameters.
@@ -1145,6 +1151,7 @@ def getfullargspec(func):
11451151
args = []
11461152
varargs = None
11471153
varkw = None
1154+
posonlyargs = []
11481155
kwonlyargs = []
11491156
defaults = ()
11501157
annotations = {}
@@ -1159,7 +1166,9 @@ def getfullargspec(func):
11591166
name = param.name
11601167

11611168
if kind is _POSITIONAL_ONLY:
1162-
args.append(name)
1169+
posonlyargs.append(name)
1170+
if param.default is not param.empty:
1171+
defaults += (param.default,)
11631172
elif kind is _POSITIONAL_OR_KEYWORD:
11641173
args.append(name)
11651174
if param.default is not param.empty:
@@ -1185,7 +1194,7 @@ def getfullargspec(func):
11851194
defaults = None
11861195

11871196
return FullArgSpec(args, varargs, varkw, defaults,
1188-
kwonlyargs, kwdefaults, annotations)
1197+
posonlyargs, kwonlyargs, kwdefaults, annotations)
11891198

11901199

11911200
ArgInfo = namedtuple('ArgInfo', 'args varargs keywords locals')
@@ -1216,7 +1225,8 @@ def _formatannotation(annotation):
12161225
return _formatannotation
12171226

12181227
def formatargspec(args, varargs=None, varkw=None, defaults=None,
1219-
kwonlyargs=(), kwonlydefaults={}, annotations={},
1228+
posonlyargs=(), kwonlyargs=(), kwonlydefaults={},
1229+
annotations={},
12201230
formatarg=str,
12211231
formatvarargs=lambda name: '*' + name,
12221232
formatvarkw=lambda name: '**' + name,
@@ -1249,12 +1259,17 @@ def formatargandannotation(arg):
12491259
return result
12501260
specs = []
12511261
if defaults:
1252-
firstdefault = len(args) - len(defaults)
1253-
for i, arg in enumerate(args):
1262+
firstdefault = len(posonlyargs) + len(args) - len(defaults)
1263+
posonly_left = len(posonlyargs)
1264+
for i, arg in enumerate(list(posonlyargs) + list(args)):
12541265
spec = formatargandannotation(arg)
12551266
if defaults and i >= firstdefault:
12561267
spec = spec + formatvalue(defaults[i - firstdefault])
12571268
specs.append(spec)
1269+
posonly_left -= 1
1270+
if posonlyargs and posonly_left == 0:
1271+
specs.append('/')
1272+
12581273
if varargs is not None:
12591274
specs.append(formatvarargs(formatargandannotation(varargs)))
12601275
else:
@@ -1342,7 +1357,8 @@ def getcallargs(*func_and_positional, **named):
13421357
func = func_and_positional[0]
13431358
positional = func_and_positional[1:]
13441359
spec = getfullargspec(func)
1345-
args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, ann = spec
1360+
(args, varargs, varkw, defaults, posonlyargs,
1361+
kwonlyargs, kwonlydefaults, ann) = spec
13461362
f_name = func.__name__
13471363
arg2value = {}
13481364

@@ -1351,12 +1367,16 @@ def getcallargs(*func_and_positional, **named):
13511367
# implicit 'self' (or 'cls' for classmethods) argument
13521368
positional = (func.__self__,) + positional
13531369
num_pos = len(positional)
1370+
num_posonlyargs = len(posonlyargs)
13541371
num_args = len(args)
13551372
num_defaults = len(defaults) if defaults else 0
13561373

1374+
n = min(num_pos, num_posonlyargs)
1375+
for i in range(num_posonlyargs):
1376+
arg2value[posonlyargs[i]] = positional[i]
13571377
n = min(num_pos, num_args)
13581378
for i in range(n):
1359-
arg2value[args[i]] = positional[i]
1379+
arg2value[args[i]] = positional[num_posonlyargs+i]
13601380
if varargs:
13611381
arg2value[varargs] = tuple(positional[n:])
13621382
possible_kwargs = set(args + kwonlyargs)
@@ -2137,9 +2157,12 @@ def _signature_from_function(cls, func):
21372157
func_code = func.__code__
21382158
pos_count = func_code.co_argcount
21392159
arg_names = func_code.co_varnames
2140-
positional = tuple(arg_names[:pos_count])
2160+
posonly_count = func_code.co_posonlyargcount
2161+
positional_count = posonly_count + pos_count
2162+
positional_only = tuple(arg_names[:posonly_count])
2163+
positional = tuple(arg_names[posonly_count:positional_count])
21412164
keyword_only_count = func_code.co_kwonlyargcount
2142-
keyword_only = arg_names[pos_count:(pos_count + keyword_only_count)]
2165+
keyword_only = arg_names[positional_count:(positional_count + keyword_only_count)]
21432166
annotations = func.__annotations__
21442167
defaults = func.__defaults__
21452168
kwdefaults = func.__kwdefaults__
@@ -2151,23 +2174,33 @@ def _signature_from_function(cls, func):
21512174

21522175
parameters = []
21532176

2177+
non_default_count = positional_count - pos_default_count
2178+
all_positional = positional_only + positional
2179+
2180+
posonly_left = posonly_count
2181+
21542182
# Non-keyword-only parameters w/o defaults.
2155-
non_default_count = pos_count - pos_default_count
2156-
for name in positional[:non_default_count]:
2183+
for name in all_positional[:non_default_count]:
2184+
kind = _POSITIONAL_ONLY if posonly_left else _POSITIONAL_OR_KEYWORD
21572185
annotation = annotations.get(name, _empty)
21582186
parameters.append(Parameter(name, annotation=annotation,
2159-
kind=_POSITIONAL_OR_KEYWORD))
2187+
kind=kind))
2188+
if posonly_left:
2189+
posonly_left -= 1
21602190

21612191
# ... w/ defaults.
2162-
for offset, name in enumerate(positional[non_default_count:]):
2192+
for offset, name in enumerate(all_positional[non_default_count:]):
2193+
kind = _POSITIONAL_ONLY if posonly_left else _POSITIONAL_OR_KEYWORD
21632194
annotation = annotations.get(name, _empty)
21642195
parameters.append(Parameter(name, annotation=annotation,
2165-
kind=_POSITIONAL_OR_KEYWORD,
2196+
kind=kind,
21662197
default=defaults[offset]))
2198+
if posonly_left:
2199+
posonly_left -= 1
21672200

21682201
# *args
21692202
if func_code.co_flags & CO_VARARGS:
2170-
name = arg_names[pos_count + keyword_only_count]
2203+
name = arg_names[positional_count + keyword_only_count]
21712204
annotation = annotations.get(name, _empty)
21722205
parameters.append(Parameter(name, annotation=annotation,
21732206
kind=_VAR_POSITIONAL))
@@ -2184,7 +2217,7 @@ def _signature_from_function(cls, func):
21842217
default=default))
21852218
# **kwargs
21862219
if func_code.co_flags & CO_VARKEYWORDS:
2187-
index = pos_count + keyword_only_count
2220+
index = positional_count + keyword_only_count
21882221
if func_code.co_flags & CO_VARARGS:
21892222
index += 1
21902223

Lib/test/inspect_fodder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# line 5
66

77
# line 7
8-
def spam(a, b, c, d=3, e=4, f=5, *g, **h):
8+
def spam(a, /, b, c, d=3, e=4, f=5, *g, **h):
99
eggs(b + d, c + f)
1010

1111
# line 11

Lib/test/inspect_fodder2.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,19 @@ def func136():
137137
def func137():
138138
never_reached1
139139
never_reached2
140+
141+
#line 141
142+
def positional_only_arg(a,/):
143+
pass
144+
145+
#line 145
146+
def all_markers(a,b,/,c,d,*,e,f):
147+
pass
148+
149+
# line 149
150+
def all_markers_with_args_and_kwargs(a,b,/,c,d,*args,e,f,**kwargs):
151+
pass
152+
153+
#line 153
154+
def all_markers_with_defaults(a,b=1,/,c=2,d=3,*,e=4,f=5):
155+
pass

Lib/test/test_inspect.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -763,30 +763,31 @@ def assertArgSpecEquals(self, routine, args_e, varargs_e=None,
763763

764764
def assertFullArgSpecEquals(self, routine, args_e, varargs_e=None,
765765
varkw_e=None, defaults_e=None,
766-
kwonlyargs_e=[], kwonlydefaults_e=None,
766+
posonlyargs_e=[], kwonlyargs_e=[],
767+
kwonlydefaults_e=None,
767768
ann_e={}, formatted=None):
768-
args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, ann = \
769+
args, varargs, varkw, defaults, posonlyargs, kwonlyargs, kwonlydefaults, ann = \
769770
inspect.getfullargspec(routine)
770771
self.assertEqual(args, args_e)
771772
self.assertEqual(varargs, varargs_e)
772773
self.assertEqual(varkw, varkw_e)
773774
self.assertEqual(defaults, defaults_e)
775+
self.assertEqual(posonlyargs, posonlyargs_e)
774776
self.assertEqual(kwonlyargs, kwonlyargs_e)
775777
self.assertEqual(kwonlydefaults, kwonlydefaults_e)
776778
self.assertEqual(ann, ann_e)
777779
if formatted is not None:
778780
with self.assertWarns(DeprecationWarning):
779781
self.assertEqual(inspect.formatargspec(args, varargs, varkw, defaults,
780-
kwonlyargs, kwonlydefaults, ann),
782+
posonlyargs, kwonlyargs,
783+
kwonlydefaults, ann),
781784
formatted)
782785

783786
def test_getargspec(self):
784-
self.assertArgSpecEquals(mod.eggs, ['x', 'y'], formatted='(x, y)')
787+
# self.assertArgSpecEquals(mod.eggs, ['x', 'y'], formatted='(x, y)')
785788

786-
self.assertArgSpecEquals(mod.spam,
787-
['a', 'b', 'c', 'd', 'e', 'f'],
788-
'g', 'h', (3, 4, 5),
789-
'(a, b, c, d=3, e=4, f=5, *g, **h)')
789+
self.assertRaises(ValueError, self.assertArgSpecEquals,
790+
mod.spam, [])
790791

791792
self.assertRaises(ValueError, self.assertArgSpecEquals,
792793
mod2.keyworded, [])
@@ -810,6 +811,26 @@ def test_getfullargspec(self):
810811
kwonlyargs_e=['arg'],
811812
formatted='(*, arg)')
812813

814+
self.assertFullArgSpecEquals(mod2.all_markers, ['c', 'd'],
815+
posonlyargs_e=['a', 'b'],
816+
kwonlyargs_e=['e', 'f'],
817+
formatted='(a, b, /, c, d, *, e, f)')
818+
819+
self.assertFullArgSpecEquals(mod2.all_markers_with_args_and_kwargs,
820+
['c', 'd'],
821+
posonlyargs_e=['a', 'b'],
822+
varargs_e='args',
823+
varkw_e='kwargs',
824+
kwonlyargs_e=['e', 'f'],
825+
formatted='(a, b, /, c, d, *args, e, f, **kwargs)')
826+
827+
self.assertFullArgSpecEquals(mod2.all_markers_with_defaults, ['c', 'd'],
828+
defaults_e=(1,2,3),
829+
posonlyargs_e=['a', 'b'],
830+
kwonlyargs_e=['e', 'f'],
831+
kwonlydefaults_e={'e': 4, 'f': 5},
832+
formatted='(a, b=1, /, c=2, d=3, *, e=4, f=5)')
833+
813834
def test_argspec_api_ignores_wrapped(self):
814835
# Issue 20684: low level introspection API must ignore __wrapped__
815836
@functools.wraps(mod.spam)
@@ -856,7 +877,7 @@ def test():
856877
spam_param = inspect.Parameter('spam', inspect.Parameter.POSITIONAL_ONLY)
857878
test.__signature__ = inspect.Signature(parameters=(spam_param,))
858879

859-
self.assertFullArgSpecEquals(test, args_e=['spam'], formatted='(spam)')
880+
self.assertFullArgSpecEquals(test, [], posonlyargs_e=['spam'], formatted='(spam, /)')
860881

861882
def test_getfullargspec_signature_annos(self):
862883
def test(a:'spam') -> 'ham': pass
@@ -870,11 +891,11 @@ def test(): pass
870891
@unittest.skipIf(MISSING_C_DOCSTRINGS,
871892
"Signature information for builtins requires docstrings")
872893
def test_getfullargspec_builtin_methods(self):
873-
self.assertFullArgSpecEquals(_pickle.Pickler.dump,
874-
args_e=['self', 'obj'], formatted='(self, obj)')
894+
self.assertFullArgSpecEquals(_pickle.Pickler.dump, [],
895+
posonlyargs_e=['self', 'obj'], formatted='(self, obj, /)')
875896

876-
self.assertFullArgSpecEquals(_pickle.Pickler(io.BytesIO()).dump,
877-
args_e=['self', 'obj'], formatted='(self, obj)')
897+
self.assertFullArgSpecEquals(_pickle.Pickler(io.BytesIO()).dump, [],
898+
posonlyargs_e=['self', 'obj'], formatted='(self, obj, /)')
878899

879900
self.assertFullArgSpecEquals(
880901
os.stat,

Lib/test/test_positional_only_arg.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def assertRaisesSyntaxError(self, codestr, regex="invalid syntax"):
2222
def test_invalid_syntax_errors(self):
2323
self.assertRaisesSyntaxError("def f(a, b = 5, /, c): pass", "non-default argument follows default argument")
2424
self.assertRaisesSyntaxError("def f(a = 5, b, /, c): pass", "non-default argument follows default argument")
25+
self.assertRaisesSyntaxError("def f(a = 5, b=1, /, c, *, d=2): pass", "non-default argument follows default argument")
2526
self.assertRaisesSyntaxError("def f(a = 5, b, /): pass", "non-default argument follows default argument")
2627
self.assertRaisesSyntaxError("def f(*args, /): pass")
2728
self.assertRaisesSyntaxError("def f(*args, a, /): pass")
@@ -38,6 +39,7 @@ def test_invalid_syntax_errors(self):
3839
def test_invalid_syntax_errors_async(self):
3940
self.assertRaisesSyntaxError("async def f(a, b = 5, /, c): pass", "non-default argument follows default argument")
4041
self.assertRaisesSyntaxError("async def f(a = 5, b, /, c): pass", "non-default argument follows default argument")
42+
self.assertRaisesSyntaxError("async def f(a = 5, b=1, /, c, d=2): pass", "non-default argument follows default argument")
4143
self.assertRaisesSyntaxError("async def f(a = 5, b, /): pass", "non-default argument follows default argument")
4244
self.assertRaisesSyntaxError("async def f(*args, /): pass")
4345
self.assertRaisesSyntaxError("async def f(*args, a, /): pass")
@@ -146,6 +148,26 @@ def f(a, b, /, c=3):
146148
with self.assertRaisesRegex(TypeError, r"f\(\) takes from 2 to 3 positional arguments but 4 were given"):
147149
f(1, 2, 3, 4)
148150

151+
def test_positional_only_and_kwonlyargs_invalid_calls(self):
152+
def f(a, b, /, c, *, d, e):
153+
pass
154+
f(1, 2, 3, d=1, e=2) # does not raise
155+
with self.assertRaisesRegex(TypeError, r"missing 1 required keyword-only argument: 'd'"):
156+
f(1, 2, 3, e=2)
157+
with self.assertRaisesRegex(TypeError, r"missing 2 required keyword-only arguments: 'd' and 'e'"):
158+
f(1, 2, 3)
159+
with self.assertRaisesRegex(TypeError, r"f\(\) missing 1 required positional argument: 'c'"):
160+
f(1, 2)
161+
with self.assertRaisesRegex(TypeError, r"f\(\) missing 2 required positional arguments: 'b' and 'c'"):
162+
f(1)
163+
with self.assertRaisesRegex(TypeError, r" missing 3 required positional arguments: 'a', 'b', and 'c'"):
164+
f()
165+
with self.assertRaisesRegex(TypeError, r"f\(\) takes 3 positional arguments but 6 positional arguments "
166+
r"\(and 2 keyword-only arguments\) were given"):
167+
f(1, 2, 3, 4, 5, 6, d=7, e=8)
168+
with self.assertRaisesRegex(TypeError, r"f\(\) got an unexpected keyword argument 'f'"):
169+
f(1, 2, 3, d=1, e=4, f=56)
170+
149171
def test_positional_only_invalid_calls(self):
150172
def f(a, b, /):
151173
pass

0 commit comments

Comments
 (0)