Repository: psychopy/psychopy

Issue:
ioHub doesn't start in fresh install due to decoding errors
I installed psychopy into a fresh anaconda installation exactly as in https://www.psychopy.org/download.html#conda. The basic demos work (e.g. hello_world), but the ioHub demos don't work. When starting them from the console, I get the following error:

```
```(psychopy) $ python launchHub.py 

------------------------------------

Running testWithNoKwargs Test:
Traceback (most recent call last):
  File "src/gevent/greenlet.py", line 854, in gevent._gevent_cgreenlet.Greenlet.run
  File "/home/matthias/anaconda3/envs/psychopy/lib/python3.6/site-packages/gevent/baseserver.py", line 34, in _handle_and_close_when_done
    return handle(*args_tuple)
  File "/home/matthias/anaconda3/envs/psychopy/lib/python3.6/site-packages/psychopy/iohub/server.py", line 76, in handle
    request_type = unicode(request.pop(0), 'utf-8') # convert bytes to string for compatibility
TypeError: decoding str is not supported
2020-07-17T14:00:54Z <Greenlet at 0x7f768bdabd48: _handle_and_close_when_done(<bound method udpServer.handle of <udpServer at 0x, <bound method BaseServer.do_close of <udpServer at, (b'\x91\xb0GET_IOHUB_STATUS', ('127.0.0.1', 58533))> failed with TypeError

Traceback (most recent call last):
  File "src/gevent/greenlet.py", line 854, in gevent._gevent_cgreenlet.Greenlet.run
  File "/home/matthias/anaconda3/envs/psychopy/lib/python3.6/site-packages/gevent/baseserver.py", line 34, in _handle_and_close_when_done
    return handle(*args_tuple)
  File "/home/matthias/anaconda3/envs/psychopy/lib/python3.6/site-packages/psychopy/iohub/server.py", line 76, in handle
    request_type = unicode(request.pop(0), 'utf-8') # convert bytes to string for compatibility
TypeError: decoding str is not supported
2020-07-17T14:00:54Z <Greenlet at 0x7f768bdabd48: _handle_and_close_when_done(<bound method udpServer.handle of <udpServer at 0x, <bound method BaseServer.do_close of <udpServer at, (b'\x91\xb0GET_IOHUB_STATUS', ('127.0.0.1', 58533))> failed with TypeError

[repeating for ever]
```
I debugged a little bit and figured out that `request.pop(0)` is already a string which of course can't be decoded. I don't know where the request comes from, maybe this is a compatibility issue (the environment.yml file provided by psychopy doesn't provide any versions for the required libraries).


Discussion:
It seems to work in the standalone version under Mac, which adds more evidence that this is a compatibility issue. Is there any way to figure out which dependencies the standalone version is using? (maybe @mdcutone knows?)
The standalone mac versions of greenlet and gevent are as below:
```
>>> greenlet.__version__
'0.4.13'
>>> gevent.__version__
'1.2.2'
```
Presumably gevent used to sent/received in bytes and left it to the user to convert to unicdoe but now converts automatically. What I've done in the past to handle these is format into a string in the native format:
```python
request_type = "{}".format(unicode(request.pop(0), 'utf-8'))
```

@peircej Thanks! unfortunately, downgrading gevent and greenlet to the versions you mentioned doesn't solve the problem. It's still exactly the same error.

Resolving patch:
diff --git a/psychopy/app/coder/coder.py b/psychopy/app/coder/coder.py
--- a/psychopy/app/coder/coder.py
+++ b/psychopy/app/coder/coder.py
@@ -2264,6 +2264,8 @@ def expectedModTime(self, doc):
         filename = doc.filename
         if not os.path.exists(filename):
             return True
+        if not os.path.isabs(filename):
+            return True
         actualModTime = os.path.getmtime(filename)
         expectedModTime = doc.fileModTime
         if actualModTime != expectedModTime:
@@ -2492,10 +2494,12 @@ def paste(self, event):
             foc.Paste()
 
     def undo(self, event):
-        self.currentDoc.Undo()
+        if self.currentDoc:
+            self.currentDoc.Undo()
 
     def redo(self, event):
-        self.currentDoc.Redo()
+        if self.currentDoc:
+            self.currentDoc.Redo()
 
     def commentSelected(self, event):
         self.currentDoc.commentLines()
diff --git a/psychopy/experiment/loops.py b/psychopy/experiment/loops.py
--- a/psychopy/experiment/loops.py
+++ b/psychopy/experiment/loops.py
@@ -199,6 +199,8 @@ def writeLoopStartCodeJS(self, buff, modular):
         nReps = self.params['nReps'].val
         if nReps in ['None', None, 'none', '']:
             nReps = 'undefined'
+        elif isinstance(nReps, str):
+            nReps = nReps.strip("$")
 
         code = ("\nfunction {funName}LoopBegin(thisScheduler) {{\n"
                 "  // set up handler to look after randomisation of conditions etc\n"
diff --git a/psychopy/iohub/server.py b/psychopy/iohub/server.py
--- a/psychopy/iohub/server.py
+++ b/psychopy/iohub/server.py
@@ -73,7 +73,10 @@ def handle(self, request, replyTo):
         self.feed(request)
         request = self.unpack()
         # print2err(">> Rx Packet: {}, {}".format(request, replyTo))
-        request_type = unicode(request.pop(0), 'utf-8') # convert bytes to string for compatibility
+        request_type = request.pop(0)
+        if not isinstance(request_type, unicode):
+            request_type = unicode(request_type, 'utf-8') # convert bytes to string for compatibility
+
         if request_type == 'SYNC_REQ':
             self.sendResponse(['SYNC_REPLY', getTime()], replyTo)
             return True
@@ -209,7 +212,9 @@ def handleGetEvents(self, replyTo):
             return False
 
     def handleExperimentDeviceRequest(self, request, replyTo):
-        request_type = unicode(request.pop(0), 'utf-8') # convert bytes to string for compatibility
+        request_type = request.pop(0)
+        if not isinstance(request_type, unicode):
+            request_type = unicode(request_type, 'utf-8') # convert bytes to string for compatibility
         io_dev_dict = ioServer.deviceDict
         if request_type == 'EVENT_TX':
             exp_events = request.pop(0)
@@ -219,8 +224,12 @@ def handleExperimentDeviceRequest(self, request, replyTo):
             self.sendResponse(('EVENT_TX_RESULT', len(exp_events)), replyTo)
             return True
         elif request_type == 'DEV_RPC':
-            dclass = unicode(request.pop(0), 'utf-8')
-            dmethod = unicode(request.pop(0), 'utf-8')
+            dclass = request.pop(0)
+            if isinstance(dclass, unicode):
+                dclass = unicode(dclass, 'utf-8')
+            dmethod = request.pop(0)
+            if isinstance(dmethod, unicode):
+                dmethod = unicode(dmethod, 'utf-8')
             args = None
             kwargs = None
             if len(request) == 1:
@@ -287,7 +296,9 @@ def handleExperimentDeviceRequest(self, request, replyTo):
                 return False
 
         elif request_type == 'GET_DEV_INTERFACE':
-            dclass = unicode(request.pop(0), 'utf-8')
+            dclass = request.pop(0)
+            if not isinstance(dclass, unicode):
+                dclass = unicode(dclass, 'utf-8')
             data = None
             if dclass in ['EyeTracker', 'DAQ']:
                 for dname, hdevice in ioServer.deviceDict.items():


Test patch: