How would I create a Parameterized class programmatically?

I’m exploring converting a Pydantic class to a Param class programmatically.

Say I have

class_name= "MyClass"
parameters = [
    {"name": "value", "type": param.String, "default": "bla", "allow_None": True},
    {"name": "value2", "type": param.Integer, "default": "blabla", "allow_None": False}
]

How would I programmatically create the class below from this

class MyClass(param.Parameterized):
    value = param.String(default="bla", allow_None= True)
    value2 = param.Integer(default="blabla", allow_None= False)

Thanks.

2 Likes

Figured it out. I can use .param.add_parameter.

Which means I’m able to do basic conversions between Param models and Pydantic Models via the functions param_to_pydantic_class and pydantic_to_param_class.

import datetime
from typing import Container, Dict, List, Optional, Type, Union

import param
import pydantic
import pytest
from pydantic import BaseConfig, BaseModel, create_model

try:
    import numpy as np

    DATE_TYPE = Union[datetime.datetime, datetime.date, np.datetime64]
except:
    DATE_TYPE = Union[datetime.datetime, datetime.date]

PARAM_TO_PYTHON_TYPE: Dict[param.Parameter, Type] = {
    param.String: str,
    param.Integer: int,
    param.Number: float,
    param.Date: DATE_TYPE,
    param.List: List,
}
PYTHON_TYPE_TO_PARAM = {value: key for key, value in PARAM_TO_PYTHON_TYPE.items()}


def _get_python_type_from_parameter(parameter: param.Parameter):
    if isinstance(parameter, param.List) and parameter.item_type:
        python_type: Type = List[parameter.item_type]
    else:
        python_type = PARAM_TO_PYTHON_TYPE[parameter.__class__]

    if parameter.allow_None:
        python_type = Union[python_type, None]

    return python_type


def _get_parameter_type_from_python_type(type_: Type) -> Type[param.Parameter]:
    return PYTHON_TYPE_TO_PARAM[type_]


def _get_parameter_type_from_pydantic_field(
    field: pydantic.fields.ModelField,
) -> Type[param.Parameter]:
    return _get_parameter_type_from_python_type(field.type_)


def param_to_pydantic_class(
    parameterized: Type[param.Parameterized],
    *,
    config: Type = BaseConfig,
    exclude: Container[str] = []
) -> Type[BaseModel]:
    fields = {}
    parameters = [
        parameterized.param[key]
        for key in parameterized.param
        if not parameterized.param[key].name in exclude
    ]
    for parameter in parameters:
        python_type = _get_python_type_from_parameter(parameter)
        fields[parameter.name] = (python_type, parameter.default)
    pydantic_model = create_model(parameterized.__name__, __config__=config, **fields)  # type: ignore
    return pydantic_model


def pydantic_to_param_class(value) -> param.Parameterized:
    new_class: param.Parameterized = type(value.__name__, (param.Parameterized,), {})

    for name, field in value.__fields__.items():
        parameter_type = _get_parameter_type_from_pydantic_field(field)
        parameter = parameter_type(default=field.default, allow_None=field.allow_none)
        new_class.param.add_parameter(name, parameter)
    return new_class
4 Likes

Not sure if it’s of any use, but here’s another way if the other object has a to_dict() method:

from telethon.tl.patched import Message

# Metaclass, so we can inherit from both
class Meta(type(param.Parameterized),type(Message)):
    pass

# Derived class that inherits both
class MessageP(param.Parameterized,Message,metaclass=Meta):
    def __init__(self,**kwargs):
        # Only pass supported kwargs
        keys = inspect.getfullargspec(Message).args
        super().__init__(
            _client=kwargs['_client'],
            _input_chat=kwargs['_input_chat'],
            **{key:kwargs[key] for key in kwargs if key in keys}
        )

# Create parameterized MSG class from msg-dict
MSG = param.parameterized_class(
    'Message', # Class name
    params=param.guess_param_types(
        _client=telethon.client.telegramclient.TelegramClient, # pass additional kwargs
        _input_chat=telethon.tl.types.InputPeerChat, # pass additional kwargs
        **msg.to_dict() # exported kwargs
    ), # dict(name,Parameter)
    bases=MessageP # Derived class as base
)

