prodict 0.8.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
prodict/__init__.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Global comments:
|
|
2
|
+
# self is avoided to fix #15
|
|
3
|
+
from typing import Any, List
|
|
4
|
+
import copy
|
|
5
|
+
|
|
6
|
+
DICT_RESERVED_KEYS = vars(dict).keys()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GenericMeta(type):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _dict_value(v, is_recursive, exclude_none, exclude_none_in_lists, convert_list_items):
|
|
14
|
+
if is_recursive and isinstance(v, Prodict):
|
|
15
|
+
return v.to_dict(is_recursive=is_recursive, exclude_none=exclude_none,
|
|
16
|
+
exclude_none_in_lists=exclude_none_in_lists,
|
|
17
|
+
convert_list_items=convert_list_items)
|
|
18
|
+
|
|
19
|
+
# Handle lists: convert items to dict if requested
|
|
20
|
+
if isinstance(v, List) and (convert_list_items or exclude_none_in_lists):
|
|
21
|
+
# Determine whether to exclude None from list items
|
|
22
|
+
# If exclude_none_in_lists=True, always exclude None (legacy behavior)
|
|
23
|
+
# Otherwise, respect the parent's exclude_none setting
|
|
24
|
+
list_item_exclude_none = True if exclude_none_in_lists else exclude_none
|
|
25
|
+
|
|
26
|
+
return [
|
|
27
|
+
item.to_dict(exclude_none=list_item_exclude_none,
|
|
28
|
+
is_recursive=is_recursive,
|
|
29
|
+
exclude_none_in_lists=exclude_none_in_lists,
|
|
30
|
+
convert_list_items=convert_list_items)
|
|
31
|
+
if isinstance(item, Prodict)
|
|
32
|
+
else item
|
|
33
|
+
for item in v
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
return v
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _none_condition(v, is_recursive, exclude_none):
|
|
40
|
+
return v is not None if exclude_none else True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# noinspection PyMethodParameters
|
|
44
|
+
class Prodict(dict):
|
|
45
|
+
"""
|
|
46
|
+
Prodict = Dictionary with IDE friendly(auto code completion),
|
|
47
|
+
dot-accessible attributes and more.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self_d921dfa9_4e93_4123_893d_a7e7eb783a32, *args, **kwargs):
|
|
51
|
+
super().__init__(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
# #3: Set all properties to None
|
|
54
|
+
for (
|
|
55
|
+
k,
|
|
56
|
+
v,
|
|
57
|
+
) in self_d921dfa9_4e93_4123_893d_a7e7eb783a32.attr_types().items():
|
|
58
|
+
self_d921dfa9_4e93_4123_893d_a7e7eb783a32.set_attribute(k, None)
|
|
59
|
+
|
|
60
|
+
# Set default values of annotated attributes
|
|
61
|
+
self_d921dfa9_4e93_4123_893d_a7e7eb783a32.init()
|
|
62
|
+
self_d921dfa9_4e93_4123_893d_a7e7eb783a32.set_attributes(**kwargs)
|
|
63
|
+
|
|
64
|
+
def init(self):
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def __new__(cls, *args, **kwargs):
|
|
68
|
+
return super(Prodict, cls).__new__(cls, *args, **kwargs)
|
|
69
|
+
|
|
70
|
+
def __getstate__(self):
|
|
71
|
+
return self.to_dict()
|
|
72
|
+
|
|
73
|
+
def __setstate__(self, state):
|
|
74
|
+
return Prodict.from_dict(state)
|
|
75
|
+
|
|
76
|
+
def __deepcopy__(self, memo=None):
|
|
77
|
+
new = self.from_dict({})
|
|
78
|
+
for key in self.keys():
|
|
79
|
+
new.set_attribute(key, copy.deepcopy(self[key], memo=memo))
|
|
80
|
+
return new
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_dict(cls, d: dict):
|
|
84
|
+
return cls(**d)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def attr_has_default_value(cls, attr_name: str) -> bool:
|
|
88
|
+
return bool(hasattr(cls, attr_name))
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def get_attr_default_value(cls, attr_name: str):
|
|
92
|
+
if cls.attr_has_default_value(attr_name):
|
|
93
|
+
return getattr(cls, attr_name)
|
|
94
|
+
else:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def attr_type(cls, attr_name: str):
|
|
99
|
+
return cls.attr_types()[attr_name]
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def attr_types(cls):
|
|
103
|
+
return cls.__annotations__ if hasattr(cls, '__annotations__') else {}
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def attr_names(cls) -> List[str]:
|
|
107
|
+
"""
|
|
108
|
+
Returns annotated attribute names
|
|
109
|
+
:return: List[str]
|
|
110
|
+
"""
|
|
111
|
+
return [k for k, v in cls.attr_types().items()]
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def has_attr(cls, attr_name: str):
|
|
115
|
+
"""
|
|
116
|
+
Returns True if class have an annotated attribute
|
|
117
|
+
:param attr_name: Attribute name
|
|
118
|
+
:return: bool
|
|
119
|
+
"""
|
|
120
|
+
return bool(cls.attr_types().get(attr_name))
|
|
121
|
+
|
|
122
|
+
def set_default(self, attr_name):
|
|
123
|
+
if self.attr_has_default_value(attr_name):
|
|
124
|
+
attr_default_type = self.attr_type(attr_name)
|
|
125
|
+
attr_default_value = self.get_attr_default_value(attr_name)
|
|
126
|
+
delattr(self, attr_name)
|
|
127
|
+
self.__annotations__[attr_name] = attr_default_type
|
|
128
|
+
self.set_attribute(attr_name, None)
|
|
129
|
+
self.update({attr_name: attr_default_value})
|
|
130
|
+
|
|
131
|
+
def get_constructor(self, attr_name, value):
|
|
132
|
+
"""
|
|
133
|
+
This method is used for type conversion.
|
|
134
|
+
Prodict uses this method to get the type of a value, then based on the
|
|
135
|
+
value, it return a constructor. If the type of a value is 'float' then
|
|
136
|
+
it returns 'float' since 'float' is also a constructor to build a float
|
|
137
|
+
value.
|
|
138
|
+
"""
|
|
139
|
+
attr_type1 = self.attr_type(attr_name)
|
|
140
|
+
constructor = None
|
|
141
|
+
element_type = None
|
|
142
|
+
if attr_type1 == float:
|
|
143
|
+
constructor = float
|
|
144
|
+
elif attr_type1 == str:
|
|
145
|
+
constructor = str
|
|
146
|
+
elif attr_type1 == int:
|
|
147
|
+
constructor = int
|
|
148
|
+
elif attr_type1 == list:
|
|
149
|
+
constructor = list
|
|
150
|
+
elif isinstance(value, Prodict):
|
|
151
|
+
constructor = attr_type1.from_dict
|
|
152
|
+
elif attr_type1 is Any:
|
|
153
|
+
constructor = None
|
|
154
|
+
elif isinstance(value, dict):
|
|
155
|
+
if attr_type1 == dict:
|
|
156
|
+
constructor = Prodict.from_dict
|
|
157
|
+
elif issubclass(attr_type1, Prodict):
|
|
158
|
+
constructor = self.attr_type(attr_name).from_dict
|
|
159
|
+
elif attr_type1 is List:
|
|
160
|
+
constructor = list
|
|
161
|
+
elif hasattr(attr_type1, '__origin__'):
|
|
162
|
+
if attr_type1.__dict__['__origin__'] is list:
|
|
163
|
+
# if the type is 'List[something]'
|
|
164
|
+
if len(attr_type1.__args__) == 0:
|
|
165
|
+
constructor = list
|
|
166
|
+
elif len(attr_type1.__args__) == 1:
|
|
167
|
+
constructor = List
|
|
168
|
+
element_type = attr_type1.__args__[0]
|
|
169
|
+
elif len(attr_type1.__args__) > 1:
|
|
170
|
+
raise TypeError('Only one dimensional List is supported')
|
|
171
|
+
elif attr_type1.__dict__['__origin__'] is tuple:
|
|
172
|
+
# if the type is 'Tuple[something]'
|
|
173
|
+
constructor = tuple
|
|
174
|
+
|
|
175
|
+
# print('constru={} element_type={}'.format(constructor, element_type))
|
|
176
|
+
return constructor, element_type
|
|
177
|
+
|
|
178
|
+
def set_attribute(self, attr_name, value):
|
|
179
|
+
if attr_name in DICT_RESERVED_KEYS:
|
|
180
|
+
raise TypeError("You cannot set a reserved name as attribute")
|
|
181
|
+
if self.has_attr(attr_name):
|
|
182
|
+
if value is None:
|
|
183
|
+
self.update({attr_name: None})
|
|
184
|
+
elif self.attr_type(attr_name) == Any:
|
|
185
|
+
self[attr_name] = value
|
|
186
|
+
else:
|
|
187
|
+
constructor, element_type = self.get_constructor(
|
|
188
|
+
attr_name, value
|
|
189
|
+
)
|
|
190
|
+
if constructor is None:
|
|
191
|
+
self.update({attr_name: value})
|
|
192
|
+
elif constructor == List:
|
|
193
|
+
value_list: List[element_type] = value
|
|
194
|
+
new_list: List[element_type] = []
|
|
195
|
+
|
|
196
|
+
if issubclass(element_type, Prodict):
|
|
197
|
+
element_constructor = element_type.from_dict
|
|
198
|
+
else:
|
|
199
|
+
element_constructor = element_type
|
|
200
|
+
|
|
201
|
+
for v in value_list:
|
|
202
|
+
new_list.append(element_constructor(v))
|
|
203
|
+
self.update({attr_name: new_list})
|
|
204
|
+
elif constructor == list:
|
|
205
|
+
self.update({attr_name: list(value)})
|
|
206
|
+
else:
|
|
207
|
+
self.update({attr_name: constructor(value)})
|
|
208
|
+
else:
|
|
209
|
+
if isinstance(value, dict):
|
|
210
|
+
if isinstance(value, Prodict):
|
|
211
|
+
constructor = value.from_dict
|
|
212
|
+
else:
|
|
213
|
+
constructor = Prodict.from_dict
|
|
214
|
+
self.update({attr_name: constructor(value)})
|
|
215
|
+
else:
|
|
216
|
+
self.update({attr_name: value})
|
|
217
|
+
|
|
218
|
+
def set_attributes(self_d921dfa9_4e93_4123_893d_a7e7eb783a32, **d):
|
|
219
|
+
for k, v in d.items():
|
|
220
|
+
self_d921dfa9_4e93_4123_893d_a7e7eb783a32.set_attribute(k, v)
|
|
221
|
+
|
|
222
|
+
def __getattr__(self, item):
|
|
223
|
+
try:
|
|
224
|
+
return self[item]
|
|
225
|
+
except KeyError as exc:
|
|
226
|
+
raise AttributeError(f"{type(self).__name__!r} object has no attribute {exc!s}") from None
|
|
227
|
+
|
|
228
|
+
def __setattr__(self, name: str, value) -> None:
|
|
229
|
+
self.set_attribute(name, value)
|
|
230
|
+
|
|
231
|
+
def to_dict(
|
|
232
|
+
self,
|
|
233
|
+
*args,
|
|
234
|
+
is_recursive=False,
|
|
235
|
+
exclude_none=False,
|
|
236
|
+
exclude_none_in_lists=False,
|
|
237
|
+
convert_list_items=False,
|
|
238
|
+
**kwargs
|
|
239
|
+
):
|
|
240
|
+
"""
|
|
241
|
+
Converts Prodict to dict.
|
|
242
|
+
|
|
243
|
+
:param is_recursive: If True, nested Prodict attributes are converted to dicts
|
|
244
|
+
:param exclude_none: If True, attributes with None values are excluded from the result
|
|
245
|
+
:param exclude_none_in_lists: If True, Prodict items in lists are converted to dicts
|
|
246
|
+
with None values excluded (legacy behavior, implies convert_list_items=True)
|
|
247
|
+
:param convert_list_items: If True, Prodict items in lists are converted to dicts
|
|
248
|
+
(respects exclude_none setting for those items)
|
|
249
|
+
:return: dict
|
|
250
|
+
|
|
251
|
+
Examples:
|
|
252
|
+
# Keep list items as Prodict
|
|
253
|
+
obj.to_dict() # or to_dict(is_recursive=False, convert_list_items=False)
|
|
254
|
+
|
|
255
|
+
# Convert list items to dict, keeping None values
|
|
256
|
+
obj.to_dict(convert_list_items=True)
|
|
257
|
+
|
|
258
|
+
# Convert list items to dict, excluding None values
|
|
259
|
+
obj.to_dict(convert_list_items=True, exclude_none=True)
|
|
260
|
+
# or obj.to_dict(exclude_none_in_lists=True) # legacy, always excludes None
|
|
261
|
+
|
|
262
|
+
# Convert everything recursively
|
|
263
|
+
obj.to_dict(is_recursive=True, convert_list_items=True)
|
|
264
|
+
"""
|
|
265
|
+
ret = {
|
|
266
|
+
k: _dict_value(
|
|
267
|
+
v,
|
|
268
|
+
is_recursive=is_recursive,
|
|
269
|
+
exclude_none=exclude_none,
|
|
270
|
+
exclude_none_in_lists=exclude_none_in_lists,
|
|
271
|
+
convert_list_items=convert_list_items,
|
|
272
|
+
)
|
|
273
|
+
for k, v in self.items()
|
|
274
|
+
if _none_condition(
|
|
275
|
+
v, is_recursive=is_recursive, exclude_none=exclude_none
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
return ret
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prodict
|
|
3
|
+
Version: 0.8.22
|
|
4
|
+
Summary: Prodict = Pro Dictionary with IDE friendly(auto code completion), dot-accessible attributes and more.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ramazanpolat/prodict
|
|
6
|
+
Project-URL: Download, https://pypi.python.org/pypi/prodict/0.8.22
|
|
7
|
+
Project-URL: Repository, https://github.com/ramazanpolat/prodict
|
|
8
|
+
Project-URL: Issues, https://github.com/ramazanpolat/prodict/issues
|
|
9
|
+
Author-email: Ramazan Polat <ramazanpolat@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE.txt
|
|
12
|
+
Keywords: auto-code-complete,auto-complete,dict,dictionary,dynamic-props,prodict,python3,typehinting
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.7
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Prodict
|
|
24
|
+
Prodict = Dictionary with IDE friendly(auto code completion) and dot-accessible attributes **and more**.
|
|
25
|
+
|
|
26
|
+
# What it does
|
|
27
|
+
Ever wanted to use a `dict` like a class and access keys as attributes? **Prodict** does exactly this.
|
|
28
|
+
|
|
29
|
+
Although there are number of modules doing this, **Prodict** does a little bit **more**.
|
|
30
|
+
|
|
31
|
+
You can provide type hints and get auto code completion!
|
|
32
|
+
|
|
33
|
+
With type hints, you also get nested object instantiations, which will blow your mind.
|
|
34
|
+
|
|
35
|
+
You will never want to use `dict` again.
|
|
36
|
+
|
|
37
|
+
# Why?
|
|
38
|
+
* Because accessing `dict` keys like `d['key']` is error prone and ugly.
|
|
39
|
+
|
|
40
|
+
* Because it becomes uglier if it is nested, like `d['key1]['key2']['key3']`. Compare `d['key1]['key2']['key3']` to `d.key1.key2.key3`, which one looks better?
|
|
41
|
+
|
|
42
|
+
* Because since web technologies mostly talk with JSON, it should be much more easy to use JSON data(see sample use case below).
|
|
43
|
+
|
|
44
|
+
* Because auto code completion makes developers' life easier.
|
|
45
|
+
|
|
46
|
+
* Because serializing a Python class to `dict` and deserializing from `dict` in one line is awesome!
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Features
|
|
50
|
+
|
|
51
|
+
1) A class with dynamic properties, without defining it beforehand.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
j = Prodict()
|
|
55
|
+
j.hi = 'there'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
2) Pass named arguments and all arguments will become properties.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
p = Prodict(lang='Python', pros='Rocks!')
|
|
62
|
+
print(p.lang) # Python
|
|
63
|
+
print(p.pros) # Rocks!
|
|
64
|
+
print(p) # {'lang': 'Python', 'pros': 'Rocks!'}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
3) Instantiate from a `dict`, get `dict` keys as properties
|
|
68
|
+
```python
|
|
69
|
+
p = Prodict.from_dict({'lang': 'Python', 'pros': 'Rocks!'})
|
|
70
|
+
print(p.lang) # Python
|
|
71
|
+
p.another_property = 'this is dynamically added'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
4) Pass a `dict` as argument, get a nested `Prodict`!
|
|
75
|
+
```python
|
|
76
|
+
p = Prodict(package='Prodict', makes='Python', rock={'even': 'more!'})
|
|
77
|
+
print(p) # {'package': 'Prodict', 'makes': 'Python', 'rock': {'even': 'more!'}}
|
|
78
|
+
print(p.rock.even) # 'more!'
|
|
79
|
+
print(type(p.rock)) # <class 'prodict.Prodict'>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
5) Extend `Prodict` and use type annotations for auto type conversion and auto code completion
|
|
83
|
+
```python
|
|
84
|
+
class User(Prodict):
|
|
85
|
+
user_id: int
|
|
86
|
+
name: str
|
|
87
|
+
|
|
88
|
+
user = User(user_id="1", name="Ramazan")
|
|
89
|
+
type(user.user_id) # <class 'int'>
|
|
90
|
+
# IDE will be able to auto complete 'user_id' and 'name' properties(see example 1 below)
|
|
91
|
+
```
|
|
92
|
+
Why type conversion? Because it will be useful if the incoming data doesn't have the desired type.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
class User(Prodict):
|
|
96
|
+
user_id: int
|
|
97
|
+
name: str
|
|
98
|
+
literal: Any
|
|
99
|
+
|
|
100
|
+
response = requests.get("https://some.restservice.com/user/1").json()
|
|
101
|
+
user: User = User.from_dict(response)
|
|
102
|
+
type(user.user_id) # <class 'int'>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Notes on automatic type conversion**:
|
|
106
|
+
* In the above example code, `user.user_id` will be an `int`, even if rest service responded with `str`.
|
|
107
|
+
* Same goes for all built-in types(int, str, float, bool, list, tuple), except `dict`. Because by default, all `dict` types will be converted to `Prodict`.
|
|
108
|
+
* If you don't want any type conversion but still want to have auto code completion, use `Any` as type annotation, like the `literal` attribute defined in `User` class.
|
|
109
|
+
* If the annotated type of an attribute is sub-class of a `Prodict`, the provided `dict` will be instantiated as the instance of sub-class. Even if it is `List` of the sub-class(see sample usa case below).
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Sample use case
|
|
114
|
+
|
|
115
|
+
Suppose that you are getting this JSON response from `https://some.restservice.com/user/1`:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
{
|
|
119
|
+
user_id: 1,
|
|
120
|
+
user_name: "rambo",
|
|
121
|
+
posts: [
|
|
122
|
+
{
|
|
123
|
+
title:"Hello World",
|
|
124
|
+
text:"This is my first blog post...",
|
|
125
|
+
date:"2018-01-02 03:04:05",
|
|
126
|
+
comments: [
|
|
127
|
+
{
|
|
128
|
+
user_id:2,
|
|
129
|
+
comment:"Good to see you blogging",
|
|
130
|
+
date:"2018-01-02 03:04:06"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
user_id:3,
|
|
134
|
+
comment:"Good for you",
|
|
135
|
+
date:"2018-01-02 03:04:07"
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
title:"Leave the old behind",
|
|
141
|
+
text:"Stop using Python 2.x...",
|
|
142
|
+
date:"2018-02-03 04:05:06",
|
|
143
|
+
comments: [
|
|
144
|
+
{
|
|
145
|
+
user_id:4,
|
|
146
|
+
comment:"Python 2 is dead, long live Python",
|
|
147
|
+
date:"2018-02-03 04:05:07"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
user_id:5,
|
|
151
|
+
comment:"You are god damn right :wears Heissenberg glasses:",
|
|
152
|
+
date:"2018-02-03 04:05:08"
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
Despite the fact that JSON being schemaless, most REST services will respond with a certain structure.
|
|
160
|
+
In the above example, the structure is something like this:
|
|
161
|
+
```
|
|
162
|
+
User
|
|
163
|
+
|--> user_id
|
|
164
|
+
|--> user_name
|
|
165
|
+
|--> posts [post]
|
|
166
|
+
|--> title
|
|
167
|
+
|--> text
|
|
168
|
+
|--> date
|
|
169
|
+
|--> comments [comment]
|
|
170
|
+
|--> user_id
|
|
171
|
+
|--> comment
|
|
172
|
+
|--> date
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
And you want to convert this to appropriate Python classes.
|
|
176
|
+
|
|
177
|
+
Without `Prodict`:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
class Comment:
|
|
181
|
+
def __init__(self, user_id, comment, date):
|
|
182
|
+
self.user_id = user_id
|
|
183
|
+
self.comment = comment
|
|
184
|
+
self.date = date
|
|
185
|
+
|
|
186
|
+
class Post:
|
|
187
|
+
def __init__(self, title, text, date):
|
|
188
|
+
self.title = title
|
|
189
|
+
self.text = text
|
|
190
|
+
self.date = date
|
|
191
|
+
self.comments = []
|
|
192
|
+
|
|
193
|
+
class User:
|
|
194
|
+
def __init__(self, user_id, user_name):
|
|
195
|
+
self.user_id = user_id
|
|
196
|
+
self.user_name = user_name
|
|
197
|
+
self.posts = []
|
|
198
|
+
|
|
199
|
+
user_json = requests.get("https://some.restservice.com/user/1").json()
|
|
200
|
+
posts = [Post(post['title'], post['text'], post['date']) for post in user_json['posts']]
|
|
201
|
+
for post in posts:
|
|
202
|
+
post.comments = [[comment for comment in post['comments']] for post in user_json['posts']]
|
|
203
|
+
|
|
204
|
+
user = User(user_json['user_id'], user_json['user_name'])
|
|
205
|
+
user.posts = posts
|
|
206
|
+
|
|
207
|
+
for post in user.posts:
|
|
208
|
+
print(post.title)
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
With **Prodict** you just need to define the classes and let the prodict do the rest like this:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
class Comment(Prodict):
|
|
216
|
+
user_id: int
|
|
217
|
+
comment: str
|
|
218
|
+
date: str
|
|
219
|
+
|
|
220
|
+
class Post(Prodict):
|
|
221
|
+
title: str
|
|
222
|
+
text: str
|
|
223
|
+
date: str
|
|
224
|
+
comments: List[Comment]
|
|
225
|
+
|
|
226
|
+
class User(Prodict):
|
|
227
|
+
user_id: int
|
|
228
|
+
user_name: str
|
|
229
|
+
posts: List[Post]
|
|
230
|
+
|
|
231
|
+
user_json = requests.get("https://some.restservice.com/user/1").json()
|
|
232
|
+
user:User = User.from_dict(user_json)
|
|
233
|
+
# Don't forget to annotate the `user` with `User` type in order to get auto code completion.
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
See the difference?
|
|
237
|
+
Plus you can add new attributes to `User`, `Post` and `Comment` objects dynamically and access them as dot-accessible attributes.
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# Examples
|
|
241
|
+
|
|
242
|
+
**Example 0**: Use it like regular `dict`, because **it is** a dict.
|
|
243
|
+
```python
|
|
244
|
+
|
|
245
|
+
from prodict import Prodict
|
|
246
|
+
|
|
247
|
+
d = dict(lang='Python', pros='Rocks!')
|
|
248
|
+
p = Prodict(lang='Python', pros='Rocks!')
|
|
249
|
+
|
|
250
|
+
print(d) # {'lang': 'Python', 'pros': 'Rocks!'}
|
|
251
|
+
print(p) # {'lang': 'Python', 'pros': 'Rocks!'}
|
|
252
|
+
print(d == p) # True
|
|
253
|
+
|
|
254
|
+
p2 = Prodict.from_dict({'Hello': 'world'})
|
|
255
|
+
|
|
256
|
+
print(p2) # {'Hello': 'world'}
|
|
257
|
+
print(issubclass(Prodict, dict)) # True
|
|
258
|
+
print(isinstance(p, dict)) # True
|
|
259
|
+
print(set(dir(dict)).issubset(dir(Prodict))) # True
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Example 1**: Accessing keys as attributes and auto completion.
|
|
263
|
+
```python
|
|
264
|
+
from prodict import Prodict
|
|
265
|
+
class Country(Prodict):
|
|
266
|
+
name: str
|
|
267
|
+
population: int
|
|
268
|
+
|
|
269
|
+
turkey = Country()
|
|
270
|
+
turkey.name = 'Turkey'
|
|
271
|
+
turkey.population = 79814871
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+

|
|
275
|
+
|
|
276
|
+
**Example 2**: Auto type conversion
|
|
277
|
+
```python
|
|
278
|
+
germany = Country(name='Germany', population='82175700', flag_colors=['black', 'red', 'yellow'])
|
|
279
|
+
|
|
280
|
+
print(germany.population) # 82175700
|
|
281
|
+
print(type(germany.population)) # <class 'int'> <-- The type is `int` !
|
|
282
|
+
# If you don't want type conversion and still want to have auto code completion, use `Any` as type.
|
|
283
|
+
print(germany.flag_colors) # ['black', 'red', 'yellow']
|
|
284
|
+
print(type(germany.population)) # <class 'int'>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Example 3**: Nested class instantiation
|
|
288
|
+
```python
|
|
289
|
+
class Ram(Prodict):
|
|
290
|
+
capacity: int
|
|
291
|
+
unit: str
|
|
292
|
+
type: str
|
|
293
|
+
clock: int
|
|
294
|
+
|
|
295
|
+
class Computer(Prodict):
|
|
296
|
+
name: str
|
|
297
|
+
cpu_cores: int
|
|
298
|
+
rams: List[Ram]
|
|
299
|
+
|
|
300
|
+
def total_ram(self):
|
|
301
|
+
return sum([ram.capacity for ram in self.rams])
|
|
302
|
+
|
|
303
|
+
comp1 = Computer.from_dict(
|
|
304
|
+
{
|
|
305
|
+
'name': 'My Computer',
|
|
306
|
+
'cpu_cores': 4,
|
|
307
|
+
'rams': [
|
|
308
|
+
{'capacity': 4,
|
|
309
|
+
'unit': 'GB',
|
|
310
|
+
'type': 'DDR3',
|
|
311
|
+
'clock': 2400}
|
|
312
|
+
]
|
|
313
|
+
})
|
|
314
|
+
print(comp1.rams) # [{'capacity': 4, 'unit': 'GB', 'type': 'DDR3', 'clock': 2400}]
|
|
315
|
+
|
|
316
|
+
comp1.rams.append(Ram(capacity=8, type='DDR3'))
|
|
317
|
+
comp1.rams.append(Ram.from_dict({'capacity': 12, 'type': 'DDR3', 'clock': 2400}))
|
|
318
|
+
|
|
319
|
+
print(comp1.rams)
|
|
320
|
+
# [
|
|
321
|
+
# {'capacity': 4, 'unit': 'GB', 'type': 'DDR3', 'clock': 2400},
|
|
322
|
+
# {'capacity': 8, 'type': 'DDR3'},
|
|
323
|
+
# {'capacity': 12, 'type': 'DDR3', 'clock': 2400}
|
|
324
|
+
# ]
|
|
325
|
+
print(type(comp1.rams)) # <class 'list'>
|
|
326
|
+
print(type(comp1.rams[0])) # <class 'Ram'> <-- Mind the type !
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Example 4**: Provide default values
|
|
330
|
+
|
|
331
|
+
You can use `init` method to provide default values. Keep in mind that `init` is NOT `__init__` but `init` method will be called in `__init__` method.
|
|
332
|
+
|
|
333
|
+
Additionally, you can use `init` method instead of `__init__` without referring to `super`.
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
class MyDataWithDefaults(Prodict):
|
|
337
|
+
an_int: int
|
|
338
|
+
a_str: str
|
|
339
|
+
|
|
340
|
+
def init(self):
|
|
341
|
+
self.an_int = 42
|
|
342
|
+
self.a_str = 'string'
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
data = MyDataWithDefaults(dynamic=43)
|
|
346
|
+
print(data)
|
|
347
|
+
# {'an_int':42, 'a_str':'string', 'dynamic':43}
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
# Class attributes vs Instance attributes
|
|
352
|
+
|
|
353
|
+
Prodict only works for instance attributes.
|
|
354
|
+
Even if you try to set an inherited class attribute, a new instance attribute is created and set.
|
|
355
|
+
|
|
356
|
+
Consider this example:
|
|
357
|
+
```python
|
|
358
|
+
from prodict import Prodict
|
|
359
|
+
|
|
360
|
+
class MyClass(Prodict):
|
|
361
|
+
class_attr: int = 42 # class_attr is a class attribute, not instance attribute
|
|
362
|
+
|
|
363
|
+
my_class = MyClass()
|
|
364
|
+
print(f"my_class.class_attr: {my_class.class_attr}") # 42
|
|
365
|
+
# There is no 'class_attr' defined as instance attribute, so class attribute will be returned (42).
|
|
366
|
+
print(f"MyClass.class_attr: {MyClass.class_attr}") # 42
|
|
367
|
+
# This is a class attribute, it will be returned as is.
|
|
368
|
+
|
|
369
|
+
# Now an instance attribute 'class_attr' is created and set to 77
|
|
370
|
+
my_class.class_attr = 77
|
|
371
|
+
print(f"my_class.class_attr: {my_class.class_attr}") # 42
|
|
372
|
+
# For this matter, avoid setting class_attribute with dot notation, use class name instead
|
|
373
|
+
|
|
374
|
+
MyClass.class_attr = 88
|
|
375
|
+
print(f"MyClass.class_attr: {my_class.class_attr}") # 88
|
|
376
|
+
|
|
377
|
+
# So where did 77 go? It is in instance attribute of the class and since it's name is colliding with
|
|
378
|
+
# the class attribute, you can't get it by dot notation. You can use .get tho.
|
|
379
|
+
print(f"my_class.get('class_attr'): {my_class.get('class_attr')}") # 77
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# Installation
|
|
384
|
+
If your default Python is 3.7:
|
|
385
|
+
|
|
386
|
+
`pip install prodict`
|
|
387
|
+
|
|
388
|
+
If you have more than one Python versions installed:
|
|
389
|
+
|
|
390
|
+
`python3.7 -m pip install prodict`
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# Limitations
|
|
394
|
+
- You cannot use `dict` method names as attribute names because of ambiguity.
|
|
395
|
+
- You cannot use `Prodict` method names as attribute names(I will change `Prodict` method names with dunder names to reduce the limitation).
|
|
396
|
+
- You must use valid variable names as `Prodict` attribute names(obviously). For example, while '1' cannot be an attribute for `Prodict`, it is perfectly valid for a `dict` to have '1' as a key. You can still use prodict.set_attribute('1',123) tho.
|
|
397
|
+
- Requires Python 3.7+
|
|
398
|
+
|
|
399
|
+
# Thanks
|
|
400
|
+
I would like to thank to [JetBrains](https://www.jetbrains.com/) for creating [PyCharm](https://www.jetbrains.com/pycharm/), the IDE that made my life better.
|
|
401
|
+
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
prodict/__init__.py,sha256=2nwvI0LPfcGTeImCfXYn1AHBTXOGSWZHXN1MFXh4V4Q,10150
|
|
2
|
+
prodict-0.8.22.dist-info/METADATA,sha256=XJkAQGZbuL3YX5m59IGtT_Begq1ULMWpaWsXzVgHyMk,12357
|
|
3
|
+
prodict-0.8.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
+
prodict-0.8.22.dist-info/licenses/LICENSE.txt,sha256=TEAKv1BbARNOlwFP1msyt1RtXuDBjXIwNcjjqkJBaJk,1095
|
|
5
|
+
prodict-0.8.22.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Ramazan Polat (ramazanpolat@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|