File: validators.py

package info (click to toggle)
psychopy 2020.2.10%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 44,056 kB
  • sloc: python: 119,649; javascript: 3,022; makefile: 148; sh: 125; xml: 9
file content (253 lines) | stat: -rw-r--r-- 10,555 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2020 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).

"""
Module containing validators for various parameters.
"""
from __future__ import absolute_import, print_function

from past.builtins import basestring
import wx

import psychopy.experiment.utils
from psychopy.localization import _translate
from . import experiment
from .localizedStrings import _localized
from pkg_resources import parse_version

if parse_version(wx.__version__) < parse_version('4.0.0a1'):
    _ValidatorBase = wx.PyValidator
else:
    _ValidatorBase = wx.Validator

from pyglet.window import key


class BaseValidator(_ValidatorBase):
    """
    Component name validator for _BaseParamsDlg class. It depends on access
    to an experiment namespace.

    Validate calls check, which needs to be implemented per class.

    Messages are passed to user as text in nameOklabel.

    @see: _BaseParamsDlg
    """

    def __init__(self):
        super(BaseValidator, self).__init__()

    def Clone(self):
        return self.__class__()

    def Validate(self, parent):
        """
        """
        # we need to find the dialog to which the Validate event belongs
        # (the event might be fired by a sub-panel and won't have builder exp)
        while not hasattr(parent, 'frame'):
            try:
                parent = parent.GetParent()
            except Exception:
                print("Couldn't find the root dialog for this event")
        message, valid = self.check(parent)
        self.updateMessage(parent)
        return valid

    def TransferFromWindow(self):
        return True

    def TransferToWindow(self):
        return True

    def check(self, parent):
        raise NotImplementedError

    def updateMessage(self, parent):
        """Checks the dict of warning messages for the parent and inserts the
        top one
        :param parent: a component params dialog
        :param message: a
        :return:
        """
        warnings = [w for w in list(parent.warningsDict.values()) if w] or ['']
        msg = warnings[0]
        if parent.nameOKlabel and parent.nameOKlabel.GetLabel() != msg:
            parent.nameOKlabel.SetLabel(msg)
            parent.Layout()


class NameValidator(BaseValidator):
    """Validation checks if the value in Name field is a valid Python
    identifier and if it does not clash with existing names.
    """

    def __init__(self):
        super(NameValidator, self).__init__()

    def check(self, parent):
        """checks namespace, return error-msg (str), enable (bool)
        """
        control = self.GetWindow()
        newName = control.GetValue()
        msg, OK = '', True  # until we find otherwise
        if newName == '':
            msg = _translate("Missing name")
            OK = False
        else:
            namespace = parent.frame.exp.namespace
            used = namespace.exists(newName)
            sameAsOldName = bool(newName == parent.params['name'].val)
            if used and not sameAsOldName:
                msg = _translate("That name is in use (by %s). Try another name.") % _translate(used)
                OK = False
            elif not namespace.isValid(newName):  # valid as a var name
                msg = _translate("Name must be alpha-numeric or _, no spaces")
                OK = False
            # warn but allow, chances are good that its actually ok
            elif namespace.isPossiblyDerivable(newName):
                msg = _translate(namespace.isPossiblyDerivable(newName))
                OK = True
        parent.warningsDict['name'] = msg
        return msg, OK


