Repository: alexmojaki/pure_eval

Issue:
TypeError for malformed metaclass example
In https://github.com/ipython/ipython/issues/13481, the following example used to show a fatal error in IPython:
```python
class X(type):
    def __prepare__(cls, *args, **kwargs):
        return []
class Y(metaclass=X):
    pass
```

If I try the same example with friendly-traceback, I also get a fatal error, with the following as part of a long traceback:
```
    During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "LOCAL:\pure_eval\core.py", line 445, in group_expressions
    for node, value in expressions:
  File "FRIENDLY:\info_variables.py", line 119, in <genexpr>
    for nodes, obj in group_expressions(
  File "LOCAL:\pure_eval\core.py", line 358, in find_expressions
    value = self[node]
  File "LOCAL:\pure_eval\core.py", line 68, in __getitem__
    self._cache[node] = result = self._handle(node)
  File "LOCAL:\pure_eval\core.py", line 89, in _handle
    return self.names[node.id]
TypeError: list indices must be integers or slices, not str
```

In https://github.com/friendly-traceback/friendly-traceback/commit/276ec1b85f7c5949b0e5d1fb325b30b59b57d9c5, I've guarded against this type of fatal error.

I didn't see any evidence that the IPython crash is caused by pure_eval or an other library of yours, but I thought you might want to know about it - and possibly include some safeguards in pure_eval.




Comments:

Patch:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index fb89d46..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-dist: xenial
-language: python
-sudo: false
-
-python:
-  - 3.5
-  - 3.6
-  - 3.7
-  - 3.8-dev
-  - 3.9-dev
-
-env:
-  global:
-    - PURE_EVAL_SLOW_TESTS=1
-    - COVERALLS_PARALLEL=true
-
-before_install:
-  - pip install --upgrade coveralls setuptools>=44 setuptools_scm>=3.4.3 pep517
-
-install:
-  - pip install ".[tests]"
-
-script:
-  - coverage run --branch --include='pure_eval/*' -m pytest --junitxml=./rspec.xml
-  - coverage report -m
-
-after_success:
-  - coveralls
-
-notifications:
-  webhooks: https://coveralls.io/webhook
-  email: false
diff --git a/MANIFEST.in b/MANIFEST.in
index 800dfd8..09204c8 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1,3 @@
 include LICENSE.txt
 include pure_eval/py.typed
+include README.md
diff --git a/make_release.sh b/make_release.sh
index f9b8308..f0c1ac8 100755
--- a/make_release.sh
+++ b/make_release.sh
@@ -26,5 +26,5 @@ export TAG="v${1}"
 git tag "${TAG}"
 git push origin master "${TAG}"
 rm -rf ./build ./dist
-python3 -m pep517.build -bs .
+python -m build --sdist --wheel .
 twine upload ./dist/*.whl dist/*.tar.gz
diff --git a/pure_eval/core.py b/pure_eval/core.py
index 0a0381e..748f051 100644
--- a/pure_eval/core.py
+++ b/pure_eval/core.py
@@ -15,6 +15,7 @@ from pure_eval.utils import (
     of_standard_types,
     is_any,
     of_type,
+    ensure_dict,
 )
 
 
@@ -39,9 +40,9 @@ class Evaluator:
         """
 
         return cls(ChainMap(
-            frame.f_locals,
-            frame.f_globals,
-            frame.f_builtins,
+            ensure_dict(frame.f_locals),
+            ensure_dict(frame.f_globals),
+            ensure_dict(frame.f_builtins),
         ))
 
     def __getitem__(self, node: ast.expr) -> Any:
diff --git a/pure_eval/utils.py b/pure_eval/utils.py
index 139d6dd..a8a3730 100644
--- a/pure_eval/utils.py
+++ b/pure_eval/utils.py
@@ -189,3 +189,13 @@ def copy_ast_without_context(x):
         return list(map(copy_ast_without_context, x))
     else:
         return x
+
+
+def ensure_dict(x):
+    """
+    Handles invalid non-dict inputs
+    """
+    try:
+        return dict(x)
+    except Exception:
+        return {}
diff --git a/setup.cfg b/setup.cfg
index 330cb29..3d07ca9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -14,6 +14,7 @@ classifiers =
     Programming Language :: Python :: 3.7
     Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
+    Programming Language :: Python :: 3.10
     License :: OSI Approved :: MIT License
     Operating System :: OS Independent
 
@@ -22,7 +23,7 @@ packages = pure_eval
 install_requires =
 include_package_data = True
 tests_require = pytest
-setup_requires = setuptools>=44; wheel; setuptools_scm[toml]>=3.4.3
+setup_requires = setuptools>=44; setuptools_scm[toml]>=3.4.3
 
 [options.extras_require]
 tests = pytest
diff --git a/tox.ini b/tox.ini
index aa83fa0..3feff03 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py{35,36,37,38,39}
+envlist = py{35,36,37,38,39,310}
 
 [testenv]
 commands = pytest


Test patch:
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
new file mode 100644
index 0000000..7f68be5
--- /dev/null
+++ b/.github/workflows/pytest.yml
@@ -0,0 +1,36 @@
+name: Tests
+on: [push, pull_request]
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: [3.7, 3.8, 3.9, 3.10-dev]
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v2
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: run tests
+      env:
+        PURE_EVAL_SLOW_TESTS: 1
+      run: |
+        pip install -U pip
+        pip install --upgrade coveralls setuptools setuptools_scm pep517
+        pip install .[tests]
+        coverage run --source pure_eval -m pytest
+        coverage report -m
+    - name: Coveralls Python
+      uses: AndreMiras/coveralls-python-action@v20201129
+      with:
+        parallel: true
+        flag-name: test-${{ matrix.python-version }}
+  coveralls_finish:
+    needs: build
+    runs-on: ubuntu-latest
+    steps:
+      - name: Coveralls Finished
+        uses: AndreMiras/coveralls-python-action@v20201129
+        with:
+          parallel-finished: true
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 315ecc5..172f50e 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -17,6 +17,7 @@ from pure_eval.utils import (
     safe_name,
     typing_annotation_samples,
     is_standard_types,
+    ensure_dict,
 )
 
 
@@ -126,3 +127,10 @@ def test_is_standard_types():
     assert is_standard_types(lst, deep=False, check_dict_values=True)
     assert is_standard_types(lst[0], deep=True, check_dict_values=True)
     assert not is_standard_types(lst, deep=True, check_dict_values=True)
+
+
+def test_ensure_dict():
+    assert ensure_dict({}) == {}
+    assert ensure_dict([]) == {}
+    assert ensure_dict('foo') == {}
+    assert ensure_dict({'a': 1}) == {'a': 1}