From 9eeae6e2295fcbd2f2b0eab6ee95c09d181eff47 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Tue, 20 May 2025 15:49:56 +0200 Subject: [PATCH] feat(classgen): add support for unknown types --- pkgs/clan-cli/clan_lib/api/serde.py | 5 +++++ pkgs/clan-cli/clan_lib/api/util.py | 7 +++++++ pkgs/classgen/main.py | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/pkgs/clan-cli/clan_lib/api/serde.py b/pkgs/clan-cli/clan_lib/api/serde.py index 59a611cbe..aeefe9402 100644 --- a/pkgs/clan-cli/clan_lib/api/serde.py +++ b/pkgs/clan-cli/clan_lib/api/serde.py @@ -30,6 +30,7 @@ Note: This module assumes the presence of other modules and classes such as `Cla """ import dataclasses +import inspect from dataclasses import dataclass, fields, is_dataclass from enum import Enum from pathlib import Path @@ -261,6 +262,10 @@ def construct_value( return t(field_value) # type: ignore + if inspect.isclass(t) and t.__name__ == "Unknown": + # Return the field value as is + return field_value + msg = f"Unhandled field type {t} with value {field_value}" raise ClanError(msg) diff --git a/pkgs/clan-cli/clan_lib/api/util.py b/pkgs/clan-cli/clan_lib/api/util.py index d06de4f0d..d4ac6a206 100644 --- a/pkgs/clan-cli/clan_lib/api/util.py +++ b/pkgs/clan-cli/clan_lib/api/util.py @@ -1,5 +1,6 @@ import copy import dataclasses +import inspect import pathlib from dataclasses import MISSING from enum import EnumType @@ -110,6 +111,12 @@ def type_to_dict( if t is None: return {"type": "null"} + if inspect.isclass(t) and t.__name__ == "Unknown": + # Empty should represent unknown + # We don't know anything about this type + # Nor about the nested fields, if there are any + return {} + if dataclasses.is_dataclass(t): fields = dataclasses.fields(t) properties = {} diff --git a/pkgs/classgen/main.py b/pkgs/classgen/main.py index 856b1c545..4f01a3645 100644 --- a/pkgs/classgen/main.py +++ b/pkgs/classgen/main.py @@ -1,12 +1,15 @@ # ruff: noqa: RUF001 import argparse import json +import logging import sys from collections.abc import Callable from functools import partial from pathlib import Path from typing import Any +logger = logging.getLogger(__name__) + class Error(Exception): pass @@ -130,6 +133,12 @@ def field_def_from_default_value( finalize_field: Callable[..., tuple[str, str]], ) -> tuple[str, str] | None: # default_value = prop_info.get("default") + if "Unknown" in field_types: + # Unknown type, doesnt matter what the default value is + # type Unknown | a -> Unknown + return finalize_field( + field_types=field_types, + ) if default_value is None: return finalize_field( field_types=field_types | {"None"}, @@ -237,6 +246,9 @@ def generate_dataclass( if not prop_type and not union_variants and not enum_variants: msg = f"Type not found for property {prop} {prop_info}" raise Error(msg) + msg = f"Type not found for property {prop} {prop_info}.\nConverting to unknown type.\n" + logger.warning(msg) + prop_type = "Unknown" if union_variants: field_types = map_json_type(union_variants) @@ -283,6 +295,8 @@ def generate_dataclass( ) ) known_classes.add(nested_class_name) + elif prop_type == "Unknown": + field_types = {"Unknown"} else: field_types = map_json_type( prop_type, @@ -388,6 +402,12 @@ def run_gen(args: argparse.Namespace) -> None: # ruff: noqa: F401 # fmt: off from typing import Any, Literal, NotRequired, TypedDict\n + +# Mimic "unknown". +# 'Any' is unsafe because it allows any operations +# This forces the user to use type-narrowing or casting in the code +class Unknown: + pass """ ) f.write(dataclass_code)