class CodeSnippetValidator(BaseValidator):
    """Validation checks if field value is intended to be Python code.
    If so, check that it is valid Python, and if valid, check whether
    it contains bad identifiers (currently only self-reference).

    @see: _BaseParamsDlg
    """

    def __init__(self, fieldName):
        super(CodeSnippetValidator, self).__init__()
        self.fieldName = fieldName
        try:
            self.displayName = _localized[fieldName]
        except KeyError:
            # should have all _localized[fieldName] from .localizedStrings
            # might as well try to do something useful if fail:
            self.displayName = _translate(fieldName)

    def Clone(self):
        return self.__class__(self.fieldName)

    def check(self, parent):
        """Checks python syntax of code snippets, and for self-reference.

        Note: code snippets simply use existing names in the namespace,
        like from condition-file headers. They do not add to the
        namespace (unlike Name fields).

        Code snippets in param fields will often be user-defined
        vars, especially condition names. Can also be expressions like
        random(1,100). Hard to know what will be problematic.
        But its always the case that self-reference is wrong.
        """
        # first check if there's anything to validate (and return if not)

        def _checkParamUpdates(parent):
            """Checks whether param allows updates. Returns bool."""
            if parent.params[self.fieldName].allowedUpdates is not None:
                # Check for new set with elements common to lists compared - True if any elements are common
                return bool(set(parent.params[self.fieldName].allowedUpdates) & set(allowedUpdates))

        def _highlightParamVal(parent, error=False):
            """Highlights text containing error - defaults to black"""
            try:
                if error:
                    parent.paramCtrls[self.fieldName].valueCtrl.SetForegroundColour("Red")
                else:
                    parent.paramCtrls[self.fieldName].valueCtrl.SetForegroundColour("Black")
            except KeyError:
                pass

        control = self.GetWindow()
        if not hasattr(control, 'GetValue'):
            return '', True
        val = control.GetValue()  # same as parent.params[self.fieldName].val
        if not isinstance(val, basestring):
            return '', True

        field = self.fieldName
        msg, OK = '', True  # until we find otherwise
        _highlightParamVal(parent)
        codeWanted = psychopy.experiment.utils.unescapedDollarSign_re.search(val)
        isCodeField = bool(parent.params[self.fieldName].valType == 'code')
        allowedUpdates = ['set every repeat', 'set every frame']

        # Check if variable incorrectly defined in correct answer
        allKeyBoardKeys = list(key._key_names.values()) + [str(num) for num in range(10)]
        if self.fieldName == 'correctAns' and not val.startswith('$'):
            if ',' in val:  # comma separated
                keyList = val.upper().split(',')
                keyList = [thisKey.replace(' ', '') for thisKey in keyList if len(thisKey) > 0]
            else:  # whitespace separated
                keyList = val.upper().split(' ')
                keyList = [thisKey.replace(' ', '') for thisKey in keyList if len(thisKey) > 0]

            potentialVars = list(set(keyList) - set(allKeyBoardKeys))  # Elements of keyList not in allKeyBoardKeys
            _highlightParamVal(parent, bool(potentialVars))
            if len(potentialVars):
                msg = _translate("It looks like your 'Correct answer' contains a variable - prepend variables with '$' e.g. ${val}")
                msg = msg.format(val=potentialVars[0].lower())

        if codeWanted or isCodeField:
            # get var names from val, check against namespace:
            code = experiment.getCodeFromParamStr(val)
            try:
                names = compile(code, '', 'exec').co_names
            except (SyntaxError, TypeError) as e:
                # empty '' compiles to a syntax error, ignore
                if not code.strip() == '':
                    _highlightParamVal(parent, True)
                    msg = _translate('Python syntax error in field `{}`:  {}')
                    msg = msg.format(self.displayName, code)
                    OK = False
            else:
                # Check whether variable param entered as a constant
                if isCodeField and _checkParamUpdates(parent):
                    if parent.paramCtrls[self.fieldName].getUpdates() not in allowedUpdates:
                        try:
                            eval(code)
                        except NameError as e:
                            _highlightParamVal(parent, True)
                            msg = _translate(f"Looks like your variable '{code}' in '{self.displayName}' should be set to update.")
                            msg = msg.format(code=code, displayName=self.displayName)
                        except SyntaxError as e:
                            msg = ''

                # namespace = parent.frame.exp.namespace
                # parent.params['name'].val is not in namespace for new params
                # and is not fixed as .val until dialog closes. Use getvalue()
                # to handle dynamic changes to Name field:
                if 'name' in parent.paramCtrls:  # some components don't have names
                    parentName = parent.paramCtrls['name'].getValue()
                    for name in names:
                        # `name` means a var name within a compiled code snippet
                        if name == parentName:
                            _highlightParamVal(parent, True)
                            msg = _translate(
                                'Python var `{}` in `{}` is same as Name')
                            msg = msg.format(name, self.displayName)
                            OK = True

                    for newName in names:
                        namespace = parent.frame.exp.namespace
                        if newName in [*namespace.user, *namespace.builder, *namespace.constants]:
                            continue
                        used = namespace.exists(newName)
                        sameAsOldName = bool(newName == parent.params['name'].val)
                        if used and not sameAsOldName:
                            msg = _translate("Variable name $%s is in use (by %s). Try another name.") % (newName, _translate(used))
                            # let the user continue if this is what they intended
                            OK = True

        parent.warningsDict[field] = msg
        return msg, OK