1#############################################################################
2##
3## Copyright (C) 2019 The Qt Company Ltd.
4## Contact: https://www.qt.io/licensing/
5##
6## This file is part of Qt for Python.
7##
8## $QT_BEGIN_LICENSE:LGPL$
9## Commercial License Usage
10## Licensees holding valid commercial Qt licenses may use this file in
11## accordance with the commercial license agreement provided with the
12## Software or, alternatively, in accordance with the terms contained in
13## a written agreement between you and The Qt Company. For licensing terms
14## and conditions see https://www.qt.io/terms-conditions. For further
15## information use the contact form at https://www.qt.io/contact-us.
16##
17## GNU Lesser General Public License Usage
18## Alternatively, this file may be used under the terms of the GNU Lesser
19## General Public License version 3 as published by the Free Software
20## Foundation and appearing in the file LICENSE.LGPL3 included in the
21## packaging of this file. Please review the following information to
22## ensure the GNU Lesser General Public License version 3 requirements
23## will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24##
25## GNU General Public License Usage
26## Alternatively, this file may be used under the terms of the GNU
27## General Public License version 2.0 or (at your option) the GNU General
28## Public license version 3 or any later version approved by the KDE Free
29## Qt Foundation. The licenses are as published by the Free Software
30## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31## included in the packaging of this file. Please review the following
32## information to ensure the GNU General Public License requirements will
33## be met: https://www.gnu.org/licenses/gpl-2.0.html and
34## https://www.gnu.org/licenses/gpl-3.0.html.
35##
36## $QT_END_LICENSE$
37##
38#############################################################################
39
40from __future__ import print_function, absolute_import
41
42"""
43layout.py
44
45The signature module now has the capability to configure
46differently formatted versions of signatures. The default
47layout is known from the "__signature__" attribute.
48
49The function "get_signature(ob, modifier=None)" produces the same
50signatures by default. By passing different modifiers, you
51can select different layouts.
52
53This module configures the different layouts which can be used.
54It also implements them in this file. The configurations are
55used literally as strings like "signature", "existence", etc.
56"""
57
58from textwrap import dedent
59from shibokensupport.signature import inspect, typing
60from shibokensupport.signature.mapping import ellipsis
61from shibokensupport.signature.lib.tool import SimpleNamespace
62
63
64class SignatureLayout(SimpleNamespace):
65    """
66    Configure a signature.
67
68    The layout of signatures can have different layouts which are
69    controlled by keyword arguments:
70
71    definition=True         Determines if self will generated.
72    defaults=True
73    ellipsis=False          Replaces defaults by "...".
74    return_annotation=True
75    parameter_names=True    False removes names before ":".
76    """
77    allowed_keys = SimpleNamespace(definition=True,
78                                   defaults=True,
79                                   ellipsis=False,
80                                   return_annotation=True,
81                                   parameter_names=True)
82    allowed_values = True, False
83
84    def __init__(self, **kwds):
85        args = SimpleNamespace(**self.allowed_keys.__dict__)
86        args.__dict__.update(kwds)
87        self.__dict__.update(args.__dict__)
88        err_keys = list(set(self.__dict__) - set(self.allowed_keys.__dict__))
89        if err_keys:
90            self._attributeerror(err_keys)
91        err_values = list(set(self.__dict__.values()) - set(self.allowed_values))
92        if err_values:
93            self._valueerror(err_values)
94
95    def __setattr__(self, key, value):
96        if key not in self.allowed_keys.__dict__:
97            self._attributeerror([key])
98        if value not in self.allowed_values:
99            self._valueerror([value])
100        self.__dict__[key] = value
101
102    def _attributeerror(self, err_keys):
103        err_keys = ", ".join(err_keys)
104        allowed_keys = ", ".join(self.allowed_keys.__dict__.keys())
105        raise AttributeError(dedent("""\
106            Not allowed: '{err_keys}'.
107            The only allowed keywords are '{allowed_keys}'.
108            """.format(**locals())))
109
110    def _valueerror(self, err_values):
111        err_values = ", ".join(map(str, err_values))
112        allowed_values = ", ".join(map(str, self.allowed_values))
113        raise ValueError(dedent("""\
114            Not allowed: '{err_values}'.
115            The only allowed values are '{allowed_values}'.
116            """.format(**locals())))
117
118# The following names are used literally in this module.
119# This way, we avoid the dict hashing problem.
120signature = SignatureLayout()
121
122existence = SignatureLayout(definition=False,
123                            defaults=False,
124                            return_annotation=False,
125                            parameter_names=False)
126
127hintingstub = SignatureLayout(ellipsis=True)
128
129typeerror = SignatureLayout(definition=False,
130                            return_annotation=False,
131                            parameter_names=False)
132
133
134def define_nameless_parameter():
135    """
136    Create Nameless Parameters
137
138    A nameless parameter has a reduced string representation.
139    This is done by cloning the parameter type and overwriting its
140    __str__ method. The inner structure is still a valid parameter.
141    """
142    def __str__(self):
143        # for Python 2, we must change self to be an instance of P
144        klass = self.__class__
145        self.__class__ = P
146        txt = P.__str__(self)
147        self.__class__ = klass
148        txt = txt[txt.index(":") + 1:].strip() if ":" in txt else txt
149        return txt
150
151    P = inspect.Parameter
152    newname = "NamelessParameter"
153    bases = P.__bases__
154    body = dict(P.__dict__) # get rid of mappingproxy
155    if "__slots__" in body:
156        # __slots__ would create duplicates
157        for name in body["__slots__"]:
158            del body[name]
159    body["__str__"] = __str__
160    return type(newname, bases, body)
161
162
163NamelessParameter = define_nameless_parameter()
164
165"""
166Note on the "Optional" feature:
167
168When an annotation has a default value that is None, then the
169type has to be wrapped into "typing.Optional".
170
171Note that only the None value creates an Optional expression,
172because the None leaves the domain of the variable.
173Defaults like integer values are ignored: They stay in the domain.
174
175That information would be lost when we use the "..." convention.
176
177Note that the typing module has the remarkable expansion
178
179    Optional[T]    is    Variant[T, NoneType]
180
181We want to avoid that when generating the .pyi file.
182This is done by a regex in generate_pyi.py .
183The following would work in Python 3, but this is a version-dependent
184hack that also won't work in Python 2 and would be _very_ complex.
185"""
186# import sys
187# if sys.version_info[0] == 3:
188#     class hugo(list):pass
189#     typing._normalize_alias["hugo"] = "Optional"
190#     Optional = typing._alias(hugo, typing.T, inst=False)
191# else:
192#     Optional = typing.Optional
193
194
195def make_signature_nameless(signature):
196    """
197    Make a Signature Nameless
198
199    We use an existing signature and change the type of its parameters.
200    The signature looks different, but is totally intact.
201    """
202    for key in signature.parameters.keys():
203        signature.parameters[key].__class__ = NamelessParameter
204
205
206_POSITIONAL_ONLY         = inspect._POSITIONAL_ONLY
207_POSITIONAL_OR_KEYWORD   = inspect._POSITIONAL_OR_KEYWORD
208_VAR_POSITIONAL          = inspect._VAR_POSITIONAL
209_KEYWORD_ONLY            = inspect._KEYWORD_ONLY
210_VAR_KEYWORD             = inspect._VAR_KEYWORD
211_empty                   = inspect._empty
212
213def create_signature(props, key):
214    if not props:
215        # empty signatures string
216        return
217    if isinstance(props["multi"], list):
218        # multi sig: call recursively
219        return list(create_signature(elem, key)
220                    for elem in props["multi"])
221    if type(key) is tuple:
222        sig_kind, modifier = key
223    else:
224        sig_kind, modifier = key, "signature"
225
226    layout = globals()[modifier]  # lookup of the modifier in this module
227    if not isinstance(layout, SignatureLayout):
228        raise SystemError("Modifiers must be names of a SignatureLayout "
229                          "instance")
230
231    # this is the basic layout of a signature
232    varnames = props["varnames"]
233    if layout.definition:
234        # PYSIDE-1328: We no longer use info from the sig_kind which is
235        # more complex for multiple signatures. We now get `self` from the
236        # parser.
237        pass
238    else:
239        if "self" in varnames[:1]:
240            varnames = varnames[1:]
241
242    # calculate the modifications
243    defaults = props["defaults"][:]
244    if not layout.defaults:
245        defaults = ()
246    annotations = props["annotations"].copy()
247    if not layout.return_annotation and "return" in annotations:
248        del annotations["return"]
249
250    # Build a signature.
251    kind = inspect._POSITIONAL_OR_KEYWORD
252    params = []
253    for idx, name in enumerate(varnames):
254        if name.startswith("**"):
255            kind = _VAR_KEYWORD
256        elif name.startswith("*"):
257            kind = _VAR_POSITIONAL
258        ann = annotations.get(name, _empty)
259        if ann == "self":
260            ann = _empty
261        name = name.lstrip("*")
262        defpos = idx - len(varnames) + len(defaults)
263        default = defaults[defpos] if defpos >= 0 else _empty
264        if default is None:
265            ann = typing.Optional[ann]
266        if default is not _empty and layout.ellipsis:
267            default = ellipsis
268        param = inspect.Parameter(name, kind, annotation=ann, default=default)
269        params.append(param)
270        if kind == _VAR_POSITIONAL:
271            kind = _KEYWORD_ONLY
272    sig = inspect.Signature(params,
273           return_annotation=annotations.get('return', _empty),
274           __validate_parameters__=False)
275
276    # the special case of nameless parameters
277    if not layout.parameter_names:
278        make_signature_nameless(sig)
279    return sig
280
281# end of file
282