1#     Copyright 2021, Kay Hayen, mailto:kay.hayen@gmail.com
2#
3#     Part of "Nuitka", an optimizing Python compiler that is compatible and
4#     integrates with CPython, but also works on its own.
5#
6#     Licensed under the Apache License, Version 2.0 (the "License");
7#     you may not use this file except in compliance with the License.
8#     You may obtain a copy of the License at
9#
10#        http://www.apache.org/licenses/LICENSE-2.0
11#
12#     Unless required by applicable law or agreed to in writing, software
13#     distributed under the License is distributed on an "AS IS" BASIS,
14#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15#     See the License for the specific language governing permissions and
16#     limitations under the License.
17#
18""" This module maintains the parameter specification classes.
19
20These are used for function, lambdas, generators. They are also a factory
21for the respective variable objects. One of the difficulty of Python and
22its parameter parsing is that they are allowed to be nested like this:
23
24(a,b), c
25
26Much like in assignments, which are very similar to parameters, except
27that parameters may also be assigned from a dictionary, they are no less
28flexible.
29
30"""
31
32from nuitka import Variables
33from nuitka.PythonVersions import python_version
34from nuitka.utils.InstanceCounters import (
35    counted_del,
36    counted_init,
37    isCountingInstances,
38)
39
40
41class TooManyArguments(Exception):
42    def __init__(self, real_exception):
43        Exception.__init__(self)
44
45        self.real_exception = real_exception
46
47    def getRealException(self):
48        return self.real_exception
49
50
51class ParameterSpec(object):
52    # These got many attributes, in part duplicating name and instance of
53    # variables, pylint: disable=too-many-instance-attributes
54
55    __slots__ = (
56        "name",
57        "owner",
58        "normal_args",
59        "normal_variables",
60        "list_star_arg",
61        "dict_star_arg",
62        "list_star_variable",
63        "dict_star_variable",
64        "default_count",
65        "kw_only_args",
66        "kw_only_variables",
67        "pos_only_args",
68        "pos_only_variables",
69    )
70
71    @counted_init
72    def __init__(
73        self,
74        ps_name,
75        ps_normal_args,
76        ps_pos_only_args,
77        ps_kw_only_args,
78        ps_list_star_arg,
79        ps_dict_star_arg,
80        ps_default_count,
81    ):
82        if type(ps_normal_args) is str:
83            if ps_normal_args == "":
84                ps_normal_args = ()
85            else:
86                ps_normal_args = ps_normal_args.split(",")
87
88        if type(ps_kw_only_args) is str:
89            if ps_kw_only_args == "":
90                ps_kw_only_args = ()
91            else:
92                ps_kw_only_args = ps_kw_only_args.split(",")
93
94        assert None not in ps_normal_args
95
96        self.owner = None
97
98        self.name = ps_name
99        self.normal_args = tuple(ps_normal_args)
100        self.normal_variables = None
101
102        assert (
103            ps_list_star_arg is None or type(ps_list_star_arg) is str
104        ), ps_list_star_arg
105        assert (
106            ps_dict_star_arg is None or type(ps_dict_star_arg) is str
107        ), ps_dict_star_arg
108
109        self.list_star_arg = ps_list_star_arg if ps_list_star_arg else None
110        self.dict_star_arg = ps_dict_star_arg if ps_dict_star_arg else None
111
112        self.list_star_variable = None
113        self.dict_star_variable = None
114
115        self.default_count = ps_default_count
116
117        self.kw_only_args = tuple(ps_kw_only_args)
118        self.kw_only_variables = None
119
120        self.pos_only_args = tuple(ps_pos_only_args)
121        self.pos_only_variables = None
122
123    if isCountingInstances():
124        __del__ = counted_del()
125
126    def makeClone(self):
127        return ParameterSpec(
128            ps_name=self.name,
129            ps_normal_args=self.normal_args,
130            ps_pos_only_args=self.pos_only_args,
131            ps_kw_only_args=self.kw_only_args,
132            ps_list_star_arg=self.list_star_arg,
133            ps_dict_star_arg=self.dict_star_arg,
134            ps_default_count=self.default_count,
135        )
136
137    def getDetails(self):
138        return {
139            "ps_name": self.name,
140            "ps_normal_args": ",".join(self.normal_args),
141            "ps_pos_only_args": self.pos_only_args,
142            "ps_kw_only_args": ",".join(self.kw_only_args),
143            "ps_list_star_arg": self.list_star_arg
144            if self.list_star_arg is not None
145            else "",
146            "ps_dict_star_arg": self.dict_star_arg
147            if self.dict_star_arg is not None
148            else "",
149            "ps_default_count": self.default_count,
150        }
151
152    def checkParametersValid(self):
153        arg_names = self.getParameterNames()
154
155        # Check for duplicate arguments, could happen.
156        for arg_name in arg_names:
157            if arg_names.count(arg_name) != 1:
158                return "duplicate argument '%s' in function definition" % arg_name
159
160        return None
161
162    def __repr__(self):
163        parts = [str(normal_arg) for normal_arg in self.pos_only_args]
164        if parts:
165            parts.append("/")
166
167        parts += [str(normal_arg) for normal_arg in self.normal_args]
168
169        if self.list_star_arg is not None:
170            parts.append("*%s" % self.list_star_arg)
171
172        if self.dict_star_variable is not None:
173            parts.append("**%s" % self.dict_star_variable)
174
175        if parts:
176            return "<ParameterSpec '%s'>" % ",".join(parts)
177        else:
178            return "<NoParameters>"
179
180    def setOwner(self, owner):
181        if self.owner is not None:
182            return
183
184        self.owner = owner
185        self.normal_variables = []
186
187        for normal_arg in self.normal_args:
188            if type(normal_arg) is str:
189                normal_variable = Variables.ParameterVariable(
190                    owner=self.owner, parameter_name=normal_arg
191                )
192            else:
193                assert False, normal_arg
194
195            self.normal_variables.append(normal_variable)
196
197        if self.list_star_arg:
198            self.list_star_variable = Variables.ParameterVariable(
199                owner=owner, parameter_name=self.list_star_arg
200            )
201        else:
202            self.list_star_variable = None
203
204        if self.dict_star_arg:
205            self.dict_star_variable = Variables.ParameterVariable(
206                owner=owner, parameter_name=self.dict_star_arg
207            )
208        else:
209            self.dict_star_variable = None
210
211        self.kw_only_variables = [
212            Variables.ParameterVariable(owner=self.owner, parameter_name=kw_only_arg)
213            for kw_only_arg in self.kw_only_args
214        ]
215
216        self.pos_only_variables = [
217            Variables.ParameterVariable(owner=self.owner, parameter_name=pos_only_arg)
218            for pos_only_arg in self.pos_only_args
219        ]
220
221    def getDefaultCount(self):
222        return self.default_count
223
224    def hasDefaultParameters(self):
225        return self.getDefaultCount() > 0
226
227    def getTopLevelVariables(self):
228        return self.pos_only_variables + self.normal_variables + self.kw_only_variables
229
230    def getAllVariables(self):
231        result = list(self.pos_only_variables)
232        result += self.normal_variables
233        result += self.kw_only_variables
234
235        if self.list_star_variable is not None:
236            result.append(self.list_star_variable)
237
238        if self.dict_star_variable is not None:
239            result.append(self.dict_star_variable)
240
241        return result
242
243    def getParameterNames(self):
244        result = list(self.pos_only_args + self.normal_args)
245
246        result += self.kw_only_args
247
248        if self.list_star_arg is not None:
249            result.append(self.list_star_arg)
250
251        if self.dict_star_arg is not None:
252            result.append(self.dict_star_arg)
253
254        return result
255
256    def getStarListArgumentName(self):
257        return self.list_star_arg
258
259    def getListStarArgVariable(self):
260        return self.list_star_variable
261
262    def getStarDictArgumentName(self):
263        return self.dict_star_arg
264
265    def getDictStarArgVariable(self):
266        return self.dict_star_variable
267
268    def getKwOnlyVariables(self):
269        return self.kw_only_variables
270
271    def allowsKeywords(self):
272        # Abstract method, pylint: disable=no-self-use
273        return True
274
275    def getKeywordRefusalText(self):
276        return "%s() takes no keyword arguments" % self.name
277
278    def getArgumentNames(self):
279        return self.pos_only_args + self.normal_args
280
281    def getArgumentCount(self):
282        return len(self.normal_args) + len(self.pos_only_args)
283
284    def getKwOnlyParameterNames(self):
285        return self.kw_only_args
286
287    def getKwOnlyParameterCount(self):
288        return len(self.kw_only_args)
289
290    def getPosOnlyParameterCount(self):
291        return len(self.pos_only_args)
292
293
294def matchCall(
295    func_name,
296    args,
297    kw_only_args,
298    star_list_arg,
299    star_dict_arg,
300    num_defaults,
301    num_posonly,
302    positional,
303    pairs,
304    improved=False,
305):
306    """Match a call arguments to a signature.
307
308    Args:
309        func_name - Name of the function being matched, used to construct exception texts.
310        args - normal argument names
311        kw_only_args -  keyword only argument names (Python3)
312        star_list_arg - name of star list argument if any
313        star_dict_arg - name of star dict argument if any
314        num_defaults - amount of arguments that have default values
315        num_posonly - amount of arguments that must be given by position
316        positional - tuple of argument values given for simulated call
317        pairs - tuple of pairs arg argument name and argument values
318        improved - (bool) should we give better errors than CPython or not.
319    Returns:
320        Dictionary of argument name to value mappings
321    Notes:
322        Based loosely on "inspect.getcallargs" with corrections.
323    """
324
325    # This is of incredible code complexity, but there really is no other way to
326    # express this with less statements, branches, or variables.
327    # pylint: disable=too-many-branches,too-many-locals,too-many-statements
328
329    assert type(positional) is tuple, positional
330    assert type(pairs) in (tuple, list), pairs
331
332    # Make a copy, we are going to modify it.
333    pairs = list(pairs)
334
335    result = {}
336
337    assigned_tuple_params = []
338
339    def assign(arg, value):
340        if type(arg) is str:
341            # Normal case:
342            result[arg] = value
343        else:
344            # Tuple argument case:
345
346            assigned_tuple_params.append(arg)
347            value = iter(value.getIterationValues())
348
349            for i, subarg in enumerate(arg):
350                try:
351                    subvalue = next(value)
352                except StopIteration:
353                    raise TooManyArguments(
354                        ValueError(
355                            "need more than %d %s to unpack"
356                            % (i, "values" if i > 1 else "value")
357                        )
358                    )
359
360                # Recurse into tuple argument values, could be more tuples.
361                assign(subarg, subvalue)
362
363            # Check that not too many values we provided.
364            try:
365                next(value)
366            except StopIteration:
367                pass
368            else:
369                raise TooManyArguments(ValueError("too many values to unpack"))
370
371    def isAssigned(arg):
372        if type(arg) is str:
373            return arg in result
374
375        return arg in assigned_tuple_params
376
377    num_pos = len(positional)
378    num_total = num_pos + len(pairs)
379    num_args = len(args)
380
381    for arg, value in zip(args, positional):
382        assign(arg, value)
383
384    # Python3 does this check earlier.
385    if python_version >= 0x300 and not star_dict_arg:
386        for pair in pairs:
387            try:
388                arg_index = (args + kw_only_args).index(pair[0])
389            except ValueError:
390                if improved or python_version >= 0x370:
391                    message = "'%s' is an invalid keyword argument for %s()" % (
392                        pair[0],
393                        func_name,
394                    )
395                else:
396                    message = (
397                        "'%s' is an invalid keyword argument for this function"
398                        % pair[0]
399                    )
400
401                raise TooManyArguments(TypeError(message))
402            else:
403                if arg_index < num_posonly:
404                    message = "'%s' is an invalid keyword argument for %s()" % (
405                        pair[0],
406                        func_name,
407                    )
408
409                    raise TooManyArguments(TypeError(message))
410
411    if star_list_arg:
412        if num_pos > num_args:
413            assign(star_list_arg, positional[-(num_pos - num_args) :])
414        else:
415            assign(star_list_arg, ())
416    elif 0 < num_args < num_total:
417        # Special case for no default values.
418        if num_defaults == 0:
419            # Special cases text for one argument.
420            if num_args == 1:
421                raise TooManyArguments(
422                    TypeError(
423                        "%s() takes exactly one argument (%d given)"
424                        % (func_name, num_total)
425                    )
426                )
427
428            raise TooManyArguments(
429                TypeError(
430                    "%s expected %d arguments, got %d"
431                    % (func_name, num_args, num_total)
432                )
433            )
434
435        raise TooManyArguments(
436            TypeError(
437                "%s() takes at most %d %s (%d given)"
438                % (
439                    func_name,
440                    num_args,
441                    "argument" if num_args == 1 else "arguments",
442                    num_total,
443                )
444            )
445        )
446    elif num_args == 0 and num_total:
447        if star_dict_arg:
448            if num_pos:
449                # Could use num_pos, but Python also uses num_total.
450                raise TooManyArguments(
451                    TypeError(
452                        "%s() takes exactly 0 arguments (%d given)"
453                        % (func_name, num_total)
454                    )
455                )
456        else:
457            raise TooManyArguments(
458                TypeError("%s() takes no arguments (%d given)" % (func_name, num_total))
459            )
460
461    named_argument_names = [pair[0] for pair in pairs]
462
463    for arg in args + kw_only_args:
464        if type(arg) is str and arg in named_argument_names:
465            if isAssigned(arg):
466                raise TooManyArguments(
467                    TypeError(
468                        "%s() got multiple values for keyword argument '%s'"
469                        % (func_name, arg)
470                    )
471                )
472
473            new_pairs = []
474
475            for pair in pairs:
476                if arg == pair[0]:
477                    assign(arg, pair[1])
478                else:
479                    new_pairs.append(pair)
480
481            assert len(new_pairs) == len(pairs) - 1
482
483            pairs = new_pairs
484
485    # Fill in any missing values with the None to indicate "default".
486    if num_defaults > 0:
487        for arg in (kw_only_args + args)[-num_defaults:]:
488            if not isAssigned(arg):
489                assign(arg, None)
490
491    if star_dict_arg:
492        assign(star_dict_arg, pairs)
493    elif pairs:
494        unexpected = next(iter(dict(pairs)))
495
496        if improved:
497            message = "%s() got an unexpected keyword argument '%s'" % (
498                func_name,
499                unexpected,
500            )
501        else:
502            message = (
503                "'%s' is an invalid keyword argument for this function" % unexpected
504            )
505
506        raise TooManyArguments(TypeError(message))
507
508    unassigned = num_args - len([arg for arg in args if isAssigned(arg)])
509
510    if unassigned:
511        num_required = num_args - num_defaults
512
513        # Special case required arguments.
514        if num_required > 0 or improved:
515            if num_defaults == 0 and num_args != 1:
516                raise TooManyArguments(
517                    TypeError(
518                        "%s expected %d arguments, got %d"
519                        % (func_name, num_args, num_total)
520                    )
521                )
522
523            if num_required == 1:
524                arg_desc = "1 argument" if python_version < 0x350 else "one argument"
525            else:
526                arg_desc = "%d arguments" % num_required
527
528            raise TooManyArguments(
529                TypeError(
530                    "%s() takes %s %s (%d given)"
531                    % (
532                        func_name,
533                        "at least" if num_defaults > 0 else "exactly",
534                        arg_desc,
535                        num_total,
536                    )
537                )
538            )
539
540        raise TooManyArguments(
541            TypeError(
542                "%s expected %s%s, got %d"
543                % (
544                    func_name,
545                    ("at least " if python_version < 0x300 else "")
546                    if num_defaults > 0
547                    else "exactly ",
548                    "%d arguments" % num_required,
549                    num_total,
550                )
551            )
552        )
553
554    unassigned = len(kw_only_args) - len(
555        [arg for arg in kw_only_args if isAssigned(arg)]
556    )
557    if unassigned:
558        raise TooManyArguments(
559            TypeError(
560                "%s missing %d required keyword-only argument%s: %s"
561                % (
562                    func_name,
563                    unassigned,
564                    "s" if unassigned > 1 else "",
565                    " and ".join(
566                        "'%s'"
567                        % [arg.getName() for arg in kw_only_args if not isAssigned(arg)]
568                    ),
569                )
570            )
571        )
572
573    return result
574