ENH Add --no-deps parameter to build-recipes CLI. ### Description

This adds `--no-deps` parameter to `pyodide build-recipes` CLI, which is a replacement for `pyodide_build buildpkg` entrypoint.

Ref: #2879

### Checklists

- [x] Add a [CHANGELOG](https://github.com/pyodide/pyodide/blob/main/docs/project/changelog.md) entry
- [x] Add / update tests

import gzip
import inspect
import json
import sys
from pathlib import Path

from docutils.frontend import OptionParser
from docutils.utils import new_document

if not hasattr(inspect, "getargspec"):
    inspect.getargspec = inspect.getfullargspec  # type: ignore[assignment]


from sphinx_js.suffix_tree import SuffixTree
from sphinx_js.typedoc import Analyzer as TsAnalyzer

test_directory = Path(__file__).resolve().parent
sys.path.append(str(test_directory.parent))
src_dir = test_directory.parents[2] / "src"


# tsdoc_dump.json.gz is the source file for the test docs. It can be updated as follows:
#
# cp src/core/pyproxy.ts src/js/pyproxy.gen.ts
# typedoc src/js/*.ts --tsconfig src/js/tsconfig.json --json docs/sphinx_pyodide/tests/tsdoc_dump.json
# gzip docs/sphinx_pyodide/tests/tsdoc_dump.json
# rm src/js/pyproxy.gen.ts
with gzip.open(test_directory / "tsdoc_dump.json.gz") as fh:
    jsdoc_json = json.load(fh)
settings_json = json.loads((test_directory / "app_settings.json").read_text())

from sphinx_pyodide.jsdoc import (
    PyodideAnalyzer,
    flatten_suffix_tree,
    get_jsdoc_content_directive,
    get_jsdoc_summary_directive,
)

inner_analyzer = TsAnalyzer(jsdoc_json, str(src_dir))
settings = OptionParser().get_default_values()
settings.update(settings_json, OptionParser())

document = new_document("", settings)
pyodide_analyzer = PyodideAnalyzer(inner_analyzer)


def test_flatten_suffix_tree():
    t = SuffixTree()
    d = {
        ("a", "b", "c"): 1,
        ("a", "b", "d"): 2,
        ("a", "d", "d"): 3,
        ("a", "x", "y"): 4,
        ("b", "x", "c"): 5,
        ("b", "x", "d"): 6,
        ("b", "y", "d"): 7,
    }
    t.add_many(d.items())
    r = flatten_suffix_tree(t._tree)
    assert d == r


class dummy_app:
    _sphinxjs_analyzer = pyodide_analyzer
    document = document


class dummy_state:
    document = document


def test_pyodide_analyzer():
    function_names = {x.name for x in pyodide_analyzer.js_docs["pyodide"]["function"]}
    attribute_names = {x.name for x in pyodide_analyzer.js_docs["pyodide"]["attribute"]}
    assert function_names == {
        "checkInterrupt",
        "isPyProxy",
        "loadPackage",
        "loadPackagesFromImports",
        "mountNativeFS",
        "pyimport",
        "registerComlink",
        "registerJsModule",
        "runPython",
        "runPythonAsync",
        "setDefaultStdout",
        "setInterruptBuffer",
        "setStderr",
        "setStdin",
        "setStdout",
        "toPy",
        "unpackArchive",
        "unregisterJsModule",
    }

    assert attribute_names == {
        "ERRNO_CODES",
        "FS",
        "PATH",
        "version",
        "globals",
        "loadedPackages",
        "pyodide_py",
        "version",
    }


def test_content():
    JsDocContent = get_jsdoc_content_directive(dummy_app)

    a = JsDocContent.__new__(JsDocContent)
    a.arguments = ["pyodide"]
    a.state = dummy_state

    def no_op_parse_rst(rst):
        return rst

    a.parse_rst = no_op_parse_rst

    results = {}
    for idx, entry in enumerate(a.run().split(".. js:")):
        [first_line, _, body] = entry.partition("\n")
        if "::" not in first_line:
            continue
        [directive, name] = first_line.split("::")
        directive = directive.strip()
        name = name.strip()
        if directive == "module":
            assert name == a.arguments[0]
            continue
        body = body.strip()
        d = dict(idx=idx, directive=directive, body=body, sig="")
        if "(" in name:
            [name, sig] = name.split("(")
            d["sig"] = sig
        results[name] = d

    rp = results["globals"]
    assert rp["directive"] == "attribute"
    assert rp["sig"] == ""
    assert "An alias to the global Python namespace." in rp["body"]

    rp = results["runPython"]
    assert rp["directive"] == "function"
    assert rp["sig"] == "code, options)"
    assert "Runs a string of Python code from JavaScript" in rp["body"]


JsDocSummary = get_jsdoc_summary_directive(dummy_app)
jsdoc_summary = JsDocSummary.__new__(JsDocSummary)
jsdoc_summary.state = dummy_state
jsdoc_summary.options = {}


def test_extract_summary():
    assert (
        jsdoc_summary.extract_summary(
            "Registers the Js object ``module`` as a Js module with ``name``. This module can then be imported from Python using the standard Python\nimport system. :func:`some_func`"
        )
        == "Registers the Js object ``module`` as a Js module with ``name``."
    )


def test_summary():
    globals = jsdoc_summary.get_summary_table(
        "globalThis", dummy_app._sphinxjs_analyzer.js_docs["globalThis"]["function"]
    )
    attributes = jsdoc_summary.get_summary_table(
        "pyodide", dummy_app._sphinxjs_analyzer.js_docs["pyodide"]["attribute"]
    )
    functions = jsdoc_summary.get_summary_table(
        "pyodide", dummy_app._sphinxjs_analyzer.js_docs["pyodide"]["function"]
    )
    globals = {t[1]: t for t in globals}
    attributes = {t[1]: t for t in attributes}
    functions = {t[1]: t for t in functions}
    assert globals["loadPyodide"] == (
        "**async** ",
        "loadPyodide",
        "(options)",
        "Load the main Pyodide wasm module and initialize it.",
        "globalThis.loadPyodide",
    )

    assert attributes["pyodide_py"] == (
        "",
        "pyodide_py",
        "",
        "An alias to the Python :py:mod:`pyodide` package.",
        "pyodide.pyodide_py",
    )
    assert attributes["version"] == (
        "",
        "version",
        "",
        "The Pyodide version.",
        "pyodide.version",
    )
    assert attributes["loadedPackages"] == (
        "",
        "loadedPackages",
        "",
        "The list of packages that Pyodide has loaded.",
        "pyodide.loadedPackages",
    )

    assert functions["loadPackagesFromImports"][:-2] == (
        "**async** ",
        "loadPackagesFromImports",
        "(code, options)",
    )


def test_type_name():
    tn = inner_analyzer._type_name
    assert tn({"name": "void", "type": "intrinsic"}) == ":js:data:`void`"
    assert tn({"value": None, "type": "literal"}) == "null"
    assert (
        tn(
            {
                "name": "Promise",
                "type": "reference",
                "typeArguments": [{"name": "string", "type": "intrinsic"}],
            }
        ).strip()
        == r":js:class:`Promise`\ **<**\ :js:data:`string`\ **>**"
    )

    assert (
        tn(
            {
                "asserts": False,
                "name": "jsobj",
                "targetType": {"name": "PyProxy", "type": "reference"},
                "type": "predicate",
            }
        ).strip()
        == ":js:data:`boolean` (typeguard for :js:class:`PyProxy`)"
    )

    assert (
        tn(
            {
                "declaration": {
                    "kindString": "Method",
                    "name": "messageCallback",
                    "signatures": [
                        {
                            "kindString": "Call signature",
                            "name": "messageCallback",
                            "parameters": [
                                {
                                    "flags": {},
                                    "kindString": "Parameter",
                                    "name": "message",
                                    "type": {"name": "string", "type": "intrinsic"},
                                }
                            ],
                            "type": {"name": "void", "type": "intrinsic"},
                        }
                    ],
                },
                "type": "reflection",
            }
        ).strip()
        == r"\ **(**\ \ **message:** :js:data:`string`\ **) =>** :js:data:`void`"
    )

    assert (
        tn(
            {
                "name": "Iterable",
                "type": "reference",
                "typeArguments": [
                    {
                        "elements": [
                            {
                                "element": {"name": "string", "type": "intrinsic"},
                                "isOptional": False,
                                "name": "key",
                                "type": "named-tuple-member",
                            },
                            {
                                "element": {"name": "any", "type": "intrinsic"},
                                "isOptional": False,
                                "name": "value",
                                "type": "named-tuple-member",
                            },
                        ],
                        "type": "tuple",
                    }
                ],
            }
        ).strip()
        == r":js:data:`Iterable`\ **<**\ \ **[**\ \ **key:** :js:data:`string`\ **,** \ **value:** :js:data:`any`\ **]** \ **>**"
    )

    assert (
        tn(
            {
                "declaration": {
                    "flags": {},
                    "indexSignature": {
                        "flags": {},
                        "kindString": "Index signature",
                        "parameters": [
                            {
                                "flags": {},
                                "name": "key",
                                "type": {"name": "string", "type": "intrinsic"},
                            }
                        ],
                        "type": {"name": "string", "type": "intrinsic"},
                    },
                    "kindString": "Type literal",
                },
                "type": "reflection",
            }
        ).strip()
        == r"""\ **{**\ \ **[key:** :js:data:`string`\ **]:** :js:data:`string`\ **}**\ """.strip()
    )

    assert (
        tn(
            {
                "declaration": {
                    "children": [
                        {
                            "flags": {},
                            "kindString": "Property",
                            "name": "cache",
                            "type": {"name": "PyProxyCache", "type": "reference"},
                        },
                        {
                            "flags": {"isOptional": True},
                            "kindString": "Property",
                            "name": "destroyed_msg",
                            "type": {"name": "string", "type": "intrinsic"},
                        },
                        {
                            "flags": {},
                            "kindString": "Property",
                            "name": "ptr",
                            "type": {"name": "number", "type": "intrinsic"},
                        },
                    ],
                    "flags": {},
                    "kindString": "Type literal",
                },
                "type": "reflection",
            }
        ).strip()
        == r"""\ **{**\ \ **cache:** :js:class:`PyProxyCache`\ **,** \ **destroyed_msg?:** :js:data:`string`\ **,** \ **ptr:** :js:data:`number`\ **}**\ """.strip()
    )

import gzip
import inspect
import json
import sys
from pathlib import Path

from docutils.frontend import OptionParser
from docutils.utils import new_document

if not hasattr(inspect, "getargspec"):
    inspect.getargspec = inspect.getfullargspec  # type: ignore[assignment]


from sphinx_js.suffix_tree import SuffixTree
from sphinx_js.typedoc import Analyzer as TsAnalyzer

test_directory = Path(__file__).resolve().parent
sys.path.append(str(test_directory.parent))
src_dir = test_directory.parents[2] / "src"


# tsdoc_dump.json.gz is the source file for the test docs. It can be updated as follows:
#
# cp src/core/pyproxy.ts src/js/pyproxy.gen.ts
# typedoc src/js/*.ts --tsconfig src/js/tsconfig.json --json docs/sphinx_pyodide/tests/tsdoc_dump.json
# gzip docs/sphinx_pyodide/tests/tsdoc_dump.json
# rm src/js/pyproxy.gen.ts
with gzip.open(test_directory / "tsdoc_dump.json.gz") as fh:
    jsdoc_json = json.load(fh)
settings_json = json.loads((test_directory / "app_settings.json").read_text())

from sphinx_pyodide.jsdoc import (
    PyodideAnalyzer,
    flatten_suffix_tree,
    get_jsdoc_content_directive,
    get_jsdoc_summary_directive,
)

inner_analyzer = TsAnalyzer(jsdoc_json, str(src_dir))
settings = OptionParser().get_default_values()
settings.update(settings_json, OptionParser())

document = new_document("", settings)
pyodide_analyzer = PyodideAnalyzer(inner_analyzer)


def test_flatten_suffix_tree():
    t = SuffixTree()
    d = {
        ("a", "b", "c"): 1,
        ("a", "b", "d"): 2,
        ("a", "d", "d"): 3,
        ("a", "x", "y"): 4,
        ("b", "x", "c"): 5,
        ("b", "x", "d"): 6,
        ("b", "y", "d"): 7,
    }
    t.add_many(d.items())
    r = flatten_suffix_tree(t._tree)
    assert d == r


class dummy_app:
    _sphinxjs_analyzer = pyodide_analyzer
    document = document


class dummy_state:
    document = document


def test_pyodide_analyzer():
    function_names = {x.name for x in pyodide_analyzer.js_docs["pyodide"]["function"]}
    attribute_names = {x.name for x in pyodide_analyzer.js_docs["pyodide"]["attribute"]}
    assert function_names == {
        "checkInterrupt",
        "isPyProxy",
        "loadPackage",
        "loadPackagesFromImports",
        "mountNativeFS",
        "pyimport",
        "registerComlink",
        "registerJsModule",
        "runPython",
        "runPythonAsync",
        "setDefaultStdout",
        "setInterruptBuffer",
        "setStderr",
        "setStdin",
        "setStdout",
        "toPy",
        "unpackArchive",
        "unregisterJsModule",
    }

    assert attribute_names == {
        "ERRNO_CODES",
        "FS",
        "PATH",
        "version",
        "globals",
        "loadedPackages",
        "pyodide_py",
        "version",
    }


def test_content():
    JsDocContent = get_jsdoc_content_directive(dummy_app)

    a = JsDocContent.__new__(JsDocContent)
    a.arguments = ["pyodide"]
    a.state = dummy_state

    def no_op_parse_rst(rst):
        return rst

    a.parse_rst = no_op_parse_rst

    results = {}
    for idx, entry in enumerate(a.run().split(".. js:")):
        [first_line, _, body] = entry.partition("\n")
        if "::" not in first_line:
            continue
        [directive, name] = first_line.split("::")
        directive = directive.strip()
        name = name.strip()
        if directive == "module":
            assert name == a.arguments[0]
            continue
        body = body.strip()
        d = dict(idx=idx, directive=directive, body=body, sig="")
        if "(" in name:
            [name, sig] = name.split("(")
            d["sig"] = sig
        results[name] = d

    rp = results["globals"]
    assert rp["directive"] == "attribute"
    assert rp["sig"] == ""
    assert "An alias to the global Python namespace." in rp["body"]

    rp = results["runPython"]
    assert rp["directive"] == "function"
    assert rp["sig"] == "code, options)"
    assert "Runs a string of Python code from JavaScript" in rp["body"]


JsDocSummary = get_jsdoc_summary_directive(dummy_app)
jsdoc_summary = JsDocSummary.__new__(JsDocSummary)
jsdoc_summary.state = dummy_state
jsdoc_summary.options = {}


def test_extract_summary():
    assert (
        jsdoc_summary.extract_summary(
            "Registers the Js object ``module`` as a Js module with ``name``. This module can then be imported from Python using the standard Python\nimport system. :func:`some_func`"
        )
        == "Registers the Js object ``module`` as a Js module with ``name``."
    )


def test_summary():
    globals = jsdoc_summary.get_summary_table(
        "globalThis", dummy_app._sphinxjs_analyzer.js_docs["globalThis"]["function"]
    )
    attributes = jsdoc_summary.get_summary_table(
        "pyodide", dummy_app._sphinxjs_analyzer.js_docs["pyodide"]["attribute"]
    )
    functions = jsdoc_summary.get_summary_table(
        "pyodide", dummy_app._sphinxjs_analyzer.js_docs["pyodide"]["function"]
    )
    globals = {t[1]: t for t in globals}
    attributes = {t[1]: t for t in attributes}
    functions = {t[1]: t for t in functions}
    assert globals["loadPyodide"] == (
        "**async** ",
        "loadPyodide",
        "(options)",
        "Load the main Pyodide wasm module and initialize it.",
        "globalThis.loadPyodide",
    )

    assert attributes["pyodide_py"] == (
        "",
        "pyodide_py",
        "",
        "An alias to the Python :py:mod:`pyodide` package.",
        "pyodide.pyodide_py",
    )
    assert attributes["version"] == (
        "",
        "version",
        "",
        "The Pyodide version.",
        "pyodide.version",
    )
    assert attributes["loadedPackages"] == (
        "",
        "loadedPackages",
        "",
        "The list of packages that Pyodide has loaded.",
        "pyodide.loadedPackages",
    )

    assert functions["loadPackagesFromImports"][:-2] == (
        "**async** ",
        "loadPackagesFromImports",
        "(code, options)",
    )


def test_type_name():
    tn = inner_analyzer._type_name
    assert tn({"name": "void", "type": "intrinsic"}) == ":js:data:`void`"
    assert tn({"value": None, "type": "literal"}) == "null"
    assert (
        tn(
            {
                "name": "Promise",
                "type": "reference",
                "typeArguments": [{"name": "string", "type": "intrinsic"}],
            }
        ).strip()
        == r":js:class:`Promise`\ **<**\ :js:data:`string`\ **>**"
    )

    assert (
        tn(
            {
                "asserts": False,
                "name": "jsobj",
                "targetType": {"name": "PyProxy", "type": "reference"},
                "type": "predicate",
            }
        ).strip()
        == ":js:data:`boolean` (typeguard for :js:class:`PyProxy`)"
    )

    assert (
        tn(
            {
                "declaration": {
                    "kindString": "Method",
                    "name": "messageCallback",
                    "signatures": [
                        {
                            "kindString": "Call signature",
                            "name": "messageCallback",
                            "parameters": [
                                {
                                    "flags": {},
                                    "kindString": "Parameter",
                                    "name": "message",
                                    "type": {"name": "string", "type": "intrinsic"},
                                }
                            ],
                            "type": {"name": "void", "type": "intrinsic"},
                        }
                    ],
                },
                "type": "reflection",
            }
        ).strip()
        == r"\ **(**\ \ **message:** :js:data:`string`\ **) =>** :js:data:`void`"
    )

    assert (
        tn(
            {
                "name": "Iterable",
                "type": "reference",
                "typeArguments": [
                    {
                        "elements": [
                            {
                                "element": {"name": "string", "type": "intrinsic"},
                                "isOptional": False,
                                "name": "key",
                                "type": "named-tuple-member",
                            },
                            {
                                "element": {"name": "any", "type": "intrinsic"},
                                "isOptional": False,
                                "name": "value",
                                "type": "named-tuple-member",
                            },
                        ],
                        "type": "tuple",
                    }
                ],
            }
        ).strip()
        == r":js:data:`Iterable`\ **<**\ \ **[**\ \ **key:** :js:data:`string`\ **,** \ **value:** :js:data:`any`\ **]** \ **>**"
    )

    assert (
        tn(
            {
                "declaration": {
                    "flags": {},
                    "indexSignature": {
                        "flags": {},
                        "kindString": "Index signature",
                        "parameters": [
                            {
                                "flags": {},
                                "name": "key",
                                "type": {"name": "string", "type": "intrinsic"},
                            }
                        ],
                        "type": {"name": "string", "type": "intrinsic"},
                    },
                    "kindString": "Type literal",
                },
                "type": "reflection",
            }
        ).strip()
        == r"""\ **{**\ \ **[key:** :js:data:`string`\ **]:** :js:data:`string`\ **}**\ """.strip()
    )

    assert (
        tn(
            {
                "declaration": {
                    "children": [
                        {
                            "flags": {},
                            "kindString": "Property",
                            "name": "cache",
                            "type": {"name": "PyProxyCache", "type": "reference"},
                        },
                        {
                            "flags": {"isOptional": True},
                            "kindString": "Property",
                            "name": "destroyed_msg",
                            "type": {"name": "string", "type": "intrinsic"},
                        },
                        {
                            "flags": {},
                            "kindString": "Property",
                            "name": "ptr",
                            "type": {"name": "number", "type": "intrinsic"},
                        },
                    ],
                    "flags": {},
                    "kindString": "Type literal",
                },
                "type": "reflection",
            }
        ).strip()
        == r"""\ **{**\ \ **cache:** :js:class:`PyProxyCache`\ **,** \ **destroyed_msg?:** :js:data:`string`\ **,** \ **ptr:** :js:data:`number`\ **}**\ """.strip()
    )
