modict 0.1.1__tar.gz
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.
- modict-0.1.1/LICENSE +21 -0
- modict-0.1.1/MANIFEST.in +3 -0
- modict-0.1.1/PKG-INFO +487 -0
- modict-0.1.1/README.md +446 -0
- modict-0.1.1/modict/__init__.py +3 -0
- modict-0.1.1/modict/__main__.py +13 -0
- modict-0.1.1/modict/_collections_utils.py +664 -0
- modict-0.1.1/modict/_modict.py +844 -0
- modict-0.1.1/modict/_modict_meta.py +458 -0
- modict-0.1.1/modict/_typechecker.py +2215 -0
- modict-0.1.1/modict.egg-info/PKG-INFO +487 -0
- modict-0.1.1/modict.egg-info/SOURCES.txt +21 -0
- modict-0.1.1/modict.egg-info/dependency_links.txt +1 -0
- modict-0.1.1/modict.egg-info/entry_points.txt +2 -0
- modict-0.1.1/modict.egg-info/requires.txt +7 -0
- modict-0.1.1/modict.egg-info/top_level.txt +3 -0
- modict-0.1.1/pyproject.toml +47 -0
- modict-0.1.1/setup.cfg +4 -0
- modict-0.1.1/tests/__init__.py +1 -0
- modict-0.1.1/tests/__pycache__/__init__.cpython-310.pyc +0 -0
- modict-0.1.1/tests/__pycache__/test_modict.cpython-310-pytest-8.3.3.pyc +0 -0
- modict-0.1.1/tests/run_tests.py +65 -0
- modict-0.1.1/tests/test_modict.py +95 -0
modict-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Baptiste Ferrand
|
|
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.
|
modict-0.1.1/MANIFEST.in
ADDED
modict-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modict
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A Python package named modict
|
|
5
|
+
Author-email: baptiste <bferrand.maths@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Baptiste Ferrand
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/B4PT0R/modict
|
|
28
|
+
Classifier: Programming Language :: Python :: 3
|
|
29
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
30
|
+
Classifier: Operating System :: OS Independent
|
|
31
|
+
Requires-Python: >=3.9
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
37
|
+
Requires-Dist: pytest-html>=4.1.1; extra == "dev"
|
|
38
|
+
Requires-Dist: flake8; extra == "dev"
|
|
39
|
+
Requires-Dist: black; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# modict - The Swiss Army Knife of Python Data Structures
|
|
43
|
+
|
|
44
|
+
[](https://www.python.org/downloads/)
|
|
45
|
+
[](https://opensource.org/licenses/MIT)
|
|
46
|
+
|
|
47
|
+
**modict** is a sophisticated, hybrid data structure that combines the simplicity of Python dictionaries with the power of dataclasses and the robustness and runtime typechecking capabilities of Pydantic models. It's designed to be the versatile tool you'll want to use in every project for handling structured data.
|
|
48
|
+
|
|
49
|
+
## 🎯 Philosophy & Goals
|
|
50
|
+
|
|
51
|
+
**modict** bridges the gap between different Python data paradigms:
|
|
52
|
+
|
|
53
|
+
- **📚 Dict-like**: Native dictionary inheritance with full compatibility - modicts ARE dicts!
|
|
54
|
+
- **🏗️ Dataclass-like**: Type annotations and structured field definitions
|
|
55
|
+
- **🛡️ Pydantic-like**: Runtime validation, type coercion, custom validators, and computed properties
|
|
56
|
+
- **🔧 Developer-friendly**: Intuitive API that "just works" for common patterns
|
|
57
|
+
- **100% standard library** - No external dependencies, all is coded from scratch including the typechecker and coercion engine
|
|
58
|
+
|
|
59
|
+
### Why modict?
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# Traditional approaches require choosing between flexibility and structure
|
|
63
|
+
data = {"name": "Alice", "age": 30} # Dict: flexible but unstructured
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class User: name: str; age: int # Dataclass: structured but rigid
|
|
67
|
+
|
|
68
|
+
class User(BaseModel): name: str; age: int # Pydantic: powerful but heavy
|
|
69
|
+
|
|
70
|
+
# modict: Best of all worlds
|
|
71
|
+
class User(modict):
|
|
72
|
+
name: str
|
|
73
|
+
age: int = 25
|
|
74
|
+
|
|
75
|
+
user = User(name="Alice") # ✅ Structured
|
|
76
|
+
user.age # 25 ✅ Default value
|
|
77
|
+
user.email = "alice@email.com" # ✅ Flexible
|
|
78
|
+
user['phone'] = "123-456-7890" # ✅ Dict-compatible
|
|
79
|
+
isinstance(user,dict) # True (still a dict!)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 🚀 Key Features
|
|
83
|
+
|
|
84
|
+
### Core Capabilities
|
|
85
|
+
- **Full dict inheritance** - All native dict methods work seamlessly.
|
|
86
|
+
- **Attribute-style access** - `obj.key` and `obj['key']` both work
|
|
87
|
+
- **Type annotations** - Optional runtime validation with a powerful type validation and coercion system
|
|
88
|
+
- **Recursive conversion**
|
|
89
|
+
- Explicit: `modict.convert()` / `.to_modict()` for full deep conversion
|
|
90
|
+
- Automatic: `auto_convert=True` (default) converts nested dicts to `modict` on first access
|
|
91
|
+
- **JSON-first design** - Built-in JSON serialization/deserialization
|
|
92
|
+
- **Path-based access** - Access nested structures with dot notation
|
|
93
|
+
|
|
94
|
+
### Advanced Features
|
|
95
|
+
- **Computed properties** - Dynamic values with dependency tracking
|
|
96
|
+
- **Custom validators** - Field-level validation and transformation
|
|
97
|
+
- **Type coercion** - Intelligent type conversion system
|
|
98
|
+
- **Deep operations** - Merge, diff, walk through nested structures
|
|
99
|
+
- **Field extraction** - Select/exclude keys with simple methods
|
|
100
|
+
|
|
101
|
+
## 📦 Installation
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install modict
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 🏃♂️ Quick Start
|
|
108
|
+
|
|
109
|
+
### Basic Usage
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from modict import modict
|
|
113
|
+
|
|
114
|
+
# Create from dict or keyword arguments
|
|
115
|
+
user = modict({"name": "Alice", "age": 30})
|
|
116
|
+
user = modict(name="Alice", age=30)
|
|
117
|
+
|
|
118
|
+
# Attribute and dict-style access
|
|
119
|
+
print(user.name) # "Alice"
|
|
120
|
+
print(user['age']) # 30
|
|
121
|
+
|
|
122
|
+
# Add new fields dynamically
|
|
123
|
+
user.email = "alice@email.com"
|
|
124
|
+
user['phone'] = "123-456-7890"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Structured Classes
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from modict import modict
|
|
131
|
+
from typing import List, Optional
|
|
132
|
+
|
|
133
|
+
class User(modict):
|
|
134
|
+
name: str
|
|
135
|
+
age: int = 25
|
|
136
|
+
email: Optional[str] = None
|
|
137
|
+
tags: List[str] = modict.factory(list) # Factory for mutable defaults
|
|
138
|
+
|
|
139
|
+
# Type-safe creation
|
|
140
|
+
user = User(name="Bob", age=35)
|
|
141
|
+
print(user.age) # 35
|
|
142
|
+
print(user.tags) # []
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Nested Structures & Path Access
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# Automatic recursive conversion
|
|
149
|
+
data = modict({
|
|
150
|
+
"users": [
|
|
151
|
+
{"name": "Alice", "profile": {"city": "Paris"}},
|
|
152
|
+
{"name": "Bob", "profile": {"city": "Lyon"}}
|
|
153
|
+
],
|
|
154
|
+
"settings": {"theme": "dark"}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
# Path-based access
|
|
158
|
+
print(data.get_nested("users.0.name")) # "Alice"
|
|
159
|
+
data.set_nested("users.0.profile.country", "France")
|
|
160
|
+
print(data.has_nested("settings.theme")) # True
|
|
161
|
+
|
|
162
|
+
# Chained attribute access works too
|
|
163
|
+
# (Only if auto_convert=True (default) - see below about config)
|
|
164
|
+
print(data.users[0].profile.city) # "Paris"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 💫 Advanced Features
|
|
168
|
+
|
|
169
|
+
### Computed Properties
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
class Calculator(modict):
|
|
173
|
+
a: float = 0
|
|
174
|
+
b: float = 0
|
|
175
|
+
|
|
176
|
+
@modict.computed(cache=True, deps=['a', 'b'])
|
|
177
|
+
def sum_ab(self):
|
|
178
|
+
print("Computing sum...")
|
|
179
|
+
return self.a + self.b
|
|
180
|
+
|
|
181
|
+
@modict.computed(cache=True, deps=['sum_ab']) # Cascading dependencies
|
|
182
|
+
def doubled_sum(self):
|
|
183
|
+
return self.sum_ab * 2
|
|
184
|
+
|
|
185
|
+
calc = Calculator(a=10, b=20)
|
|
186
|
+
print(calc.sum_ab) # "Computing sum..." → 30
|
|
187
|
+
print(calc.sum_ab) # 30 (cached)
|
|
188
|
+
calc.a = 15 # Invalidates cache automatically
|
|
189
|
+
print(calc.sum_ab) # "Computing sum..." → 35
|
|
190
|
+
print(calc.doubled_sum) # 70
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Custom Validators
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
class Profile(modict):
|
|
197
|
+
email: str
|
|
198
|
+
age: int
|
|
199
|
+
|
|
200
|
+
@modict.check('email')
|
|
201
|
+
def validate_email(self, value):
|
|
202
|
+
"""Clean and validate email addresses"""
|
|
203
|
+
email = value.lower().strip()
|
|
204
|
+
if '@' not in email:
|
|
205
|
+
raise ValueError("Invalid email format")
|
|
206
|
+
return email
|
|
207
|
+
|
|
208
|
+
@modict.check('age')
|
|
209
|
+
def validate_age(self, value):
|
|
210
|
+
"""Ensure age is reasonable"""
|
|
211
|
+
age = int(value)
|
|
212
|
+
if age < 0 or age > 150:
|
|
213
|
+
raise ValueError("Invalid age range")
|
|
214
|
+
return age
|
|
215
|
+
|
|
216
|
+
profile = Profile(email=" ALICE@EMAIL.COM ", age="30")
|
|
217
|
+
print(profile.email) # "alice@email.com" (cleaned)
|
|
218
|
+
print(profile.age) # 30 (converted to int)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Deep Operations
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
# Deep merging
|
|
225
|
+
network_config = modict({"db": {"host": "localhost", "port": 5432}})
|
|
226
|
+
overrides = {"db": {"port": 3306, "ssl": True}}
|
|
227
|
+
network_config.merge(overrides)
|
|
228
|
+
# Result: {"db": {"host": "localhost", "port": 3306, "ssl": True}}
|
|
229
|
+
|
|
230
|
+
# Walking through nested structures
|
|
231
|
+
data = modict({"users": [{"name": "Alice"}, {"name": "Bob"}]})
|
|
232
|
+
for path, value in data.walk():
|
|
233
|
+
print(f"{path}: {value}")
|
|
234
|
+
# Output:
|
|
235
|
+
# users.0.name: Alice
|
|
236
|
+
# users.1.name: Bob
|
|
237
|
+
|
|
238
|
+
# Flattened view
|
|
239
|
+
flat = data.walked() # {"users.0.name": "Alice", "users.1.name": "Bob"}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## 🛠️ Configuration Options
|
|
243
|
+
|
|
244
|
+
The cassmethod `modict.config` allows you to customize the behavior of your modict subclass.
|
|
245
|
+
It returns an `modictConfig` object (dataclass) that you may pass as the `_config` class variable or your modict.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
class MyModict(modict):
|
|
249
|
+
_config = modict.config(
|
|
250
|
+
auto_convert=True, # Auto-convert dicts to modicts in nested sub-containers (upon access)
|
|
251
|
+
strict=False, # Strict runtime type checking
|
|
252
|
+
coerce=False, # Enable automatic type coercion
|
|
253
|
+
allow_extra=True, # Disallow extra attributes
|
|
254
|
+
enforce_json=False, # Enforce JSON serializability of values
|
|
255
|
+
)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`auto_convert` controls whether dicts found in nested mutable containers (MutableMappings, MutableSequence)
|
|
259
|
+
are automatically converted to `modict` (if they aren't already) on first access.
|
|
260
|
+
Note that MutableMappings that are NOT dicts won't be converted, but their content may if they are dicts.
|
|
261
|
+
|
|
262
|
+
Subclass configs are properly merged with parent class configs, also supporting multiple inheritance patterns (following MRO order).
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
class Parent(modict):
|
|
266
|
+
_config = modict.config(strict=True, coerce=False)
|
|
267
|
+
|
|
268
|
+
class Child(Parent):
|
|
269
|
+
_config = modict.config(coerce=True) # strict=True, coerce=True (overrides Parent)
|
|
270
|
+
|
|
271
|
+
class A(modict):
|
|
272
|
+
_config = modict.config(strict=True)
|
|
273
|
+
a: int=1
|
|
274
|
+
value: str="A"
|
|
275
|
+
|
|
276
|
+
class B(modict):
|
|
277
|
+
_config = modict.config(strict=False, coerce=True)
|
|
278
|
+
b: int=2
|
|
279
|
+
value: str="B"
|
|
280
|
+
|
|
281
|
+
class C(A,B):
|
|
282
|
+
_config = modict.config(allow_extra=False)
|
|
283
|
+
# strict=True from A (A overrides B, since A follows B in MRO),
|
|
284
|
+
# coerce=True from B
|
|
285
|
+
# allow_extra=False from C
|
|
286
|
+
|
|
287
|
+
c = C()
|
|
288
|
+
print(c.a) # 1
|
|
289
|
+
print(c.b) # 2
|
|
290
|
+
print(c.value) # "A" (A overrides B)
|
|
291
|
+
c.a = "3"
|
|
292
|
+
print(c.a) # 3 (coercion enabled)
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
c.a = "invalid"
|
|
296
|
+
except Exception as e:
|
|
297
|
+
print(e) # ❌ TypeError (strict mode enabled)
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
c.undefined = "value"
|
|
301
|
+
except Exception as e:
|
|
302
|
+
print(e) # ❌ KeyError (extra fields not allowed)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Example
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
class StrictConfig(modict):
|
|
309
|
+
|
|
310
|
+
_config=modict.config(
|
|
311
|
+
strict = True # Enable runtime type checking
|
|
312
|
+
allow_extra = False # Disallow undefined fields
|
|
313
|
+
coerce = True # Enable type coercion
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
name: str
|
|
317
|
+
count: int
|
|
318
|
+
|
|
319
|
+
config = StrictConfig(name="test", count=42)
|
|
320
|
+
# config.undefined = "value" # ❌ KeyError (extra fields not allowed)
|
|
321
|
+
# config.count = "32" # coerced to int (coercion enabled)
|
|
322
|
+
# config.count = "invalid" # ❌ TypeError (can't be coerced, type checking raises an error)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## 📄 JSON Integration
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
|
|
329
|
+
# JSON-enforced mode
|
|
330
|
+
class JSONConfig(modict):
|
|
331
|
+
|
|
332
|
+
_config=modict.config(
|
|
333
|
+
enforce_json=True
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Built-in JSON support
|
|
337
|
+
config = JSONConfig.load("config.json") # Load from file
|
|
338
|
+
config = JSONConfig.loads(json_string) # Load from string
|
|
339
|
+
|
|
340
|
+
config.dump("output.json", indent=2) # Save to file
|
|
341
|
+
json_str = config.dumps(indent=2) # Convert to string
|
|
342
|
+
|
|
343
|
+
config.data = {1, 2, 3} # ❌ ValueError (sets are not JSON-serializable)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## 🎨 Field Utilities
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
user = modict(name="Alice", age=30, email="alice@email.com", phone="123-456")
|
|
350
|
+
|
|
351
|
+
# Extract specific fields
|
|
352
|
+
basic_info = user.extract('name', 'age') # {"name": "Alice", "age": 30}
|
|
353
|
+
|
|
354
|
+
# Exclude sensitive fields
|
|
355
|
+
public_info = user.exclude('email', 'phone') # {"name": "Alice", "age": 30}
|
|
356
|
+
|
|
357
|
+
# Rename fields
|
|
358
|
+
user.rename(email='email_address') # Changes key name
|
|
359
|
+
|
|
360
|
+
# Deep copy
|
|
361
|
+
backup = user.deepcopy()
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## 🔄 Conversion & Compatibility
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
|
|
368
|
+
# let's turn auto-conversion off globally (affects all modict instances created after this change)
|
|
369
|
+
modict._config.auto_convert = False
|
|
370
|
+
|
|
371
|
+
# Convert existing dicts to modicts (recursive)
|
|
372
|
+
data = {"user": {"name": "Alice"}, "count": 42}
|
|
373
|
+
|
|
374
|
+
safe_modict = modict(data) # No auto-conversion
|
|
375
|
+
safe_modict.user.name # ❌ AttributeError (user is still a dict)
|
|
376
|
+
safe_modict.user["name"] # "Alice" (works with dict access)
|
|
377
|
+
isinstance(safe_modict.user, modict) # False (it's a plain dict)
|
|
378
|
+
data["user"] is safe_modict.user # True (same object)
|
|
379
|
+
|
|
380
|
+
modict_data = safe_modict.to_modict() # Deep conversion (in-place on the structure)
|
|
381
|
+
isinstance(modict_data.user, modict) # True (now it's a modict)
|
|
382
|
+
data["user"] is modict_data.user # False: user has been converted to a new modict
|
|
383
|
+
modict_data.user.name # ✅ "Alice" (user is now a modict)
|
|
384
|
+
dict_data = modict_data.to_dict() # Back to plain dicts
|
|
385
|
+
|
|
386
|
+
# Factory method for clean conversion
|
|
387
|
+
converted = modict.convert(complex_nested_dict)
|
|
388
|
+
unconverted = modict.unconvert(converted) # Back to plain dicts
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## ⚠️ Important Behaviors & Limitations
|
|
392
|
+
|
|
393
|
+
### Descriptor Handling
|
|
394
|
+
|
|
395
|
+
modict distinguishes between **definitions** and **assignments** in class namespaces:
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
class MyModict(modict):
|
|
399
|
+
# ✅ DEFINITIONS (stay as class methods)
|
|
400
|
+
@classmethod
|
|
401
|
+
def my_classmethod(cls):
|
|
402
|
+
return "method behavior"
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def my_property(self):
|
|
406
|
+
return "property behavior"
|
|
407
|
+
|
|
408
|
+
# ✅ ASSIGNMENTS (become dict fields)
|
|
409
|
+
external_func = some_external_function # Stored in dict
|
|
410
|
+
external_cm = classmethod(external_function) # Stored in dict (may be non-callable)
|
|
411
|
+
|
|
412
|
+
obj = MyModict()
|
|
413
|
+
obj.my_classmethod() # ✅ Works (bound method)
|
|
414
|
+
obj.external_func("x") # ✅ Works (raw function, no binding)
|
|
415
|
+
obj.external_cm("x") # ❌ May fail ('classmethod' object not callable)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**Principle**: *Syntax determines behavior*
|
|
419
|
+
- `def`/`@decorator` syntax → Class behavior (Python semantics)
|
|
420
|
+
- `=` assignment syntax → Data storage (user responsibility)
|
|
421
|
+
|
|
422
|
+
### Import Limitations
|
|
423
|
+
|
|
424
|
+
Imports inside class namespaces are treated as field assignments:
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
# ❌ PROBLEMATIC
|
|
428
|
+
class MyModict(modict):
|
|
429
|
+
import json # Becomes a 'json' field in the modict
|
|
430
|
+
|
|
431
|
+
def method(self):
|
|
432
|
+
return json.dumps(self) # ❌ NameError: 'json' not defined
|
|
433
|
+
|
|
434
|
+
# ✅ RECOMMENDED
|
|
435
|
+
import json
|
|
436
|
+
class MyModict(modict):
|
|
437
|
+
# json accessible via module scope
|
|
438
|
+
pass
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
This limitation rarely affects normal usage of modict as a data structure.
|
|
442
|
+
|
|
443
|
+
### Memory Considerations
|
|
444
|
+
|
|
445
|
+
- **Validation overhead**: Type checking and coercion add runtime cost
|
|
446
|
+
- **Computed properties**: Cached values consume additional memory
|
|
447
|
+
- **Recursive conversion**: Deep nesting may impact performance
|
|
448
|
+
|
|
449
|
+
## 🆚 Comparison with Alternatives
|
|
450
|
+
|
|
451
|
+
| Feature | modict | dict | dataclass | Pydantic |
|
|
452
|
+
|---------|-------|------|-----------|----------|
|
|
453
|
+
| Dict compatibility | ✅ Full | ✅ Native | ❌ No | ❌ Limited |
|
|
454
|
+
| Attribute access | ✅ Yes | ❌ No | ✅ Yes | ✅ Yes |
|
|
455
|
+
| Type validation | ✅ Optional | ❌ No | ❌ No | ✅ Yes |
|
|
456
|
+
| Runtime flexibility | ✅ High | ✅ High | ❌ Low | ❌ Medium |
|
|
457
|
+
| Nested structures | ✅ Auto | ❌ Manual | ❌ Manual | ✅ Auto |
|
|
458
|
+
| JSON integration | ✅ Built-in | ❌ Manual | ❌ Manual | ✅ Built-in |
|
|
459
|
+
| Learning curve | 🟡 Medium | 🟢 Low | 🟢 Low | 🔴 High |
|
|
460
|
+
| Performance | 🟡 Good | 🟢 Excellent | 🟢 Excellent | 🟡 Good |
|
|
461
|
+
|
|
462
|
+
## 🤝 Contributing
|
|
463
|
+
|
|
464
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
465
|
+
|
|
466
|
+
### Development Setup
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
git clone https://github.com/your-username/modict.git
|
|
470
|
+
cd modict
|
|
471
|
+
pip install -e .[dev]
|
|
472
|
+
pytest
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## 📜 License
|
|
476
|
+
|
|
477
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
478
|
+
|
|
479
|
+
## 🙏 Acknowledgments
|
|
480
|
+
|
|
481
|
+
- Inspired by the flexibility of Python dicts, the structure of dataclasses, and the power of Pydantic
|
|
482
|
+
- Built with modern Python typing and metaclass techniques
|
|
483
|
+
- Community feedback and real-world usage patterns
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
**modict**: *Because data structures should be both powerful and pleasant to use* 🚀
|