Skip to content

Analyzer#

flet_pkg.core.analyzer #

Package analyzer for Dart-to-Python code generation.

Analyzes a DartPackageAPI and produces a GenerationPlan that drives the code generators: detects namespaces, events, properties, and maps types.

PackageAnalyzer #

PackageAnalyzer(min_namespace_methods: int = 2)

Analyzes a parsed Dart API and produces a generation plan.

The analyzer detects: - Namespaces: Groups of related methods (e.g., User, Notifications) that become separate Python sub-module files. - Events: Listener/Observer methods that become ft.EventHandler attrs. - Properties: Constructor params and getters that become dataclass fields. - Enums: Dart enums that become Python Enum classes.

Parameters:

Name Type Description Default
min_namespace_methods int

Minimum methods to justify a separate sub-module.

2

Initialise the analyzer.

Parameters:

Name Type Description Default
min_namespace_methods int

Minimum number of methods required to justify extracting a separate sub-module.

2
Source code in src/flet_pkg/core/analyzer.py
def __init__(self, min_namespace_methods: int = 2):
    """Initialise the analyzer.

    Args:
        min_namespace_methods: Minimum number of methods required to
            justify extracting a separate sub-module.
    """
    self.min_namespace_methods = min_namespace_methods
    self._known_types: frozenset[str] = frozenset()

analyze #

analyze(api: DartPackageAPI, control_name: str, extension_type: str, flutter_package: str = '', package_name: str = '', description: str = '') -> GenerationPlan

Analyze a Dart API and produce a GenerationPlan.

Source code in src/flet_pkg/core/analyzer.py
def analyze(
    self,
    api: DartPackageAPI,
    control_name: str,
    extension_type: str,
    flutter_package: str = "",
    package_name: str = "",
    description: str = "",
) -> GenerationPlan:
    """Analyze a Dart API and produce a GenerationPlan."""
    base_class = "ft.Service" if extension_type == "service" else "ft.LayoutControl"
    control_name_lower = control_name.lower()

    # Build set of known type names for type sanitization.
    # Includes re-exported types AND locally parsed enum names and
    # helper class names so they are not replaced with `dict | None`.
    known = set(api.reexported_types.keys())
    known.update(e.name for e in api.enums)
    known.update(h.name for h in api.helper_classes)
    self._known_types = frozenset(known)

    # Detect the main SDK class (the one matching the control name)
    dart_main_class = self._detect_main_class(api, control_name)

    plan = GenerationPlan(
        control_name=control_name,
        package_name=package_name,
        base_class=base_class,
        description=description,
        flutter_package=flutter_package,
        dart_import=f"package:{flutter_package}/{flutter_package}.dart",
        dart_main_class=dart_main_class,
    )

    # --- UI control path: widget constructor params → properties ---
    has_widget_classes = any(cls.constructor_params for cls in api.classes)
    if extension_type == "ui_control" and has_widget_classes:
        self._process_widget_classes(api, plan, control_name)
    else:
        # --- Service path: methods → async methods ---
        # Detect namespaces by class name prefix
        namespace_classes: dict[str, list[DartClass]] = {}
        main_classes: list[DartClass] = []

        # Single-class packages: the only class IS the main class, no sub-modules
        if len(api.classes) == 1:
            main_classes = api.classes[:]
        else:
            for cls in api.classes:
                ns = self._detect_namespace(cls, control_name, control_name_lower)
                if ns:
                    namespace_classes.setdefault(ns, []).append(cls)
                else:
                    main_classes.append(cls)

        # Infer properties from initialize() method on main class
        for cls in main_classes:
            self._infer_properties_from_initialize(cls, plan)

        # Infer properties from pre-init setter methods (consent, log level, etc.)
        self._infer_pre_init_properties(api, plan, control_name)

        # Process main classes: extract events, properties, methods
        for cls in main_classes:
            self._process_main_class(cls, plan, dart_main_class, api)

        # Detect sub-object namespaces (e.g., User.pushSubscription → fold into user)
        self._fold_sub_objects(api, namespace_classes, control_name, plan)

        # Process namespace classes into sub-modules
        for ns_name, classes in namespace_classes.items():
            # Extract events from namespace classes (events go to main plan)
            for cls in classes:
                for method in cls.methods:
                    event = self._detect_event(
                        method, control_name, api, dart_main_class, cls.name
                    )
                    if event and not any(
                        e.python_attr_name == event.python_attr_name for e in plan.events
                    ):
                        plan.events.append(event)

            sub_module = self._build_sub_module(
                ns_name, classes, control_name, dart_main_class, plan
            )
            if sub_module and len(sub_module.methods) >= self.min_namespace_methods:
                plan.sub_modules.append(sub_module)
            else:
                # Not enough methods for a sub-module; merge into main
                for cls in classes:
                    self._process_main_class(cls, plan, dart_main_class, api)

        # Process top-level functions as main methods
        for func in api.top_level_functions:
            method_plan = self._build_method_plan(func, "", dart_main_class)
            if not any(m.python_name == method_plan.python_name for m in plan.main_methods):
                plan.main_methods.append(method_plan)

    # Process enums — only keep those that are used or commonly useful
    # Start with any enums already added (e.g. family type enum)
    generated_type_names: set[str] = {e.python_name for e in plan.enums}
    for dart_enum in api.enums:
        plan.enums.append(
            EnumPlan(
                python_name=dart_enum.name,
                values=[(v, v.lower(), doc) for v, doc in dart_enum.values],
                docstring=dart_enum.docstring,
            )
        )
        generated_type_names.add(dart_enum.name)

    # Generate stub types for re-exported types from platform_interface
    # that are referenced by methods but not locally defined.
    self._generate_reexported_types(api, plan, generated_type_names)

    # Generate data classes from local helper classes (Configuration,
    # Options, Params, etc.) that are referenced as method parameters.
    self._generate_local_data_classes(api, plan, generated_type_names)

    # Post-process: re-sanitize types against actually-generated names.
    # Types from known_types that were NOT generated must be replaced
    # with dict | None (e.g. WorkoutRouteLocation → dict | None).
    # Include sub-control names so they don't get sanitized away.
    for sc in plan.sub_controls:
        generated_type_names.add(sc.control_name)
    _post_sanitize_property_types(plan, generated_type_names)

    # Set error event class name with short prefix
    prefix = "OS" if control_name.lower().startswith("one") else control_name
    plan.error_event_class = f"{prefix}ErrorEvent"

    # Build dart_listeners from events
    plan.dart_listeners = [
        {
            "event_name": event.dart_event_name,
            "python_attr": event.python_attr_name,
            "dart_listener_method": event.dart_listener_method,
            "dart_sdk_accessor": event.dart_sdk_accessor,
        }
        for event in plan.events
    ]

    return plan

