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 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.
@@ -0,0 +1,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include tests *
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
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
45
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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* 🚀