Skip to content

fix(monte_carlo): rehydrate nested dataclass lists when reconstructing SimConfig#12

Open
RTHYMS wants to merge 1 commit into
kcolbchain:mainfrom
RTHYMS:contrib/fix-mc-dataclass-roundtrip
Open

fix(monte_carlo): rehydrate nested dataclass lists when reconstructing SimConfig#12
RTHYMS wants to merge 1 commit into
kcolbchain:mainfrom
RTHYMS:contrib/fix-mc-dataclass-roundtrip

Conversation

@RTHYMS

@RTHYMS RTHYMS commented Jun 10, 2026

Copy link
Copy Markdown

Summary

4 of 18 Monte Carlo tests fail on main with:

```
AttributeError: 'dict' object has no attribute 'volume_usd_m0'
File "token_simulator/model.py", line 195, in run
stream_volumes = [s.volume_usd_m0 for s in cfg.revenue_streams]
```

Root cause

`mc_run` round-trips `SimConfig` through `_dataclass_to_dict` and back through `_dict_to_config` to apply distribution overrides. The reconstruct step did a flat `setattr`:

```python
def _dict_to_config(d: dict) -> SimConfig:
"""Convert a flat dict back to a SimConfig (lossy — only scalar fields)."""
cfg = SimConfig()
for key, value in d.items():
if hasattr(cfg, key):
setattr(cfg, key, value)
return cfg
```

So `SimConfig.revenue_streams: List[RevenueStream]` came back as a list of dicts (one per dataclass). Then `model.run(cfg)` crashed at `s.volume_usd_m0` because `s` was a dict, not a `RevenueStream`. The docstring even admitted "lossy — only scalar fields" — but the loss was load-bearing.

Fix

Rewrite `_dict_to_config` to introspect `SimConfig`'s resolved type hints (via `typing.get_type_hints` — handles `from future import annotations` where field types are strings) and rebuild `List[]` fields by `**`-unpacking each dict back into the element type.

```python
def _dict_to_config(d: dict) -> SimConfig:
"""Convert a flat dict back to a SimConfig, rehydrating nested dataclass lists."""
field_types = get_type_hints(SimConfig)
cfg = SimConfig()
for key, value in d.items():
if not hasattr(cfg, key):
continue
item_cls = _list_item_dataclass(field_types.get(key))
if item_cls and isinstance(value, list):
value = [item_cls(**v) if isinstance(v, dict) else v for v in value]
setattr(cfg, key, value)
return cfg

def _list_item_dataclass(type_hint):
"""Return the dataclass element type for List[Foo] style hints, else None."""
if get_origin(type_hint) is list:
args = get_args(type_hint)
if args and is_dataclass(args[0]):
return args[0]
return None
```

Result

```
18 passed in 0.29s
```

(was 14 passed, 4 failed)

Drive-by

CI workflow (pytest on Python 3.11/3.12) + SECURITY.md + CI badge.

…g SimConfig

mc_run round-tripped SimConfig through _dataclass_to_dict /
_dict_to_config to apply distribution overrides. The reconstruct step did
a flat setattr — so List[RevenueStream] came back as a list of dicts, and
model.run() crashed at `s.volume_usd_m0` with
`AttributeError: 'dict' object has no attribute 'volume_usd_m0'`.

Rewrite _dict_to_config to introspect SimConfig's resolved type hints
(typing.get_type_hints handles `from __future__ import annotations`) and
rebuild List[<dataclass>] fields by ** -unpacking each dict back into the
element type. The docstring's "lossy — only scalar fields" admission is
no longer accurate, hence the rewrite.

4 of 18 mc tests were failing on main; all 18 pass after this fix.

Also: wire CI workflow + badge + SECURITY policy.
@abhicris

Copy link
Copy Markdown

Welcome to kcolbchain, @RTHYMS — glad you're here. 🌱

Here's what happens from this PR:

  1. Our automated review looks for obvious issues (tests, secrets, size) within a couple of hours.
  2. If it's clean and CI passes, we merge without back-and-forth.
  3. If we need changes, we'll leave a specific comment — not a generic nit. Push another commit and we re-review.

While you wait:

  • Run the repo's tests locally (see the repo README.md).
  • Keep the PR scoped to one concern — bigger PRs land slower.
  • Don't commit tokens or .env contents.

What happens after your first merge

Thanks for writing the code. We're building this to last.

abhicris pushed a commit that referenced this pull request Jun 19, 2026
)

_dataclass_to_dict flattens nested lists (RevenueStream, VestBucket)
to dicts but _dict_to_config did a flat setattr, destroying structure.

Fix: detect list fields whose originals are dataclass lists and
recursively reconstruct via _dict_to_dataclass helper.

Closes kcolbchain/token-simulator #12

Co-authored-by: Gaotax2006 <gaotax2006@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants