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.
from pathlib import Path
import sys
import argparse
from dataclasses import dataclass

import pytest

sys.path.append(str(Path(__file__).parents[2]))

from pyodide_build.pywasmcross import handle_command  # noqa: E402
from pyodide_build.pywasmcross import f2c  # noqa: E402
from pyodide_build.pywasmcross import make_parser


@dataclass
class BuildArgs:
    """An object to hold build arguments"""

    cflags: str = ""
    cxxflags: str = ""
    ldflags: str = ""
    host: str = ""
    replace_libs: str = ""
    install_dir: str = ""


def _args_wrapper(func):
    """Convert function to take as input / return a string instead of a
    list of arguments

    Also sets dryrun=True
    """

    def _inner(line, *pargs):
        args = line.split()
        res = func(args, *pargs, dryrun=True)
        if hasattr(res, "__len__"):
            return " ".join(res)
        else:
            return res

    return _inner


handle_command_wrap = _args_wrapper(handle_command)
f2c_wrap = _args_wrapper(f2c)


def test_handle_command():
    args = BuildArgs()
    assert handle_command_wrap("gcc -print-multiarch", args) is None
    assert handle_command_wrap("gcc test.c", args) == "emcc test.c"
    assert (
        handle_command_wrap("gcc -shared -c test.o -o test.so", args)
        == "emcc -c test.o -o test.so"
    )

    # check cxxflags injection and cpp detection
    args = BuildArgs(
        cflags="-I./lib2",
        cxxflags="-std=c++11",
        ldflags="-lm",
    )
    assert (
        handle_command_wrap("gcc -I./lib1 test.cpp -o test.o", args)
        == "em++ -I./lib2 -std=c++11 -I./lib1 test.cpp -o test.o"
    )

    # check ldflags injection
    args = BuildArgs(
        cflags="", cxxflags="", ldflags="-lm", host="", replace_libs="", install_dir=""
    )
    assert (
        handle_command_wrap("gcc -shared -c test.o -o test.so", args)
        == "emcc -lm -c test.o -o test.so"
    )

    # check library replacement and removal of double libraries
    args = BuildArgs(
        replace_libs="bob=fred",
    )
    assert (
        handle_command_wrap("gcc -shared test.o -lbob -ljim -ljim -o test.so", args)
        == "emcc test.o -lfred -ljim -o test.so"
    )

    # compilation checks in numpy
    assert handle_command_wrap("gcc /usr/file.c", args) is None


@pytest.mark.parametrize(
    "in_ext, out_ext, executable, flag_name",
    [
        (".c", ".o", "emcc", "cflags"),
        (".cpp", ".o", "em++", "cxxflags"),
        (".c", ".so", "emcc", "ldflags"),
    ],
)
def test_handle_command_optflags(in_ext, out_ext, executable, flag_name):
    # Make sure that when multiple optflags are present those in cflags,
    # cxxflags, or ldflags has priority

    args = BuildArgs(**{flag_name: "-Oz"})
    assert (
        handle_command_wrap(f"gcc -O3 test.{in_ext} -o test.{out_ext}", args)
        == f"{executable} -Oz test.{in_ext} -o test.{out_ext}"
    )


def test_f2c():
    assert f2c_wrap("gfortran test.f") == "gfortran test.c"
    assert f2c_wrap("gcc test.c") is None
    assert f2c_wrap("gfortran --version") is None
    assert (
        f2c_wrap("gfortran --shared -c test.o -o test.so")
        == "gfortran --shared -c test.o -o test.so"
    )


def test_conda_compiler_compat():
    args = BuildArgs()
    assert handle_command_wrap(
        "gcc -shared -c test.o -B /compiler_compat -o test.so", args
    ) == ("emcc -c test.o -o test.so")


def test_environment_var_substitution(monkeypatch):
    monkeypatch.setenv("PYODIDE_BASE", "pyodide_build_dir")
    monkeypatch.setenv("BOB", "Robert Mc Roberts")
    monkeypatch.setenv("FRED", "Frederick F. Freddertson Esq.")
    monkeypatch.setenv("JIM", "James Ignatius Morrison:Jimmy")
    call_args = 'pywasmcross.py --ldflags "-l$(PYODIDE_BASE)" --cxxflags $(BOB) --cflags $(FRED) --replace-libs $(JIM)'
    monkeypatch.setattr(sys, "argv", call_args.split(" "))
    parser = argparse.ArgumentParser()
    make_parser(parser)
    args = parser.parse_args()
    assert (
        args.cflags == "Frederick F. Freddertson Esq."
        and args.cxxflags == "Robert Mc Roberts"
        and args.ldflags == '"-lpyodide_build_dir"'
        and args.replace_libs == "James Ignatius Morrison:Jimmy"
    )

