Repository: pyodide/pyodide

Issue:
Global namespace not accessible within a function
## 🐛 Bug

A globally declared variable cannot be read in a function:

```
globVar = "Hello"
def test():
    print(globVar)

test()
```
gives error:
```
NameError: name 'globVar' is not defined
```
Interestingly, when used with the `global` keyword, it works:

```
globVar = "Hello"
def test():
    global globVar
    print(globVar)

test()
```
prints
```
Hello
```

This issue only happens when the python code is passed to the function `pyodide.pyodide_py.code.eval_code`. The bug is not present when the code is executed with `pyodide.runPython`, such as in the [console.html](https://pyodide.org/en/stable/console.html).

### To Reproduce

Run a slightly modified demo from the [Getting Started page](https://pyodide.org/en/stable/usage/quickstart.html) of pyodide:

```
<!doctype html>
<html>
  <head>
      <script src="https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.js"></script>
  </head>
  <body>
    Pyodide test page <br>
    Open your browser console to see Pyodide output
    <script type="text/javascript">
      async function main(){
        let pyodide = await loadPyodide();
        console.log(pyodide.pyodide_py.code.eval_code(`
            globVar = "Hello"
            def test():
                print(globVar)

            test()
        `));
      }
      main();
    </script>
  </body>
</html>
```

### Expected behavior

When the page above is opened in a web browser, it should print "Hello" to console. Instead, it throws a NameError: name 'globVar' is not defined:

```
pyodide.asm.js:10 Uncaught (in promise) PythonError: Traceback (most recent call last):
  File "/lib/python3.10/_pyodide/_base.py", line 460, in eval_code
    .run(globals, locals)
  File "/lib/python3.10/_pyodide/_base.py", line 306, in run
    coroutine = eval(self.code, globals, locals)
  File "<exec>", line 6, in <module>
  File "<exec>", line 4, in test
NameError: name 'globVar' is not defined

    at new_error (pyodide.asm.js:10:180810)
    at pyodide.asm.wasm:0xe78a8
    at pyodide.asm.wasm:0xe79ad
    at Module._pythonexc2js (pyodide.asm.js:10:929422)
    at Module.callPyObjectKwargs (pyodide.asm.js:10:123563)
    at Module.callPyObject (pyodide.asm.js:10:123772)
    at PyProxyClass.apply (pyodide.asm.js:10:134742)
    at Object.apply (pyodide.asm.js:10:133736)
    at main (bug.html:12:45)
new_error	@	pyodide.asm.js:10
$wrap_exception	@	pyodide.asm.wasm:0xe78a8
$pythonexc2js	@	pyodide.asm.wasm:0xe79ad
Module._pythonexc2js	@	pyodide.asm.js:10
Module.callPyObjectKwargs	@	pyodide.asm.js:10
Module.callPyObject	@	pyodide.asm.js:10
apply	@	pyodide.asm.js:10
apply	@	pyodide.asm.js:10
main	@	bug.html:12
await in main (async)		
(anonymous)	@	bug.html:20
```


### Environment

- Pyodide Version 0.22.1:
- Browser version : Version 110.0.5481.77 (Official Build) (64-bit)

Build from commit `5ef5622414f0fe8ab8c54694e04ab1b13939b7d4` (tagged `0.22.1`), but can be reproduced as well with the CDN build on https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.js

### Additional context

The issue only happens when the code is invoked using the [eval_code](https://github.com/pyodide/pyodide/blob/main/src/py/_pyodide/_base.py#L362) function. 

The documentation for the function assumes the `eval_code` can be called with `globals` argument set to `None`, and then an empty dictionary will be created. We relied on this feature since pyodide v0.17. In pyodide v0.18, the issue with global variables appeared and we tested and encountered the issue again in version v0.22.1. Is this behavior of hiding global variables a bug?
```
    globals :
        The global scope in which to execute code. This is used as the
        ``globals`` parameter for :py:func:`exec`. If ``globals`` is absent, a new
        empty dictionary is used.
``` 


Discussion:
It seems the JS API doesn't suffer from this problem, as the `runPython` never calls `eval_code` without a `globals` argument. If caller doesn't provide `globals`, the API draws some `API.globals` it has at hand, see: https://github.com/pyodide/pyodide/blob/main/src/js/api.ts#L74

Is there any reason why this is done this way? Is it correct if the caller relies on the `eval_code` function to create empty globals for him if parameter `globals` is absent?
I think this matches the behavior of normal eval/exec, which is the intended behavior.
I tested with Python 3.10.8, and the `exec` handles the code just right. Can this be a problem specific to pyodide?
```
$ python
Python 3.10.8 (main, Nov  1 2022, 14:18:21) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> exec("""
... globVar = "Hello"
... def test():
...     print(globVar)
... 
... test()
... """)
Hello
```
Huh okay then it's definitely not the intended behavior. Thanks for the report!
I'm sorry I didn't read your original report carefully enough -- I thought `globVar` was defined outside of the exec'd string not inside of it.
It seems to depend on which file executes `eval`. Start with:
```py
cr = CodeRunner(
    """
    globVar = "Hello"
    def test():
        print(globVar)

    test()
    """,
    return_mode="none"
).compile()
```

Works:
```py
eval(cr.code, None, None)
```
Doesn't work:
```py
cr.run()
```
Doesn't work: change the definition in `_base.py` of `run()` to:
```py
def run(self, *args):
   eval(self.code, None, None)
```
Does work: monkey patch `run` to be the definition above, but do the monkey patch in the current file.
Doesn't work: monkey patch `run` in a different file.
If `globals` is `None`, then `PyEval_GetGlobals` uses `PyEval_GetGlobals()` to determine the value of `globals`.
https://github.com/python/cpython/blob/main/Python/bltinmodule.c?plain=1#L920

This is sensitive to the file. I'm still not sure why it causes this problem though.
Minimal reproduction:

file1.py:
```py
def eval_code(code):
   eval(code)
```

file2.py:
```py
from file1 import eval_code

code = compile(
    """
globVar = "Hello"
def test():
    print(globVar)

test()
    """, "<exec>", "exec", 0
)
eval_code(code)
```
Okay the problem is caused by the behavior of the locals argument. If you change it to `eval(code, None, globals())` it works.
Another symptom of this:
```py
from pyodide.code import eval_code

eval_code("print(self)")
```
prints `<_pyodide._base.CodeRunner object at 0xptr>`.

Resolving patch:
diff --git a/src/py/_pyodide/_base.py b/src/py/_pyodide/_base.py
--- a/src/py/_pyodide/_base.py
+++ b/src/py/_pyodide/_base.py
@@ -298,6 +298,10 @@ def run(
             ``quiet_trailing_semicolon`` parameters to modify this default
             behavior.
         """
+        if globals is None:
+            globals = {}
+        if locals is None:
+            locals = globals
         if not self._compiled:
             raise RuntimeError("Not yet compiled")
         if self.code is None:
@@ -347,6 +351,10 @@ async def run_async(
             ``quiet_trailing_semicolon`` parameters to modify this default
             behavior.
         """
+        if globals is None:
+            globals = {}
+        if locals is None:
+            locals = globals
         if not self._compiled:
             raise RuntimeError("Not yet compiled")
         if self.code is None:


Test patch:
diff --git a/src/tests/test_pyodide.py b/src/tests/test_pyodide.py
--- a/src/tests/test_pyodide.py
+++ b/src/tests/test_pyodide.py
@@ -197,6 +197,20 @@ def test_eval_code_locals():
         eval_code("invalidate_caches()", globals, globals)
     eval_code("invalidate_caches()", globals, locals)
 
+    # See https://github.com/pyodide/pyodide/issues/3578
+    with pytest.raises(NameError):
+        eval_code("print(self)")
+
+    res = eval_code(
+        """
+        var = "Hello"
+        def test():
+            return var
+        test()
+        """
+    )
+    assert res == "Hello"
+
 
 def test_unpack_archive(selenium_standalone):
     selenium = selenium_standalone