Quickstart

In real world projects, maintaining a large amount of parameters for user inputs/options, function input variables and program runtime status is almost inevitable. A clear, robust and extensible design of parameter structures, rules and restrictions greatly contributes to the quality of project codes. Paramdict provides a Pythonic way to achieve this goal: parameters are stored in a dictionary, wrapped simply with a class and several decorators.

What is a Paramdict

A paramdict defines a specific organization of a set of parameters: what is the type of the parameter, whether to strictly check the type, can this parameter be left blank, is there any limit about the parameter’s range, etc. Following is an exmaple:

[1]:
from paramdict import *

class pdTest(baseParamDict):

    def __init__(self, id:str='', source=None):
        super().__init__(id, source)
    def _setcls(self):
        return 'pdtest'
    def _pdinit(self):
        pass
    def _check(self):
        return super()._check()

    @pSimple
    def a(self) -> int: ...

    @pSimple
    def b(self) -> float: ...

    @pStrict
    def c(self) -> str: ...

    @pList
    def d(self) -> list[int]: ...

Here, a paramdict class named “pdtest” with 4 parameters, “a”, “b”, “c”, and “d”, is defined. Any paramdict instance of this class will be able to and only able to contain these 4 parameters. The decorators, pSimple, pStrict, and pList, works like Python’s built-in property decorator, except that setting method is implemented automatically. So to set or get a parameter:

[2]:
pd1 = pdTest()

# set values
pd1.a = 1
pd1.b = 2.0
pd1.c = '3'
pd1.d = [4, 5, 6]

# get values
print(f"a: {pd1.a}")
print(f"b: {pd1.b}")
print(f"c: {pd1.c}")
print(f"d: {pd1.d}")
a: 1
b: 2.0
c: 3
d: [4, 5, 6]

Annotations matters for the decorated parameter methods. When setting a parameter decorated by pSimple, the input value will be automatically converted to the annotated type:

[3]:
pd1.a = 3.0
print(f"a: {pd1.a}")
print(f"Type of a: {type(pd1.a)}")
a: 3
Type of a: <class 'int'>

pStrict behaves differently from pSimple. It checks the type of input value, and raise errors when input type is not the annotated type and does not inherit from the annotated type:

[4]:
pd1.c = 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 pd1.c = 1

File ~/utils/paramdict/paramDictProperties.py:79, in pStrict.__set__(self, instance, value)
     77 aType:type = get_type_hints(self.fget)['return']
     78 if not isinstance(value, aType):
---> 79     raise TypeError(_('Property %s must be of type %s.') % (outYellow('\''+self.fget.__name__+'\''), outGreen(aType)))
     80 instance._cont[self.fget.__name__] = value

TypeError: Property 'c' must be of type <class 'str'>.

pList not only checks if the input value is a list, but also if all the elements are of the right type.

[5]:
pd1.d = [4, 5, '6']
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 pd1.d = [4, 5, '6']

File ~/utils/paramdict/paramDictProperties.py:110, in pList.__set__(self, instance, value)
    108 for item in value:
    109     if not isinstance(item, aType):
--> 110         raise TypeError(_('Property %s must be of type %s.') % (outYellow('\''+self.fget.__name__+'\''), outGreen(List[aType])))
    111 instance._cont[self.fget.__name__] = value

TypeError: Property 'd' must be of type typing.List[int].

Paramdict Nested in Paramdict

An important trait of paramdict is that a paramdict can be nested in another paramdict while maintaining all the functionality:

[6]:
from copy import deepcopy

class pdTest2(baseParamDict):

    def __init__(self, id:str='', source=None):
        super().__init__(id, source)
    def _setcls(self):
        return 'pdtest2'
    def _pdinit(self):
        pass
    def _check(self):
        return super()._check()

    @pPd
    def param_pdtest(self) -> pdTest: ...

    @pPdList
    def param_pdtest_list(self) -> list[pdTest]: ...

pd2 = pdTest2()
pd2.param_pdtest = deepcopy(pd1)
pd2.param_pdtest.a = 10
pd2.param_pdtest_list = [deepcopy(pd1), deepcopy(pd1)]
pd2.param_pdtest_list[0].b = 20.0
pd2.param_pdtest_list[1].c = '30'

pd2.content
[6]:
{'class': 'pdtest2',
 'id': '',
 'param_pdtest': {'class': 'pdtest',
  'id': '',
  'a': 10,
  'b': 2.0,
  'c': '3',
  'd': [4, 5, 6]},
 'param_pdtest_list': [{'class': 'pdtest',
   'id': '',
   'a': 3,
   'b': 20.0,
   'c': '3',
   'd': [4, 5, 6]},
  {'class': 'pdtest', 'id': '', 'a': 3, 'b': 2.0, 'c': '30', 'd': [4, 5, 6]}]}

This paramdict class contains two parameters: “param_pdtest” is a paramdict that follows pdTest paramdict class definition, and “param_pdtest_list” is a list that contains paramdicts that also follows pdTest. Then a paramdict instance (pd2) is initialized and modified. By referring the content built-in property, we can check the dictionary where values are actually stored.

Persistence

It is also easy to serialize the content:

[7]:
import json

json_str = json.dumps(pd2.content, indent=4)
print(json_str)
{
    "class": "pdtest2",
    "id": "",
    "param_pdtest": {
        "class": "pdtest",
        "id": "",
        "a": 10,
        "b": 2.0,
        "c": "3",
        "d": [
            4,
            5,
            6
        ]
    },
    "param_pdtest_list": [
        {
            "class": "pdtest",
            "id": "",
            "a": 3,
            "b": 20.0,
            "c": "3",
            "d": [
                4,
                5,
                6
            ]
        },
        {
            "class": "pdtest",
            "id": "",
            "a": 3,
            "b": 2.0,
            "c": "30",
            "d": [
                4,
                5,
                6
            ]
        }
    ]
}

Or to deserialize:

[8]:
pdcont = json.loads(json_str)
pd3 = pdTest2(source=pdcont)

pd3.content
[8]:
{'class': 'pdtest2',
 'id': '',
 'param_pdtest': {'class': 'pdtest',
  'id': '',
  'a': 10,
  'b': 2.0,
  'c': '3',
  'd': [4, 5, 6]},
 'param_pdtest_list': [{'class': 'pdtest',
   'id': '',
   'a': 3,
   'b': 20.0,
   'c': '3',
   'd': [4, 5, 6]},
  {'class': 'pdtest', 'id': '', 'a': 3, 'b': 2.0, 'c': '30', 'd': [4, 5, 6]}]}