from pathlib import Path
import sys
import argparse
from dataclasses import dataclass

import pytest

sys.path.append(str(Path(__file__).parents[2]))

from pyodide_build.pywasmcross import handle_command  # noqa: E402
from pyodide_build.pywasmcross import f2c  # noqa: E402
from pyodide_build.pywasmcross import make_parser


@dataclass
class BuildArgs:
    """An object to hold build arguments"""

    cflags: str = ""
    cxxflags: str = ""
    ldflags: str = ""
    host: str = ""
    replace_libs: str = ""
    install_dir: str = ""


def _args_wrapper(func):
    """Convert function to take as input / return a string instead of a
    list of arguments

    Also sets dryrun=True
    """

    def _inner(line, *pargs):
        args = line.split()
        res = func(args, *pargs, dryrun=True)
        if hasattr(res, "__len__"):
            return " ".join(res)
        else:
            return res

    return _inner


handle_command_wrap = _args_wrapper(handle_command)
f2c_wrap = _args_wrapper(f2c)


def test_handle_command():
    args = BuildArgs()
    assert handle_command_wrap("gcc -print-multiarch", args) is None
    assert handle_command_wrap("gcc test.c", args) == "emcc test.c"
    assert (
        handle_command_wrap("gcc -shared -c test.o -o test.so", args)
        == "emcc -c test.o -o test.so"
    )

    # check cxxflags injection and cpp detection
    args = BuildArgs(
        cflags="-I./lib2",
        cxxflags="-std=c++11",
        ldflags="-lm",
    )
    assert (
        handle_command_wrap("gcc -I./lib1 test.cpp -o test.o", args)
        == "em++ -I./lib2 -std=c++11 -I./lib1 test.cpp -o test.o"
    )

    # check ldflags injection
    args = BuildArgs(
        cflags="", cxxflags="", ldflags="-lm", host="", replace_libs="", install_dir=""
    )
    assert (
        handle_command_wrap("gcc -shared -c test.o -o test.so", args)
        == "emcc -lm -c test.o -o test.so"
    )

    # check library replacement and removal of double libraries
    args = BuildArgs(
        replace_libs="bob=fred",
    )
    assert (
        handle_command_wrap("gcc -shared test.o -lbob -ljim -ljim -o test.so", args)
        == "emcc test.o -lfred -ljim -o test.so"
    )

    # compilation checks in numpy
    assert handle_command_wrap("gcc /usr/file.c", args) is None


@pytest.mark.parametrize(
    "in_ext, out_ext, executable, flag_name",
    [
        (".c", ".o", "emcc", "cflags"),
        (".cpp", ".o", "em++", "cxxflags"),
        (".c", ".so", "emcc", "ldflags"),
    ],
)
def test_handle_command_optflags(in_ext, out_ext, executable, flag_name):
    # Make sure that when multiple optflags are present those in cflags,
    # cxxflags, or ldflags has priority

    args = BuildArgs(**{flag_name: "-Oz"})
    assert (
        handle_command_wrap(f"gcc -O3 test.{in_ext} -o test.{out_ext}", args)
        == f"{executable} -Oz test.{in_ext} -o test.{out_ext}"
    )


def test_f2c():
    assert f2c_wrap("gfortran test.f") == "gfortran test.c"
    assert f2c_wrap("gcc test.c") is None
    assert f2c_wrap("gfortran --version") is None
    assert (
        f2c_wrap("gfortran --shared -c test.o -o test.so")
        == "gfortran --shared -c test.o -o test.so"
    )


def test_conda_compiler_compat():
    args = BuildArgs()
    assert handle_command_wrap(
        "gcc -shared -c test.o -B /compiler_compat -o test.so", args
    ) == ("emcc -c test.o -o test.so")


def test_environment_var_substitution(monkeypatch):
    monkeypatch.setenv("PYODIDE_BASE", "pyodide_build_dir")
    monkeypatch.setenv("BOB", "Robert Mc Roberts")
    monkeypatch.setenv("FRED", "Frederick F. Freddertson Esq.")
    monkeypatch.setenv("JIM", "James Ignatius Morrison:Jimmy")
    call_args = 'pywasmcross.py --ldflags "-l$(PYODIDE_BASE)" --cxxflags $(BOB) --cflags $(FRED) --replace-libs $(JIM)'
    monkeypatch.setattr(sys, "argv", call_args.split(" "))
    parser = argparse.ArgumentParser()
    make_parser(parser)
    args = parser.parse_args()
    assert (
        args.cflags == "Frederick F. Freddertson Esq."
        and args.cxxflags == "Robert Mc Roberts"
        and args.ldflags == '"-lpyodide_build_dir"'
        and args.replace_libs == "James Ignatius Morrison:Jimmy"
    )
