Skip to content

Feature/dict generation#20

Merged
mikejturner merged 13 commits intomainfrom
feature/dict-generation
Feb 25, 2026
Merged

Feature/dict generation#20
mikejturner merged 13 commits intomainfrom
feature/dict-generation

Conversation

@mikejturner
Copy link
Copy Markdown
Contributor

Prior to this PR, assertical could generate only classes instances (including special treatment of dataclasses). Members could be primitive (ints, floats etc), classes (including dataclasses), lists or sets.

This PR allows generation of classes with dictionary members. Not only that, but it allows arbitrary nesting of all collection types (dict, list, etc), for example,

@dataclass
class MyDataclass():
  d1: dict[str, int]
  d2: dict[str, ReferenceDataclass]
  d3: dict[str, list[int]]
  d4: dict[str, dict[str, int]]
  d5: dict[int, dict[str, list[int]]]
  d6: dict[str, dict[str, ReferenceDataclass]]
  l1: list[dict[str, list[ReferenceDataclass]]]
  
instance = generate_class_instance(MyDataclass)

Implementation Details

This PR really contains two separate changes (i) to support generation of dict members and (ii) to support the generation of arbitrary nesting of all collection types. The sections below discuss details of each implementation separately to make reviewing the code hopefully a little easier.

Generation of dict members

Adds necessary dicts enum values to CollectionType.

Unlike lists or set, dicts have two types (one for the keys and one for the values). To support this PropertyGenerationDetails has been extended to store type information for the second type (namely second_type_to_generate, second_type_is_primitive attributes). These get set for dictionaries in the enumerate_class_properties function.

One complication is this second value (dict value) needs to be generated in a similar way to first (dict key), except using second_type_to_generate and second_type_is_primitive instead of type_to_generate and type_is_primitive. To this end the value generation logic is pulled out into an inner function called generate_value(). This inner function is a closure capturing the values of optional_is_none, generate_relationships and _visited_type_stack. This inner function can be found within the generate_class_instance function.

Generation of nested collections types

The change discussed above is sufficient to generate dictionaries of primitive values e.g. dict[str, int], dict[int, float] and also generate dictionaries with class values e.g. dict[str, MyDataclass], dict[int, MyClass] (assuming generate_relationship is True).

I wanted to extend the generation to also be able to handle arbitrary nesting of collection types e.g. dict[int, list[int]], list[dict[str, MyClass]].

This is tricky but possible if we treat the collections as-if they were classes and use the machinery already in-place to perform class instance generation.

  • Adds the _PlaceholderCollectionBase as "base-class" for all the collections type.
  • Registering base types now allows you to override methods for determining whether a member is public (public_member_checker) and for getting types hints (type_hints_fetcher). There is now also a TYPE_HINT_FETCHER global for storing the per-type 'type hints fetcher' function.
  • _PlaceholderCollectionBase is registered base type. To get class-like behaviour from a collection, we override the instance generator, the member fetcher, the type-hint fetcher and the is-public-member checker.
  • A small change to get_generatable_class_base, allows it to recognise collection types and return _PlaceholderCollectionBase when appropriate.

A consequence of this change, is that it's now possible to generate lists, dicts or sets directly with a call to generate_class_instance, since collections are themselves treated as pseudo-classes, for example,

list1 = generate_class_instance(list[int])
dict1 = generate_class_instance(dict[str, ReferenceDataclass])
set1 = generate_class_instance(set[float])

@github-actions
Copy link
Copy Markdown

✅ Code coverage on feature/dict-generation is at 90% (compared to main being at 90%)

🎉 No files have reduced coverage 🎉

Full Coverage Report

File Details
src/assertical/__init__.py ✅ 100% → 100%
src/assertical/asserts/__init__.py ✅ 100% → 100%
src/assertical/asserts/generator.py ✅ 100% → 100%
src/assertical/asserts/pandas.py ✅ 88% → 88%
src/assertical/asserts/time.py ✅ 95% → 95%
src/assertical/asserts/type.py ✅ 92% → 92%
src/assertical/fake/__init__.py ✅ 100% → 100%
src/assertical/fake/asyncio.py ✅ 100% → 100%
src/assertical/fake/generator.py ✅ 92% → 92%
src/assertical/fake/http.py ✅ 71% → 71%
src/assertical/fixtures/__init__.py ✅ 100% → 100%
src/assertical/fixtures/environment.py ✅ 100% → 100%
src/assertical/fixtures/fastapi.py ✅ 97% → 97%
src/assertical/fixtures/generator.py ✅ 100% → 100%
src/assertical/snapshot.py ✅ 100% → 100%
TOTAL ✅ 90% → 90%

