|
1 | | -# dataclasses.py: Minimal MicroPython compatibility layer for Python's dataclasses |
2 | | -# Implements @dataclass with __init__ and __repr__ generation |
| 1 | +# dataclasses.py: MicroPython compatibility layer for Python's dataclasses |
| 2 | +# Implements @dataclass, field, and Field with support for default_factory |
| 3 | + |
| 4 | +MISSING = object() # Sentinel for missing values |
| 5 | + |
| 6 | +class Field: |
| 7 | + """Represents a dataclass field, supporting default_factory.""" |
| 8 | + def __init__(self, default=MISSING, default_factory=MISSING, init=True, repr=True): |
| 9 | + self.name = None # Set by dataclass decorator |
| 10 | + self.type = None # Set by dataclass decorator |
| 11 | + self.default = default |
| 12 | + self.default_factory = default_factory |
| 13 | + self.init = init |
| 14 | + self.repr = repr |
| 15 | + |
| 16 | +def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True): |
| 17 | + """Specify a dataclass field with optional default_factory.""" |
| 18 | + if default is not MISSING and default_factory is not MISSING: |
| 19 | + raise ValueError("Cannot specify both default and default_factory") |
| 20 | + return Field(default=default, default_factory=default_factory, init=init, repr=repr) |
3 | 21 |
|
4 | 22 | def dataclass(cls): |
5 | | - """Decorator to emulate Python's @dataclass, generating __init__ and __repr__.""" |
| 23 | + """Decorator to emulate @dataclass, generating __init__ and __repr__.""" |
6 | 24 | # Get class annotations and defaults |
7 | 25 | annotations = getattr(cls, '__annotations__', {}) |
8 | 26 | defaults = {} |
| 27 | + fields = {} |
| 28 | + |
| 29 | + # Process class attributes for defaults and field() calls |
9 | 30 | for name in dir(cls): |
10 | 31 | if not name.startswith('__'): |
11 | 32 | attr = getattr(cls, name, None) |
12 | | - if not callable(attr) and name in annotations: |
13 | | - defaults[name] = attr |
| 33 | + if not callable(attr): |
| 34 | + if isinstance(attr, Field): |
| 35 | + fields[name] = attr |
| 36 | + fields[name].name = name |
| 37 | + fields[name].type = annotations.get(name) |
| 38 | + if attr.default is not MISSING: |
| 39 | + defaults[name] = attr.default |
| 40 | + elif name in annotations: |
| 41 | + defaults[name] = attr |
| 42 | + |
| 43 | + # Ensure all annotated fields have a Field object |
| 44 | + for name in annotations: |
| 45 | + if name not in fields: |
| 46 | + fields[name] = Field(default=defaults.get(name, MISSING), init=True, repr=True) |
| 47 | + fields[name].name = name |
| 48 | + fields[name].type = annotations.get(name) |
14 | 49 |
|
15 | 50 | # Generate __init__ method |
16 | 51 | def __init__(self, *args, **kwargs): |
17 | 52 | # Positional arguments |
18 | | - fields = list(annotations.keys()) |
| 53 | + init_fields = [name for name, f in fields.items() if f.init] |
19 | 54 | for i, value in enumerate(args): |
20 | | - if i >= len(fields): |
| 55 | + if i >= len(init_fields): |
21 | 56 | raise TypeError(f"Too many positional arguments") |
22 | | - setattr(self, fields[i], value) |
| 57 | + setattr(self, init_fields[i], value) |
23 | 58 |
|
24 | | - # Keyword arguments and defaults |
25 | | - for name in fields: |
26 | | - if name in kwargs: |
| 59 | + # Keyword arguments, defaults, and default_factory |
| 60 | + for name, field in fields.items(): |
| 61 | + if field.init and name in kwargs: |
27 | 62 | setattr(self, name, kwargs[name]) |
28 | 63 | elif not hasattr(self, name): |
29 | | - if name in defaults: |
30 | | - setattr(self, name, defaults[name]) |
31 | | - else: |
| 64 | + if field.default_factory is not MISSING: |
| 65 | + setattr(self, name, field.default_factory()) |
| 66 | + elif field.default is not MISSING: |
| 67 | + setattr(self, name, field.default) |
| 68 | + elif field.init: |
32 | 69 | raise TypeError(f"Missing required argument: {name}") |
33 | 70 |
|
| 71 | + # Call __post_init__ if defined |
| 72 | + if hasattr(self, '__post_init__'): |
| 73 | + self.__post_init__() |
| 74 | + |
34 | 75 | # Generate __repr__ method |
35 | 76 | def __repr__(self): |
36 | | - fields = [ |
| 77 | + fields_repr = [ |
37 | 78 | f"{name}={getattr(self, name)!r}" |
38 | | - for name in annotations |
| 79 | + for name, field in fields.items() |
| 80 | + if field.repr |
39 | 81 | ] |
40 | | - return f"{cls.__name__}({', '.join(fields)})" |
| 82 | + return f"{cls.__name__}({', '.join(fields_repr)})" |
41 | 83 |
|
42 | 84 | # Attach generated methods to class |
43 | 85 | setattr(cls, '__init__', __init__) |
|
0 commit comments