ci: support development releases of Python (#3419)

* ci: support development releases of Python

* fix: better PyPy support

* fix: patch over a few more pypy issues

* Try to patch

* Properly follow pep667

* Fix typo

* Whoops, 667 not in yet

* For testing

* More testing

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Try to backport

* Try to simplify fix

* Nail down the fix

* Try pypy workaround

* Typo

* one last typo

* Replacing 0x03110000 with 0x030B0000

* Add TODO. Drop PyPy

* Fix typo

* Revert catch upgrade

* fix: minor cleanup, try pypy again

Co-authored-by: Aaron Gokaslan <skylion.aaron@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ralf W. Grosse-Kunstleve <rwgk@google.com>
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2888c42..dcc61ec 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,16 +30,14 @@
         - '3.6'
         - '3.9'
         - '3.10'
-        # - '3.11-dev'
-        - 'pypy-3.7-v7.3.5'
-        # - 'pypy-3.8'
+        - 'pypy-3.7-v7.3.7'
+        - 'pypy-3.8-v7.3.7'
 
         # Items in here will either be added to the build matrix (if not
         # present), or add new keys to an existing matrix element if all the
         # existing keys match.
         #
-        # We support three optional keys: args (both build), args1 (first
-        # build), and args2 (second build).
+        # We support an optional keys: args, for cmake args
         include:
           # Just add a key
           - runs-on: ubuntu-latest
@@ -122,7 +120,7 @@
       run: git clean -fdx
 
     # Second build - C++17 mode and in a build directory
-    - name: Configure ${{ matrix.args2 }}
+    - name: Configure C++17
       run: >
         cmake -S . -B build2
         -DPYBIND11_WERROR=ON
@@ -130,7 +128,6 @@
         -DDOWNLOAD_EIGEN=ON
         -DCMAKE_CXX_STANDARD=17
         ${{ matrix.args }}
-        ${{ matrix.args2 }}
 
     - name: Build
       run: cmake --build build2 -j 2
diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml
new file mode 100644
index 0000000..6549474
--- /dev/null
+++ b/.github/workflows/upstream.yml
@@ -0,0 +1,112 @@
+
+name: Upstream
+
+on:
+  workflow_dispatch:
+  pull_request:
+
+concurrency:
+  group: upstream-${{ github.ref }}
+  cancel-in-progress: true
+
+env:
+  PIP_ONLY_BINARY: numpy
+
+jobs:
+  standard:
+    name: "🐍 3.11 dev • ubuntu-latest • x64"
+    runs-on: ubuntu-latest
+    if: "contains(github.event.pull_request.labels.*.name, 'python dev')"
+
+    steps:
+    - uses: actions/checkout@v2
+
+    - name: Setup Python 3.11
+      uses: actions/setup-python@v2
+      with:
+        python-version: "3.11-dev"
+
+    - name: Setup Boost (Linux)
+      if: runner.os == 'Linux'
+      run: sudo apt-get install libboost-dev
+
+    - name: Update CMake
+      uses: jwlawson/actions-setup-cmake@v1.11
+
+    - name: Prepare env
+      run: |
+        python -m pip install -r tests/requirements.txt
+
+    - name: Setup annotations on Linux
+      if: runner.os == 'Linux'
+      run: python -m pip install pytest-github-actions-annotate-failures
+
+    # First build - C++11 mode and inplace
+    - name: Configure C++11
+      run: >
+        cmake -S . -B .
+        -DPYBIND11_WERROR=ON
+        -DDOWNLOAD_CATCH=ON
+        -DDOWNLOAD_EIGEN=ON
+        -DCMAKE_CXX_STANDARD=11
+
+    - name: Build C++11
+      run: cmake --build . -j 2
+
+    - name: Python tests C++11
+      run: cmake --build . --target pytest -j 2
+
+    - name: C++11 tests
+      run: cmake --build .  --target cpptest -j 2
+
+    - name: Interface test C++11
+      run: cmake --build . --target test_cmake_build
+
+    - name: Clean directory
+      run: git clean -fdx
+
+    # Second build - C++17 mode and in a build directory
+    - name: Configure C++17
+      run: >
+        cmake -S . -B build2
+        -DPYBIND11_WERROR=ON
+        -DDOWNLOAD_CATCH=ON
+        -DDOWNLOAD_EIGEN=ON
+        -DCMAKE_CXX_STANDARD=17
+        ${{ matrix.args }}
+        ${{ matrix.args2 }}
+
+    - name: Build
+      run: cmake --build build2 -j 2
+
+    - name: Python tests
+      run: cmake --build build2 --target pytest
+
+    - name: C++ tests
+      run: cmake --build build2 --target cpptest
+
+    # Third build - C++17 mode with unstable ABI
+    - name: Configure (unstable ABI)
+      run: >
+        cmake -S . -B build3
+        -DPYBIND11_WERROR=ON
+        -DDOWNLOAD_CATCH=ON
+        -DDOWNLOAD_EIGEN=ON
+        -DCMAKE_CXX_STANDARD=17
+        -DPYBIND11_INTERNALS_VERSION=10000000
+        "-DPYBIND11_TEST_OVERRIDE=test_call_policies.cpp;test_gil_scoped.cpp;test_thread.cpp"
+        ${{ matrix.args }}
+
+    - name: Build (unstable ABI)
+      run: cmake --build build3 -j 2
+
+    - name: Python tests (unstable ABI)
+      run: cmake --build build3 --target pytest
+
+    - name: Interface test
+      run: cmake --build build2 --target test_cmake_build
+
+    # This makes sure the setup_helpers module can build packages using
+    # setuptools
+    - name: Setuptools helpers test
+      run: pytest tests/extra_setuptools
diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h
index 4b39827..097d41b 100644
--- a/include/pybind11/cast.h
+++ b/include/pybind11/cast.h
@@ -150,7 +150,8 @@
             return false;
         } else {
             handle src_or_index = src;
-#if PY_VERSION_HEX < 0x03080000
+            // PyPy: 7.3.7's 3.8 does not implement PyLong_*'s __index__ calls.
+#if PY_VERSION_HEX < 0x03080000 || defined(PYPY_VERSION)
             object index;
             if (!PYBIND11_LONG_CHECK(src.ptr())) {  // So: index_check(src.ptr())
                 index = reinterpret_steal<object>(PyNumber_Index(src.ptr()));
diff --git a/include/pybind11/eval.h b/include/pybind11/eval.h
index 6cc672e..4248551 100644
--- a/include/pybind11/eval.h
+++ b/include/pybind11/eval.h
@@ -19,11 +19,11 @@
 PYBIND11_NAMESPACE_BEGIN(detail)
 
 inline void ensure_builtins_in_globals(object &global) {
-    #if PY_VERSION_HEX < 0x03080000
+    #if defined(PYPY_VERSION) || PY_VERSION_HEX < 0x03080000
         // Running exec and eval on Python 2 and 3 adds `builtins` module under
         // `__builtins__` key to globals if not yet present.
         // Python 3.8 made PyRun_String behave similarly. Let's also do that for
-        // older versions, for consistency.
+        // older versions, for consistency. This was missing from PyPy3.8 7.3.7.
         if (!global.contains("__builtins__"))
             global["__builtins__"] = module_::import(PYBIND11_BUILTINS_MODULE);
     #else
diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h
index c2b8868..cfe03f6 100644
--- a/include/pybind11/pybind11.h
+++ b/include/pybind11/pybind11.h
@@ -2365,8 +2365,9 @@
 
     /* Don't call dispatch code if invoked from overridden function.
        Unfortunately this doesn't work on PyPy. */
-#if !defined(PYPY_VERSION)
-
+#if !defined(PYPY_VERSION) && PY_VERSION_HEX < 0x030B0000
+    // TODO: Remove PyPy workaround for Python 3.11.
+    // Current API fails on 3.11 since co_varnames can be null.
 #if PY_VERSION_HEX >= 0x03090000
     PyFrameObject *frame = PyThreadState_GetFrame(PyThreadState_Get());
     if (frame != nullptr) {
@@ -2374,7 +2375,7 @@
         // f_code is guaranteed to not be NULL
         if ((std::string) str(f_code->co_name) == name && f_code->co_argcount > 0) {
             PyObject* locals = PyEval_GetLocals();
-            if (locals != nullptr) {
+            if (locals != nullptr && f_code->co_varnames != nullptr) {
                 PyObject *self_caller = dict_getitem(
                     locals, PyTuple_GET_ITEM(f_code->co_varnames, 0)
                 );
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 8d2742a..98ca46d 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -2,7 +2,7 @@
 numpy==1.19.0; platform_python_implementation=="PyPy" and sys_platform=="linux" and python_version=="3.6"
 numpy==1.20.0; platform_python_implementation=="PyPy" and sys_platform=="linux" and python_version=="3.7"
 numpy==1.19.3; platform_python_implementation!="PyPy" and python_version=="3.6"
-numpy==1.21.3; platform_python_implementation!="PyPy" and python_version>="3.7"
+numpy==1.21.3; platform_python_implementation!="PyPy" and python_version>="3.7" and python_version<"3.11"
 py @ git+https://github.com/pytest-dev/py; python_version>="3.11"
 pytest==4.6.9; python_version<"3.5"
 pytest==6.1.2; python_version=="3.5"
diff --git a/tests/test_buffers.py b/tests/test_buffers.py
index adf7cad..0877dc0 100644
--- a/tests/test_buffers.py
+++ b/tests/test_buffers.py
@@ -36,6 +36,10 @@
 
 
 # https://foss.heptapod.net/pypy/pypy/-/issues/2444
+# TODO: fix on recent PyPy
+@pytest.mark.xfail(
+    env.PYPY, reason="PyPy 7.3.7 doesn't clear this anymore", stict=False
+)
 def test_to_python():
     mat = m.Matrix(5, 4)
     assert memoryview(mat).shape == (5, 4)
diff --git a/tests/test_builtin_casters.py b/tests/test_builtin_casters.py
index 2a061c1..9c5e17a 100644
--- a/tests/test_builtin_casters.py
+++ b/tests/test_builtin_casters.py
@@ -299,7 +299,8 @@
     assert noconvert(7) == 7
     cant_convert(3.14159)
     # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar)
-    if (3, 8) <= env.PY < (3, 10):
+    # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7)
+    if (3, 8) <= env.PY < (3, 10) and env.CPYTHON:
         with env.deprecated_call():
             assert convert(Int()) == 42
     else:
@@ -334,7 +335,9 @@
 
     # The implicit conversion from np.float32 is undesirable but currently accepted.
     # TODO: Avoid DeprecationWarning in `PyLong_AsLong` (and similar)
-    if (3, 8) <= env.PY < (3, 10):
+    # TODO: PyPy 3.8 does not behave like CPython 3.8 here yet (7.3.7)
+    # https://github.com/pybind/pybind11/issues/3408
+    if (3, 8) <= env.PY < (3, 10) and env.CPYTHON:
         with env.deprecated_call():
             assert convert(np.float32(3.14159)) == 3
     else:
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index 3821ead..56201a8 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -3,7 +3,7 @@
 
 import pytest
 
-import env  # noqa: F401
+import env
 import pybind11_cross_module_tests as cm
 from pybind11_tests import exceptions as m
 
@@ -97,6 +97,8 @@
         return f
 
 
+# TODO: find out why this fails on PyPy, https://foss.heptapod.net/pypy/pypy/-/issues/3583
+@pytest.mark.xfail(env.PYPY, reason="Failure on PyPy 3.8 (7.3.7)", strict=False)
 @ignore_pytest_unraisable_warning
 def test_python_alreadyset_in_destructor(monkeypatch, capsys):
     hooked = False