test = MSG(
    _client=msgs[-3].client,
    _input_chat=msgs[-3].input_chat,
    **msgs[-3].to_dict() # We can now instantiate this class with different param values
)

test

Output:



image

I’ve never dealt with meta-classes before, but it was required for inheriting from both classes.

The code above creates a parameterized class from a derived class that inherits from both the Telegram Message class as well as param.Parameterized. Given how to_dict() produces some kwargs that are not allowed in Message instantiation, I’ve set the derived class to only pass kwargs that are in both.

1 Like

Here’s another version of param to pydantic:

[import datetime
from typing import Dict, List, Literal, Optional, Tuple, Type, TypeVar, Union

import inspect
import param
from pydantic import BaseConfig, BaseModel, create_model
from pydantic.color import Color
from pydantic.fields import FieldInfo
from typing_extensions import Self
from pydantic.fields import Undefined

DATE_TYPE = Union[datetime.datetime, datetime.date]
PARAM_TYPE_MAPPING: Dict[param.Parameter, Type] = {
    param.String: str,
    param.Integer: int,
    param.Number: float,
    param.Boolean: bool,
    param.Event: bool,
    param.Date: DATE_TYPE,
    param.DateRange: Tuple[DATE_TYPE],
    param.CalendarDate: DATE_TYPE,
    param.CalendarDateRange: Tuple[DATE_TYPE],
    param.ListSelector: List,
    param.Parameter: object,
    param.Color: Color,
}
PandasDataFrame = TypeVar("pandas.core.frame.DataFrame")


class ArbitraryTypesModel(BaseModel):
    """
    A Pydantic model that allows arbitrary types.
    """

    class Config(BaseConfig):
        arbitrary_types_allowed = True


def _create_literal(obj: List[Union[str, Type]]) -> Type:
    """
    Create a literal type from a list of objects.
    """
    types = []
    for obj in obj:
        if obj is None:
            continue
        elif isinstance(obj, str):
            types.append(obj)
        else:
            types.append(obj.__name__)
    if types:
        return Literal[tuple(types)]
    else:
        return str


def parameter_to_field(
    parameter: param.Parameter, created_models: Dict[str, BaseModel]
) -> (Type, FieldInfo):
    """
    Translate a parameter to a pydantic field.
    """
    param_type = parameter.__class__
    description = " ".join(parameter.doc.split()) if parameter.doc else None
    field_info = FieldInfo(description=description)

    if param_type in PARAM_TYPE_MAPPING:
        type_ = PARAM_TYPE_MAPPING[param_type]
        field_info.default = parameter.default
    elif param_type is param.ClassSelector:
        type_ = parameter.class_
        try:
            if issubclass(type_, param.Parameterized):
                type_ = created_models.get(type_.__name__, type_.__name__)
        except TypeError:
            pass
        if isinstance(type_, tuple):
            type_ = _create_literal(type_)
        if parameter.default is not None:
            default_factory = parameter.default
            if not callable(default_factory):
                default_factory = type(default_factory)
            field_info.default_factory = default_factory
    elif param_type is param.List:
        type_ = List
        if parameter.default == []:
            field_info.default_factory = list
        elif parameter.default is not None:
            field_info.default_factory = parameter.default
    elif param_type is param.Dict:
        type_ = Dict
        if parameter.default == {}:
            field_info.default_factory = dict
        elif parameter.default is not None:
            field_info.default_factory = parameter.default
    elif param_type in [param.Selector, param.ObjectSelector]:
        if parameter.objects:
            type_ = _create_literal(parameter.objects)
        else:
            type_ = str
    elif issubclass(param_type, param.DataFrame):
        type_ = PandasDataFrame
    elif parameter.name == "align":
        type_ = _create_literal(["auto", "start", "center", "end"])
    elif parameter.name == "aspect_ratio":
        type_ = Union[Literal["auto"], float]
    elif parameter.name == "margin":
        type_ = Union[float, Tuple[float, float], Tuple[float, float, float, float]]
    else:
        raise NotImplementedError(
            f"Parameter {parameter.name!r} of {param_type.__name__!r} not supported"
        )
    return type_, field_info


def param_to_pydantic(
    parameterized: Type[param.Parameterized],
    base_model: Type[BaseModel] = ArbitraryTypesModel,
    created_models: Optional[Dict[str, BaseModel]] = None,
) -> Dict[str, BaseModel]:
    """
    Translate a param Parameterized to a Pydantic BaseModel.

    Parameters
    ----------
    parameterized : Type[param.Parameterized]
        The parameterized class to translate.
    base_model : Type[BaseModel], optional
        The base model to use, by default ArbitraryTypesModel
    created_models : Optional[Dict[str, BaseModel]], optional
        A dictionary of already created models, by default None
    """
    parameterized_name = parameterized.__name__
    if created_models is None:
        created_models = {}
    if parameterized_name in created_models:
        return created_models

    parameterized_signature = inspect.signature(parameterized.__init__)
    required_args = [
        arg.name
        for arg in parameterized_signature.parameters.values()
        if arg.name not in ["self", "params"] and arg.default == inspect._empty
    ]

    fields = {}
    for parameter_name in parameterized.param:
        parameter = parameterized.param[parameter_name]
        if hasattr(parameter, "class_") and hasattr(parameter.class_, "param"):
            parameter_class_name = parameter.class_.__name__
            if parameterized_name != parameter_class_name:
                param_to_pydantic(parameter.class_, created_models=created_models)
        type_, field_info = parameter_to_field(parameter, created_models)
        if parameter_name == "schema":
            field_info.alias = "schema"
            parameter_name = "schema_"
        elif parameter_name == "copy":
            field_info.alias = "copy"
            parameter_name = "copy_"
        elif parameter_name[0] == "_":
            field_info.alias = parameter_name
            parameter_name = parameter_name.lstrip("_")

        if parameter_name in required_args:
            field_info.default = Undefined
        elif field_info.default == Undefined and not field_info.default_factory:
            field_info.default = None](https://github.com/holoviz-dev/lumen-llm)

        fields[parameter_name] = (type_, field_info)

    pydantic_model = create_model(parameterized.__name__, __base__=base_model, **fields)
    created_models[pydantic_model.__name__] = pydantic_model
    return created_models
2 Likes

@ahuang11 , can you make a PR to put that into Param or at least its docs?

1 Like

Mentioned it here for near future

I am looking to generate a parameterized class based on a python object that would wrap specified fields with their current inspected type and serve as a pass through for specified methods

This would allow arbitrary objects to be wrapped just in time to represent them in a UI and call actions on them

Have you done or seen anything similar?

NVM, it was quite easy following the thread above

import param
from typing import Any, List, Type

def object_to_param_class(obj: Any, attributes: List[str], methods: List[str]) -> Type[param.Parameterized]:
    # Store the original object in the param class
    class ParamWrapper(param.Parameterized):
        _original_object = obj

        def __setattr__(self, key, value):
            # Set attribute both on param class and original object
            super().__setattr__(key, value)
            if key in attributes:
                setattr(self._original_object, key, value)

    param_attrs = {'_original_object': obj}

    # Handling attributes
    for attr_name in attributes:
        attr_value = getattr(obj, attr_name)
        attr_type = type(attr_value)

        # Choose appropriate param type based on attribute type
        if attr_type is int:
            param_attrs[attr_name] = param.Integer(default=attr_value)
        elif attr_type is float:
            param_attrs[attr_name] = param.Number(default=attr_value)
        elif attr_type is str:
            param_attrs[attr_name] = param.String(default=attr_value)
        elif attr_type is bool:
            param_attrs[attr_name] = param.Boolean(default=attr_value)
        # Add more type mappings as necessary

    # Handling methods
    for method_name in methods:
        if hasattr(obj, method_name) and callable(getattr(obj, method_name)):
            method = getattr(obj, method_name)
            param_attrs[method_name] = method

    # Dynamically create the param class
    param_class = type(obj.__class__.__name__ + 'Param', (ParamWrapper,), param_attrs)
    return param_class

def get_methods(obj):
    return [attr for attr in dir(obj) 
           if callable(getattr(obj, attr)) and not attr.startswith('__')]

def get_attributes(obj):
    return [attr for attr in dir(obj) 
           if not callable(getattr(obj, attr)) and not attr.startswith('__')]