This changes the calling convention for calling a Javascript function from Python so that any proxied arguments are automatically destroyed at the end of the function call. This is a breaking change. It should make it much more convenient to use the foreign function interface. 

The docs need to be updated to explain the new calling convention before this can be merged. We should put a link to the appropriate section in the docs into the error message emitted when a borrowed proxy is used after it is free so that people have an easy time learning the model by trial and error.
ENH Destroy proxies created in JsProxy.__call__ when done with call. This changes the calling convention for calling a Javascript function from Python so that any proxied arguments are automatically destroyed at the end of the function call. This is a breaking change. It should make it much more convenient to use the foreign function interface. 

The docs need to be updated to explain the new calling convention before this can be merged. We should put a link to the appropriate section in the docs into the error message emitted when a borrowed proxy is used after it is free so that people have an easy time learning the model by trial and error.
import pytest


@pytest.fixture(scope="session")
def print_info():
    headings = [
        "browser",
        "py_limit",
        "py_usage",
        "js_depth",
        "py_depth",
        "js_depth/py_usage",
        "js_depth/py_depth",
    ]
    fmt = "## {{:{:d}s}}  {{:{:d}g}}  {{:{:d}.2f}}  {{:{:d}g}}  {{:{:d}g}}  {{:{:d}g}}  {{:{:d}g}}".format(
        *map(len, headings)
    )
    printed_heading = False

    def print_info(*args):
        nonlocal printed_heading
        if not printed_heading:
            printed_heading = True
            print("## " + "  ".join(headings))
        print(fmt.format(*args))

    yield print_info


@pytest.mark.skip_refcount_check
@pytest.mark.skip_pyproxy_check
def test_stack_usage(selenium, print_info):
    res = selenium.run_js(
        """
        self.measure_available_js_stack_depth = () => {
            let depth = 0;
            function recurse() { depth += 1; recurse(); }
            try { recurse(); } catch (err) { }
            return depth;
        };
        let py_limit = pyodide.runPython("import sys; sys.getrecursionlimit()");
        let py_usage = pyodide.runPython(`
            from js import measure_available_js_stack_depth
            def recurse(n):
                return measure_available_js_stack_depth() if n==0 else recurse(n-1)
            (recurse(0)-recurse(100))/100
        `);
        let js_depth = measure_available_js_stack_depth();
        self.py_depth = [0];
        try {
        pyodide.runPython(`
        import sys
        from js import py_depth
        sys.setrecursionlimit(2000)
        def infiniterecurse():
            py_depth[0] += 1
            infiniterecurse()
        infiniterecurse()
        `);
        } catch {}

        py_depth = py_depth[0];
        return [
            py_limit,
            py_usage,
            js_depth,
            py_depth,
            Math.floor(js_depth/py_usage),
            Math.floor(js_depth/py_depth),
        ]
        """
    )
    # "py_usage",
    # "js_depth",
    # "py_depth",
    # "js_depth/py_usage",
    # "js_depth/py_depth",

    print_info(selenium.browser, *res)

    selenium.clean_logs()

import pytest


@pytest.fixture(scope="session")
def print_info():
    headings = [
        "browser",
        "py_limit",
        "py_usage",
        "js_depth",
        "py_depth",
        "js_depth/py_usage",
        "js_depth/py_depth",
    ]
    fmt = "## {{:{:d}s}}  {{:{:d}g}}  {{:{:d}.2f}}  {{:{:d}g}}  {{:{:d}g}}  {{:{:d}g}}  {{:{:d}g}}".format(
        *map(len, headings)
    )
    printed_heading = False

    def print_info(*args):
        nonlocal printed_heading
        if not printed_heading:
            printed_heading = True
            print("## " + "  ".join(headings))
        print(fmt.format(*args))

    yield print_info


@pytest.mark.skip_refcount_check
@pytest.mark.skip_pyproxy_check
def test_stack_usage(selenium, print_info):
    res = selenium.run_js(
        """
        self.measure_available_js_stack_depth = () => {
            let depth = 0;
            function recurse() { depth += 1; recurse(); }
            try { recurse(); } catch (err) { }
            return depth;
        };
        let py_limit = pyodide.runPython("import sys; sys.getrecursionlimit()");
        let py_usage = pyodide.runPython(`
            from js import measure_available_js_stack_depth
            def recurse(n):
                return measure_available_js_stack_depth() if n==0 else recurse(n-1)
            (recurse(0)-recurse(100))/100
        `);
        let js_depth = measure_available_js_stack_depth();
        self.py_depth = [0];
        try {
        pyodide.runPython(`
        import sys
        from js import py_depth
        sys.setrecursionlimit(2000)
        def infiniterecurse():
            py_depth[0] += 1
            infiniterecurse()
        infiniterecurse()
        `);
        } catch {}

        py_depth = py_depth[0];
        return [
            py_limit,
            py_usage,
            js_depth,
            py_depth,
            Math.floor(js_depth/py_usage),
            Math.floor(js_depth/py_depth),
        ]
        """
    )
    # "py_usage",
    # "js_depth",
    # "py_depth",
    # "js_depth/py_usage",
    # "js_depth/py_depth",

    print_info(selenium.browser, *res)

    selenium.clean_logs()