@github-actions
Copy link
Copy Markdown

✅ Code coverage on feature/dict-generation is at 90% (compared to main being at 90%)

🎉 No files have reduced coverage 🎉

Full Coverage Report

File Details
src/assertical/__init__.py ✅ 100% → 100%
src/assertical/asserts/__init__.py ✅ 100% → 100%
src/assertical/asserts/generator.py ✅ 100% → 100%
src/assertical/asserts/pandas.py ✅ 88% → 88%
src/assertical/asserts/time.py ✅ 95% → 95%
src/assertical/asserts/type.py ✅ 92% → 92%
src/assertical/fake/__init__.py ✅ 100% → 100%
src/assertical/fake/asyncio.py ✅ 100% → 100%
src/assertical/fake/generator.py ✅ 92% → 92%
src/assertical/fake/http.py ✅ 71% → 71%
src/assertical/fixtures/__init__.py ✅ 100% → 100%
src/assertical/fixtures/environment.py ✅ 100% → 100%
src/assertical/fixtures/fastapi.py ✅ 97% → 97%
src/assertical/fixtures/generator.py ✅ 100% → 100%
src/assertical/snapshot.py ✅ 100% → 100%
TOTAL ✅ 90% → 90%

@github-actions
Copy link
Copy Markdown

✅ Code coverage on feature/dict-generation is at 90% (compared to main being at 90%)

🎉 No files have reduced coverage 🎉

Full Coverage Report

File Details
src/assertical/__init__.py ✅ 100% → 100%
src/assertical/asserts/__init__.py ✅ 100% → 100%
src/assertical/asserts/generator.py ✅ 100% → 100%
src/assertical/asserts/pandas.py ✅ 88% → 88%
src/assertical/asserts/time.py ✅ 95% → 95%
src/assertical/asserts/type.py ✅ 92% → 92%
src/assertical/fake/__init__.py ✅ 100% → 100%
src/assertical/fake/asyncio.py ✅ 100% → 100%
src/assertical/fake/generator.py ✅ 92% → 92%
src/assertical/fake/http.py ✅ 71% → 71%
src/assertical/fixtures/__init__.py ✅ 100% → 100%
src/assertical/fixtures/environment.py ✅ 100% → 100%
src/assertical/fixtures/fastapi.py ✅ 97% → 97%
src/assertical/fixtures/generator.py ✅ 100% → 100%
src/assertical/snapshot.py ✅ 100% → 100%
TOTAL ✅ 90% → 90%

@mikejturner
Copy link
Copy Markdown
Contributor Author

A fix has also been added to remove the arbitrary increase of 1000 for seeds when handling recursion generation of classes. The code now correctly keeps track of how many values are generated (and by extension what the seed increment should be).

@mikejturner mikejturner requested a review from joshvote February 21, 2026 08:12
Comment thread src/assertical/fake/generator.py
Comment thread src/assertical/fake/generator.py
Comment thread tests/fake/test_generator_dataclass.py Outdated
@joshvote
Copy link
Copy Markdown
Contributor

Forgot to mention - overall I'm happy with the approach

@github-actions
Copy link
Copy Markdown

✅ Code coverage on feature/dict-generation is at 90% (compared to main being at 90%)

🎉 No files have reduced coverage 🎉

Full Coverage Report

File Details
src/assertical/__init__.py ✅ 100% → 100%
src/assertical/asserts/__init__.py ✅ 100% → 100%
src/assertical/asserts/generator.py ✅ 100% → 100%
src/assertical/asserts/pandas.py ✅ 88% → 88%
src/assertical/asserts/time.py ✅ 95% → 95%
src/assertical/asserts/type.py ✅ 92% → 92%
src/assertical/fake/__init__.py ✅ 100% → 100%
src/assertical/fake/asyncio.py ✅ 100% → 100%
src/assertical/fake/generator.py ✅ 92% → 93%
src/assertical/fake/http.py ✅ 71% → 71%
src/assertical/fixtures/__init__.py ✅ 100% → 100%
src/assertical/fixtures/environment.py ✅ 100% → 100%
src/assertical/fixtures/fastapi.py ✅ 97% → 97%
src/assertical/fixtures/generator.py ✅ 100% → 100%
src/assertical/snapshot.py ✅ 100% → 100%
TOTAL ✅ 90% → 90%

@github-actions
Copy link
Copy Markdown

✅ Code coverage on feature/dict-generation is at 90% (compared to main being at 90%)

🎉 No files have reduced coverage 🎉

Full Coverage Report