resolve_platform_types #

resolve_platform_types(api: DartPackageAPI, plan: GenerationPlan) -> None

Download platform_interface packages and resolve stub types.

Replaces UNKNOWN-only enum stubs and data: dict stub data classes with real values/fields parsed from the source packages.

Source code in src/flet_pkg/core/analyzer.py
def resolve_platform_types(
    self,
    api: DartPackageAPI,
    plan: GenerationPlan,
) -> None:
    """Download platform_interface packages and resolve stub types.

    Replaces UNKNOWN-only enum stubs and ``data: dict`` stub data
    classes with real values/fields parsed from the source packages.
    """
    from flet_pkg.core.downloader import PubDevDownloader

    # Collect unique source packages that need resolution
    packages_to_resolve: dict[str, list[str]] = {}
    for type_name, source_pkg in api.reexported_types.items():
        if not source_pkg:
            continue
        packages_to_resolve.setdefault(source_pkg, []).append(type_name)

    if not packages_to_resolve:
        return

    downloader = PubDevDownloader()

    for pkg_name, type_names in packages_to_resolve.items():
        try:
            pkg_path = downloader.download(pkg_name)
        except Exception:
            continue

        try:
            platform_api = parse_dart_package_api(pkg_path)
        except Exception:
            continue

        # Build lookup maps from the platform_interface source
        enum_map: dict[str, list[tuple[str, str]]] = {}
        for dart_enum in platform_api.enums:
            enum_map[dart_enum.name] = dart_enum.values

        helper_map: dict[str, list[tuple[str, str]]] = {}
        for helper_cls in platform_api.helper_classes:
            fields = []
            for method in helper_cls.methods:
                if method.is_getter and not method.params:
                    if method.name == helper_cls.name:
                        continue
                    field_name = camel_to_snake(method.name)
                    field_type = map_dart_type(method.return_type)
                    fields.append((field_name, field_type))
            if fields:
                helper_map[helper_cls.name] = fields

        # Standard Dart object members to skip when extracting fields
        _OBJECT_MEMBERS = {
            "hashCode",
            "runtimeType",
            "toString",
            "noSuchMethod",
            "operator",
            "hash_code",
            "runtime_type",
        }

        def _clean_fields(raw_fields: list[tuple[str, str]]) -> list[tuple[str, str]]:
            """Filter out Dart object members and sanitize types."""
            cleaned = []
            for fname, ftype in raw_fields:
                if fname in _OBJECT_MEMBERS:
                    continue
                # Sanitize unknown types to str | None
                ftype = _sanitize_python_type(ftype, self._known_types)
                cleaned.append((fname, ftype))
            return cleaned

        # Also check regular classes for data classes with getters/fields
        for cls in platform_api.classes:
            if cls.name in helper_map:
                continue
            fields = []
            for method in cls.methods:
                if method.is_getter and not method.params:
                    if method.name == cls.name:
                        continue
                    field_name = camel_to_snake(method.name)
                    field_type = map_dart_type(method.return_type)
                    fields.append((field_name, field_type))
            if fields:
                helper_map[cls.name] = fields

        # Clean all fields
        for name in helper_map:
            helper_map[name] = _clean_fields(helper_map[name])

        # Update plan enums with real values
        for enum_plan in plan.enums:
            if enum_plan.python_name in enum_map:
                real_values = enum_map[enum_plan.python_name]
                if real_values:
                    enum_plan.values = [(v, v.lower(), doc) for v, doc in real_values]
                    enum_plan.docstring = ""

        # Convert stub data classes to enums when platform_interface
        # reveals they are actually enums (not data classes).
        stubs_to_remove: list[StubDataClass] = []
        for stub in plan.stub_data_classes:
            if stub.python_name in enum_map:
                real_values = enum_map[stub.python_name]
                if real_values and not any(
                    e.python_name == stub.python_name for e in plan.enums
                ):
                    plan.enums.append(
                        EnumPlan(
                            python_name=stub.python_name,
                            values=[(v, v.lower(), doc) for v, doc in real_values],
                        )
                    )
                    stubs_to_remove.append(stub)
            elif stub.python_name in helper_map:
                real_fields = helper_map[stub.python_name]
                if real_fields:
                    stub.fields = real_fields
                    stub.docstring = ""
        for stub in stubs_to_remove:
            plan.stub_data_classes.remove(stub)