Set attribute of class from Dict with cast

316 Views Asked by At

I'm trying to parse a Dict of [str,str] with key value pair into a object (class). The first contains the name of attributes and the second the value.

ex: [('CLIENT_ID','123456'), ('AUTO_COMMIT', 'False'), ('BROKER_URL', 'http://foo.bar')]

The code to parse is the next:

class KafkaSettings():
    BROKER_URL: str = None
    CLIENT_ID: str = None
    AUTO_COMMIT: bool = False

    def __init__(self, d: Dict[str, str]):
        if d is not None:
            for key, value in d.items():
                attr = self.__getattribute__(key)
                casted_value = cast(attr, value)
                self.__setattr__(key, casted_value)

The parser works but the type of the value is altered. For example, AUTO_COMMIT attr as type bool but with setattr the type changed into str. The cast of the value into the good type not work. I don't know the Python language.

How to resolve this problem ?

Thanks

3

There are 3 best solutions below

2
Kavindu Nilshan On BEST ANSWER

You can solve this using below code,

from typing import Dict


class KafkaSettings():
    BROKER_URL: str = None
    CLIENT_ID: str = None
    AUTO_COMMIT: bool = False

    def __init__(self, d: Dict[str, str]):
        if d is not None:
            for key, value in d.items():
                attr = self.__getattribute__(key)
                
                # change as follows
                if key == "AUTO_COMMIT":
                    value = bool(value)

                self.__setattr__(key, value)

You can solve this issue by checking the key and if it is equal to AUTO_COMMIT then you can cast the string value to the bool value by using bool() method.

But note that this is a specific solution for your code. So it is better to add the values with their actual type (as you needed) when making the dictionary. Then you do not need any casting.

For an example, In this case, you can use this,

[('CLIENT_ID','123456'), ('AUTO_COMMIT', False), ('BROKER_URL', 'http://foo.bar')]

(store False as boolean instead of string)

instead of this,

[('CLIENT_ID','123456'), ('AUTO_COMMIT', 'False'), ('BROKER_URL', 'http://foo.bar')]

1
chepner On

You can't perform automatic conversion of a value to another type using type hints. You need to be explicit. For example (and fixing your attempts to extract type information from the variable annotations):

class KafkaSettings():
    BROKER_URL: str = None
    CLIENT_ID: str = None
    AUTO_COMMIT: bool = False

    def __init__(self, d: Dict[str, str]):
        for key, value in d.items():
            if issubclass(KafkaSettings.__annotations__[key], bool):
                casted_value = {'True': True, 'False': False}[value]
            else:
                casted_value = value

            setattr(self, key, casted_value)

What you really want is something more explicit, and divide __init__ into a simpler __init__ and a class method. (The simpler __init__ can be autogenerated by dataclasses.dataclass.)

# Enhance as you like
bool_map = {
    'False': False,
    'True': True,
    'F': False,
    'T': True,
    'yes': True,
    'no': False,
}
    
@dataclass
class KafkaSettings:
    broker_url: str = None
    client_id: str = None
    auto_commit: bool = False

    @classmethod
    def from_dict(cls, d: Dict[str, str]):
        url = d.get('BROKER_URL')
        client_id = d.get('CLIENT_ID')
        autocommit_str = d.get('AUTO_COMMIT')
        if autocommit_str is None:
            autocommit = False
        else:
            autocommit = bool_map[autocommit_str]

        return cls(url, client_id, autocommit)
        

Using a dictionary is a way to provide values for the attribute, but it is not fundamentally necessary. What you need to instantiate KafkaSettings are two strings and a boolean value: supply those directly to __init__, and define the class method to extract them from a dictionary.

3
JL Peyret On

This is a standard pydantic elevator pitch.

from pydantic import BaseModel

class KafkaSettings(BaseModel):
    BROKER_URL: str = None
    CLIENT_ID: str = None
    AUTO_COMMIT: bool = False

    @classmethod 
    def factory(cls, tuples):
        di = {v[0]:v[1] for v in tuples}
        return KafkaSettings(**di)
    
print(KafkaSettings.factory([('CLIENT_ID','123456'), ('AUTO_COMMIT', 'False'), ('BROKER_URL', 'http://foo.bar')]))


output:

BROKER_URL='http://foo.bar' CLIENT_ID='123456' AUTO_COMMIT=False

Btw, pydantic supports multiple types and the order makes a difference "3" => foo : str|int => "3" while "3" => foo : int|str => 3 , IIRC.

And ('AUTO_COMMIT', '1') => True.

And here's a more troubleshooting-friendly version of the factory:

    @classmethod 
    def factory(cls, tuples):
        try:
            print(f"{tuples=}")
            di = {v[0]:v[1] for v in tuples}
            print(f"{di=}")
            res = KafkaSettings(**di)
            return res
            #pragma: no cover pylint: disable=unused-variable
        except (Exception,) as e: 
            print(f"{e=}")
            breakpoint()
            raise