Instead of trying to convince setup.py to do what we want, the wheel
package empowers us to unpack and directly manipulate the wheel.
In particular this is important because `--skip-build` really does
not work.

First we build a native wheel. If skip_host is True, then this
"native" wheel has defective empty files as binaries, if it's False,
we build a real native wheel. If skip_host is False, then we use
pip to install our native wheel into host directory.

Then we unpack the wheel, replace the platform tag with
wasm32_emscripten, remove the non-metadata files, and copy in the
new files with the cross compiled binaries.

Of course, this is all only necessary for binary extensions.


Hopefully this will help get us towards using alternative build
backends. The one concern is that this approach depends on
implementation details of where the build backend puts the files
into the directory tree before copying them into the wheel. If
the build backend moved the linked binaries after building them,
we would be in trouble. Or worse if the build backend deleted the
files after packing them into the wheel, we'd be in trouble.

Basically, it seems clear that we can only support alternative
build backends on a case-by-case basis.

@henryiii I'm curious what your thoughts are cross compilation + arbitrary build backends.

Better package cross compilation. Instead of trying to convince setup.py to do what we want, the wheel
package empowers us to unpack and directly manipulate the wheel.
In particular this is important because `--skip-build` really does
not work.

First we build a native wheel. If skip_host is True, then this
"native" wheel has defective empty files as binaries, if it's False,
we build a real native wheel. If skip_host is False, then we use
pip to install our native wheel into host directory.

Then we unpack the wheel, replace the platform tag with
wasm32_emscripten, remove the non-metadata files, and copy in the
new files with the cross compiled binaries.

Of course, this is all only necessary for binary extensions.


Hopefully this will help get us towards using alternative build
backends. The one concern is that this approach depends on
implementation details of where the build backend puts the files
into the directory tree before copying them into the wheel. If
the build backend moved the linked binaries after building them,
we would be in trouble. Or worse if the build backend deleted the
files after packing them into the wheel, we'd be in trouble.

Basically, it seems clear that we can only support alternative
build backends on a case-by-case basis.

@henryiii I'm curious what your thoughts are cross compilation + arbitrary build backends.

"""
Monkey patch autodoc to recursively include submodules as well. We have to
import the submodules for it to find them.
"""

from typing import Any

from sphinx.ext.autodoc import ModuleDocumenter, ObjectMember
from sphinx.util.inspect import safe_getattr

__all__ = ["monkeypatch_module_documenter"]


def get_module_members(module: Any) -> list[tuple[str, Any]]:
    members: dict[str, tuple[str, Any]] = {}
    for name in dir(module):
        try:
            value = safe_getattr(module, name, None)
            # Before patch this used to always do
            # members[name] = (name, value)
            # We want to also recursively look up names on submodules.
            if type(value).__name__ != "module":
                members[name] = (name, value)
                continue
            if name.startswith("_"):
                continue
            submodule = value  # Rename for clarity
            [base, _, rest] = submodule.__name__.partition(".")
            if not base == module.__name__:
                # Not part of package, don't document
                continue

            for (sub_name, sub_val) in get_module_members(submodule):
                # Skip names not in __all__
                if hasattr(submodule, "__all__") and sub_name not in submodule.__all__:
                    continue
                qual_name = rest + "." + sub_name
                members[qual_name] = (qual_name, sub_val)
            continue
        except AttributeError:
            continue

    return sorted(list(members.values()))


def get_object_members(self, want_all: bool):
    """
    For some reason I was unable to monkey patch get_module_members so we monkey
    patch get_object_members to call our updated `get_module_members`.

    This is similar to the original function but I dropped the `want_all` branch
    for brevity because we don't use it.
    """
    members = get_module_members(self.object)
    if not self.__all__:
        # for implicit module members, check __module__ to avoid
        # documenting imported objects
        return True, members
    else:
        ret = []
        for name, value in members:
            if name in self.__all__ or "." in name:
                ret.append(ObjectMember(name, value))
            else:
                ret.append(ObjectMember(name, value, skipped=True))

        return False, ret


def monkeypatch_module_documenter():
    ModuleDocumenter.get_object_members = get_object_members

"""
Monkey patch autodoc to recursively include submodules as well. We have to
import the submodules for it to find them.
"""

from typing import Any

from sphinx.ext.autodoc import ModuleDocumenter, ObjectMember
from sphinx.util.inspect import safe_getattr

__all__ = ["monkeypatch_module_documenter"]


def get_module_members(module: Any) -> list[tuple[str, Any]]:
    members: dict[str, tuple[str, Any]] = {}
    for name in dir(module):
        try:
            value = safe_getattr(module, name, None)
            # Before patch this used to always do
            # members[name] = (name, value)
            # We want to also recursively look up names on submodules.
            if type(value).__name__ != "module":
                members[name] = (name, value)
                continue
            if name.startswith("_"):
                continue
            submodule = value  # Rename for clarity
            [base, _, rest] = submodule.__name__.partition(".")
            if not base == module.__name__:
                # Not part of package, don't document
                continue

            for (sub_name, sub_val) in get_module_members(submodule):
                # Skip names not in __all__
                if hasattr(submodule, "__all__") and sub_name not in submodule.__all__:
                    continue
                qual_name = rest + "." + sub_name
                members[qual_name] = (qual_name, sub_val)
            continue
        except AttributeError:
            continue

    return sorted(list(members.values()))


def get_object_members(self, want_all: bool):
    """
    For some reason I was unable to monkey patch get_module_members so we monkey
    patch get_object_members to call our updated `get_module_members`.

    This is similar to the original function but I dropped the `want_all` branch
    for brevity because we don't use it.
    """
    members = get_module_members(self.object)
    if not self.__all__:
        # for implicit module members, check __module__ to avoid
        # documenting imported objects
        return True, members
    else:
        ret = []
        for name, value in members:
            if name in self.__all__ or "." in name:
                ret.append(ObjectMember(name, value))
            else:
                ret.append(ObjectMember(name, value, skipped=True))

        return False, ret


def monkeypatch_module_documenter():
    ModuleDocumenter.get_object_members = get_object_members