File Details
src/assertical/__init__.py ✅ 100% → 100%
src/assertical/asserts/__init__.py ✅ 100% → 100%
src/assertical/asserts/generator.py ✅ 100% → 100%
src/assertical/asserts/pandas.py ✅ 88% → 88%
src/assertical/asserts/time.py ✅ 95% → 95%
src/assertical/asserts/type.py ✅ 92% → 92%
src/assertical/fake/__init__.py ✅ 100% → 100%
src/assertical/fake/asyncio.py ✅ 100% → 100%
src/assertical/fake/generator.py ✅ 92% → 93%
src/assertical/fake/http.py ✅ 71% → 71%
src/assertical/fixtures/__init__.py ✅ 100% → 100%
src/assertical/fixtures/environment.py ✅ 100% → 100%
src/assertical/fixtures/fastapi.py ✅ 97% → 97%
src/assertical/fixtures/generator.py ✅ 100% → 100%
src/assertical/snapshot.py ✅ 100% → 100%
TOTAL ✅ 90% → 90%

@github-actions
Copy link
Copy Markdown

✅ Code coverage on feature/dict-generation is at 90% (compared to main being at 90%)

🎉 No files have reduced coverage 🎉

Full Coverage Report

File Details
src/assertical/__init__.py ✅ 100% → 100%
src/assertical/asserts/__init__.py ✅ 100% → 100%
src/assertical/asserts/generator.py ✅ 100% → 100%
src/assertical/asserts/pandas.py ✅ 88% → 88%
src/assertical/asserts/time.py ✅ 95% → 95%
src/assertical/asserts/type.py ✅ 92% → 92%
src/assertical/fake/__init__.py ✅ 100% → 100%
src/assertical/fake/asyncio.py ✅ 100% → 100%
src/assertical/fake/generator.py ✅ 92% → 93%
src/assertical/fake/http.py ✅ 71% → 71%
src/assertical/fixtures/__init__.py ✅ 100% → 100%
src/assertical/fixtures/environment.py ✅ 100% → 100%
src/assertical/fixtures/fastapi.py ✅ 97% → 97%
src/assertical/fixtures/generator.py ✅ 100% → 100%
src/assertical/snapshot.py ✅ 100% → 100%
TOTAL ✅ 90% → 90%

@github-actions
Copy link
Copy Markdown

✅ Code coverage on feature/dict-generation is at 90% (compared to main being at 90%)

🎉 No files have reduced coverage 🎉

Full Coverage Report

File Details
src/assertical/__init__.py ✅ 100% → 100%
src/assertical/asserts/__init__.py ✅ 100% → 100%
src/assertical/asserts/generator.py ✅ 100% → 100%
src/assertical/asserts/pandas.py ✅ 88% → 88%
src/assertical/asserts/time.py ✅ 95% → 95%
src/assertical/asserts/type.py ✅ 92% → 92%
src/assertical/fake/__init__.py ✅ 100% → 100%
src/assertical/fake/asyncio.py ✅ 100% → 100%
src/assertical/fake/generator.py ✅ 92% → 93%
src/assertical/fake/http.py ✅ 71% → 71%
src/assertical/fixtures/__init__.py ✅ 100% → 100%
src/assertical/fixtures/environment.py ✅ 100% → 100%
src/assertical/fixtures/fastapi.py ✅ 97% → 97%
src/assertical/fixtures/generator.py ✅ 100% → 100%
src/assertical/snapshot.py ✅ 100% → 100%
TOTAL ✅ 90% → 90%

@github-actions
Copy link
Copy Markdown

✅ Code coverage on feature/dict-generation is at 91% (compared to main being at 90%)

🎉 No files have reduced coverage 🎉

Full Coverage Report

File Details
src/assertical/__init__.py ✅ 100% → 100%
src/assertical/asserts/__init__.py ✅ 100% → 100%
src/assertical/asserts/generator.py ✅ 100% → 100%
src/assertical/asserts/pandas.py ✅ 88% → 88%
src/assertical/asserts/time.py ✅ 95% → 95%
src/assertical/asserts/type.py ✅ 92% → 92%
src/assertical/fake/__init__.py ✅ 100% → 100%
src/assertical/fake/asyncio.py ✅ 100% → 100%
src/assertical/fake/generator.py ✅ 92% → 94%
src/assertical/fake/http.py ✅ 71% → 71%
src/assertical/fixtures/__init__.py ✅ 100% → 100%
src/assertical/fixtures/environment.py ✅ 100% → 100%
src/assertical/fixtures/fastapi.py ✅ 97% → 97%
src/assertical/fixtures/generator.py ✅ 100% → 100%
src/assertical/snapshot.py ✅ 100% → 100%
TOTAL ✅ 90% → 91%

@mikejturner mikejturner merged commit d1a4bec into main Feb 25, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants