[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/pythonnative/pythonnative/main/src/pythonnative/sdk/codegen.py [Back]  [Original]

"""Compile Python contracts into typed native interfaces and documentation."""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from ..bridge.codec import to_jsonable
from .native_types import NativeTypes, identifier
from .schema import COMPONENTS, fingerprint, manifest


def generate(destination: str | Path) -> list[Path]:
    """Write deterministic Swift, Kotlin, Python, and browser contract artifacts."""
    destination = Path(destination)
    destination.mkdir(parents=True, exist_ok=True)
    spec = to_jsonable(manifest())
    encoded = json.dumps(spec, indent=2, sort_keys=True) + "\n"
    types = NativeTypes()
    swift = ["import Foundation", "import UIKit", ""]
    kotlin = [
        "package com.pythonnative.generated",
        "",
        "import android.view.View",
        "import org.json.JSONObject",
        "import com.pythonnative.runtime.components.PNEvents",
        "",
    ]

    # A View prop is shared (a getter on the base class, and a type named after
    # View) when every component that declares it declares the same schema.
    # Compare JSON forms: extension manifests arrive through JSON, so a
    # built-in tuple and an extension's list must still count as equal, or a
    # plugin build would rename the built-in types hand-written code uses.
    def canonical(field: Any) -> str:
        return json.dumps(to_jsonable(field), sort_keys=True, default=str)

    view_props = dict(COMPONENTS["View"].props) if "View" in COMPONENTS else {}
    common = {
        key: field
        for key, field in view_props.items()
        if all(
            key not in component.props or canonical(component.props[key]) == canonical(field)
            for component in COMPONENTS.values()
        )
    }

    def getters(fields: dict[str, Any], hint: str) -> None:
        for key, field in fields.items():
            if field.get("native", {}).get("python_only"):
                continue
            _, st, kt = types.types(field, hint + identifier(key))
            swift.extend(
                [
                    f'    public var has_{key}: Bool {{ values["{key}"] != nil }}',
                    f'    public private(set) lazy var `{key}`: {st.rstrip("?")}? = {{ guard let value = values["{key}"], !(value is NSNull) else {{ return nil }}; return PNValues.decodeOrRecord({st.rstrip("?")}.self, value, field: "{key}") }}()',  # noqa: E501
                ]
            )
            decoded = types.kotlin_decode(field, f'values.get("{key}")', hint + identifier(key))
            kotlin.extend(
                [
                    f'    val has_{key}: Boolean get() = values.has("{key}")',
                    f'    val `{key}`: {kt.rstrip("?")}? by lazy(LazyThreadSafetyMode.NONE) {{ if (PNValues.isNull(values.opt("{key}"))) null else PNValues.decodeOrRecord("{key}") {{ {decoded} }} }}',  # noqa: E501
                ]
            )

    swift.extend(
        [
            "open class PNViewProps {",
            "    public let values: [String: Any]",
            "    public init(_ values: [String: Any]) { self.values = values }",
        ]
    )
    kotlin.append("open class PNViewProps(val values: JSONObject) {")
    getters(common, "View")
    swift.extend(["}", ""])
    kotlin.extend(["}", ""])
    python = []
    docs = [
        "# Native contracts",
        "",
        f"Runtime fingerprint: `{fingerprint()}`.",
        "",
        "These contracts describe native host views. Composed Python components use these views internally.",
        "",
    ]
    sw_events, kt_events = ["public enum PNComponentEvents {"], ["object PNComponentEvents {"]
    for name, component in sorted(COMPONENTS.items()):
        if not name.isidentifier():
            raise ValueError(f"Invalid native component name {name!r}")
        swift.extend(
            [
                f"public final class {name}Props: PNViewProps, PNNativeProps {{",
                "    public init(_ values: [String: Any], partial: Bool = true, validated: Bool = false) throws {",
                f'        guard validated || PNContracts.validate("{name}", partial ? values.filter {{ !($0.value is NSNull) }} : values, partial: partial) else {{ throw NativeDecodeError.invalid("{name} props") }}',  # noqa: E501
                "        super.init(values)",
                "    }",
            ]
        )
        kotlin.extend(
            [
                f"class {name}Props(values: JSONObject, partial: Boolean = true, "
                "validated: Boolean = false): PNViewProps(values) {",
                f'    init {{ require(validated || PNContracts.validate("{name}", if (partial) PNValues.withoutNulls(values) else values, partial)) {{ "Invalid {name} props" }} }}',  # noqa: E501
            ]
        )
        parameters, values = [], []
        sw_events.append(f"    public enum {name} {{")
        kt_events.append(f"    object {name} {{")
        fields = dict(component.props)
        refresh = fields.get("refresh_control", {}).get("properties", {})
        for key, field in fields.items():
            if not key.isidentifier():
                raise ValueError(f"Invalid property name {key!r}")
            hint = name + identifier(key)
            pt, st, kt = types.types(field, hint)
            default = "" if key in component.required else " = UNSET"
            parameters.append(f"{key}: {pt}{' | UnsetType' if default else ''}{default}")
            values.append(f'"{key}": {key}')
        getters({key: value for key, value in fields.items() if key not in common}, name)
        for key, field in (fields | {key: value for key, value in refresh.items() if key.startswith("on_")}).items():
            alternatives = field.get("anyOf", [field])
            event = next((item for item in alternatives if item.get("type") == "event"), None)
            if event is None:
                continue
            arguments = event.get("arguments")
            if arguments is None:
                sw_events.append(
                    f'        public static func `{key}`(_ view: UIView, _ arguments: [Any?] = []) {{ PNEvents.emitIfWired(view, "{key}", arguments) }}'  # noqa: E501
                )
                kt_events.append(
                    f'        fun `{key}`(view: android.view.View, vararg arguments: Any?): Boolean = PNEvents.fire(view, "{key}", *arguments)'  # noqa: E501
                )
                continue
            swparams, ktparams, swvalues, ktvalues = [], [], [], []
            for index, argument in enumerate(arguments):
                _, st, kt = types.types(argument, name + identifier(key) + "Argument" + str(index))
                swparams.append(f"_ argument{index}: {st}")
                ktparams.append(f"argument{index}: {kt}")
                swvalues.append(f"PNValues.encode(argument{index})")
                ktvalues.append(f"PNValues.encode(argument{index})")
            sw_events.append(
                f"        public static func `{key}`("
                + ", ".join(["_ view: UIView"] + swparams)
                + f') {{ PNEvents.emitIfWired(view, "{key}", ['
                + ", ".join(swvalues)
                + "]) }"
            )
            kt_events.append(
                f"        fun `{key}`("
                + ", ".join(["view: android.view.View"] + ktparams)
                + f'): Boolean = PNEvents.fire(view, "{key}"'
                + (", " if ktvalues else "")
                + ", ".join(ktvalues)
                + ")"
            )
        swift.extend(["}", ""])
        kotlin.extend(["}", ""])
        sw_events.append("    }")
        kt_events.append("    }")
        python.extend(
            [
                f"def {name}(*children: Node, " + ", ".join(parameters + ["key: str | None = None"]) + ") -> Element:",
                f'    """Create a validated {name} native element."""',
                "    props = {" + ", ".join(values) + "}",
                "    props = {name: value for name, value in props.items() if value is not UNSET}",
                f'    COMPONENTS["{name}"].validate(props)',
                f'    return Element("{name}", props, children, key=key)',
                "",
            ]
        )
        docs.extend(
            [
                f"## {name}",
                "",
                f"Measurement: {component.measurement}.",
                "",
                "| Property | Type | Platforms | Affects layout | Creation only |",
                "| --- | --- | --- | --- | --- |",
            ]
        )
        for key, field in component.props.items():
            metadata = field.get("native", {})
            docs.append(
                f"| `{key}` | `{types.types(field, name + identifier(key))[0]}` | {', '.join(metadata.get('platforms', ('ios', 'android', 'web')))} | {metadata.get('invalidates_layout', False)} | {metadata.get('recreate', False)} |"  # noqa: E501
            )
        docs.append("")
    sw_events.append("}")
    kt_events.append("}")
    from .module_codegen import generate_modules

    modules = generate_modules(types)
    headers = [
        '"""Generated Python interfaces. Regenerate from the declarations."""',
        "",
        "# ruff: noqa: E501, E402, F401, I001",
        "# fmt: off",
        "from __future__ import annotations",
        "from typing import Any, Callable, Literal",
        "from pythonnative.mutations import UNSET, UnsetType",
        "from pythonnative.sdk.types import decode_value, resolve_type",
        *types.python_imports(),
        "",
    ]
    templates = Path(__file__).with_name("templates")
    outputs: dict[str, Any] = {
        "schema.json": encoded,
        "schema.js": "export default " + encoded.strip() + ";\n",
        "NativeProps.swift": "\n".join(swift + sw_events),
        "NativeProps.kt": "\n".join(kotlin + kt_events),
        "NativeValues.swift": (templates / "values.swift").read_text() + "\n" + "\n".join(types.swift),
        "NativeValues.kt": (templates / "values.kt").read_text() + "\n" + "\n".join(types.kotlin),
        "components.py": "\n".join(
            headers
            + ["from pythonnative.element import Element, Node", "from pythonnative.sdk.schema import COMPONENTS", ""]
            + python
        ),
        "README.md": "\n".join(docs),
        **modules,
    }
    outputs["modules.py"] = "\n".join(
        headers + ["from pythonnative.native_modules.registry import native_module", "", modules["modules.py"]]
    )
    from .contract_codegen import ContractCompiler

    for extension in ("swift", "kt"):
        template = (templates / f"contracts.{extension}").read_text(encoding="utf-8")
        outputs[f"PNContracts.{extension}"] = ContractCompiler(kotlin=extension == "kt").generate(
            spec, fingerprint(), template
        )
    paths = []
    for name, contents in outputs.items():
        if name.endswith(".py"):
            contents = contents.rstrip() + "\n"
        path = destination / name
        path.write_text(contents, encoding="utf-8")
        paths.append(path)
    return paths

Web Proxy Viewer  |  New URL  |  Original Page