FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

GitHub Viewer

"""Python definitions for native props, events, commands, and modules.""" from __future__ import annotations import collections.abc import dataclasses import enum import hashlib import inspect import json import math import types import typing from dataclasses import dataclass, field from typing import Any, Mapping @dataclass(frozen=True) class NativeField: """Native behavior attached to an annotated dataclass field. Use ``Annotated[T, NativeField(...)]`` for native-specific metadata. """ invalidates_layout: bool = False recreate: bool = False animated: bool = False platforms: tuple[str, ...] = ("ios", "android", "web") description: str = "" def type_schema(annotation: Any) -> dict[str, Any]: """Convert a Python annotation into a portable JSON type description.""" origin, args = typing.get_origin(annotation), typing.get_args(annotation) if origin is typing.Annotated: result = type_schema(args[0]) for metadata in args[1:]: if isinstance(metadata, NativeField): result["native"] = dataclasses.asdict(metadata) return result if origin in (typing.Union, types.UnionType): return {"anyOf": [type_schema(arg) for arg in args]} if origin is typing.Literal: return {"enum": list(args)} if annotation is type(None): return {"type": "null"} if annotation in (str, bool, int, float): return {"type": {str: "string", bool: "boolean", int: "integer", float: "number"}[annotation]} if origin in (list, tuple, set, collections.abc.Sequence): return {"type": "array", "items": type_schema(args[0]) if args else {}} if origin in (dict, collections.abc.Mapping) or annotation is dict: return {"type": "object", "additionalProperties": type_schema(args[-1]) if args else {}} if origin in (typing.Callable, collections.abc.Callable): return { "type": "event", "arguments": [type_schema(arg) for arg in args[0]] if args and args[0] is not Ellipsis else None, } if inspect.isclass(annotation) and issubclass(annotation, enum.Enum): return {"enum": [value.value for value in annotation]} if dataclasses.is_dataclass(annotation) or typing.is_typeddict(annotation): hints = typing.get_type_hints(annotation, include_extras=True) required = ( list(annotation.__required_keys__) if typing.is_typeddict(annotation) else [ f.name for f in dataclasses.fields(annotation) if f.default is dataclasses.MISSING and f.default_factory is dataclasses.MISSING ] ) return { "type": "object", "properties": {name: type_schema(value) for name, value in hints.items()}, "required": sorted(required), "additionalProperties": False, } if origin in (typing.Required, typing.NotRequired): return type_schema(args[0]) return {} def validate(value: Any, schema: Mapping[str, Any], path: str = "value") -> None: """Validate wire values without coercion or ambiguous boolean comparisons.""" if "anyOf" in schema: for alternative in schema["anyOf"]: try: validate(value, alternative, path) return except TypeError: pass raise TypeError(f"{path} does not match its annotation") if "enum" in schema: candidate = value.value if isinstance(value, enum.Enum) else value if not any( (type(candidate) is type(item) or type(candidate) in (int, float) and type(item) in (int, float)) and candidate == item for item in schema["enum"] ): raise TypeError(f"{path} must be one of {schema['enum']!r}") kind = schema.get("type") checks = { "null": lambda: value is None, "string": lambda: isinstance(value, str), "boolean": lambda: type(value) is bool, "integer": lambda: type(value) in (int, float) and math.isfinite(value) and value == int(value) and abs(value) ComponentSchema: """Describe a native widget with a frozen Python props dataclass.""" if not dataclasses.is_dataclass(props): raise TypeError("Native props must be a dataclass") hints = typing.get_type_hints(props, include_extras=True) required, defaults = [], {} for item in dataclasses.fields(props): if item.default is not dataclasses.MISSING: defaults[item.name] = item.default elif item.default_factory is not dataclasses.MISSING: defaults[item.name] = item.default_factory() else: required.append(item.name) return cls( name, {key: type_schema(value) for key, value in hints.items()}, tuple(required), defaults, measurement ) def validate(self, props: Mapping[str, Any], *, partial: bool = False, platform: str | None = None) -> None: """Validate a construction or a partial update against this contract.""" if not partial: missing = set(self.required) - props.keys() if missing: raise TypeError(f"{self.name} requires {', '.join(sorted(missing))}") for key, value in props.items(): if key not in self.props: raise TypeError(f"Unknown {self.name} prop {key!r}") platforms = self.props[key].get("native", {}).get("platforms") if platform is not None and platforms is not None and platform not in platforms: raise TypeError(f"{self.name}.{key} isn't supported on {platform}") if partial and value is None: if key in self.required: raise TypeError(f"Cannot remove required {self.name}.{key}") continue validate(value, self.props[key], f"{self.name}.{key}") def validate_event(self, name: str, arguments: list[Any]) -> None: """Validate a typed callback payload before application code receives it.""" if name.startswith("gesture:"): return schema = self.props.get(name, {}) if name == "on_refresh": schema = self.props.get("refresh_control", {}).get("properties", {}).get(name, {}) alternatives = schema.get("anyOf", [schema]) event = next((item for item in alternatives if item.get("type") == "event"), None) if event is None: raise TypeError(f"Unknown event {self.name}.{name}") expected = event.get("arguments") if expected is not None: if len(arguments) != len(expected): raise TypeError(f"{self.name}.{name} requires {len(expected)} event arguments") for index, (value, field) in enumerate(zip(arguments, expected)): validate(value, field, f"{self.name}.{name}[{index}]") @dataclass(frozen=True) class ModuleSchema: """Typed native module methods, arguments, results, and async behavior.""" name: str methods: dict[str, dict[str, Any]] @classmethod def from_protocol(cls, name: str, protocol: type) -> ModuleSchema: """Read annotated methods from a Python protocol or interface class.""" methods = {} for method_name, method in inspect.getmembers(protocol, inspect.isfunction): if method_name.startswith("_"): continue hints = typing.get_type_hints(method, include_extras=True) signature = inspect.signature(method) methods[method_name] = { "arguments": {key: type_schema(hints.get(key, Any)) for key in signature.parameters if key != "self"}, "result": type_schema(hints.get("return", Any)), "async": inspect.iscoroutinefunction(method), "required": [ key for key, param in signature.parameters.items() if key != "self" and param.default is inspect.Parameter.empty ], "defaults": { key: param.default for key, param in signature.parameters.items() if key != "self" and param.default is not inspect.Parameter.empty }, } return cls(name, methods) def validate_call(self, method: str, arguments: Mapping[str, Any]) -> dict[str, Any]: """Validate and normalize a method invocation before crossing a bridge.""" contract = self.methods.get(method) if contract is None: raise TypeError(f"Unknown command {self.name}.{method}") validate( arguments, { "type": "object", "properties": contract["arguments"], "required": contract.get("required", list(contract["arguments"])), "additionalProperties": False, }, f"{self.name}.{method}", ) return {**contract.get("defaults", {}), **arguments} def validate_result(self, method: str, result: Any) -> Any: """Reject a native result that doesn't satisfy its declared return type.""" contract = self.methods.get(method) if contract is None: raise TypeError(f"Unknown command {self.name}.{method}") validate(result, contract["result"], f"{self.name}.{method} result") return result COMPONENTS: dict[str, ComponentSchema] = {} MODULES: dict[str, ModuleSchema] = {} def register_schema(schema: ComponentSchema | ModuleSchema) -> None: """Register the contract used to generate and validate a native extension.""" if isinstance(schema, ComponentSchema): COMPONENTS[schema.name] = schema else: MODULES[schema.name] = schema def manifest() -> dict[str, Any]: """Return deterministic native metadata for code generation and tooling.""" return { "protocol": 2, "yoga": "3.2.1", "components": {name: dataclasses.asdict(value) for name, value in sorted(COMPONENTS.items())}, "modules": {name: dataclasses.asdict(value) for name, value in sorted(MODULES.items())}, } def fingerprint() -> str: """Hash the native contract; incompatible dev clients require rebuilding.""" return hashlib.sha256(json.dumps(manifest(), sort_keys=True, default=str).encode()).hexdigest() def load_manifest(document: Mapping[str, Any]) -> None: """Load declarative extension contracts without importing target binaries.""" if document.get("protocol") != 2 or document.get("yoga") != "3.2.1": raise ValueError("Native contracts require protocol 2 and Yoga 3.2.1") pending = [] for group, constructor, registered in ( ("components", ComponentSchema, COMPONENTS), ("modules", ModuleSchema, MODULES), ): for name, value in document.get(group, {}).items(): if not name.isidentifier() or value.get("name") != name: raise ValueError(f"Invalid native contract name: {name!r}") schema = constructor(**value) old = registered.get(name) if old is not None and json.dumps(dataclasses.asdict(old), sort_keys=True, default=str) != json.dumps( dataclasses.asdict(schema), sort_keys=True, default=str ): raise ValueError(f"Conflicting native contract: {name}") pending.append(schema) for schema in pending: register_schema(schema) def load_bundled_contracts() -> None: """Install the exact contracts compiled into this embedded application.""" from importlib.resources import files path = files("pythonnative").joinpath("_native_contracts.json") if path.is_file(): load_manifest(json.loads(path.read_text(encoding="utf-8")))

Back | FazBrowse Home | New Git URL