From 5898eb79534dea788e0228c36d961b10a711d84f Mon Sep 17 00:00:00 2001 From: Josh Vote Date: Thu, 7 May 2026 17:02:36 +1000 Subject: [PATCH 1/3] Fixed generate_class_instance type hint --- src/assertical/fake/generator.py | 278 +++++++++++++++++++++---------- 1 file changed, 193 insertions(+), 85 deletions(-) diff --git a/src/assertical/fake/generator.py b/src/assertical/fake/generator.py index c017451..d80686e 100644 --- a/src/assertical/fake/generator.py +++ b/src/assertical/fake/generator.py @@ -59,7 +59,10 @@ class CollectionType(IntEnum): SUPPORTED_COLLECTION_TYPES = {list, dict, set} -TWO_PARAMETER_COLLECTION_TYPES: set[CollectionType] = {CollectionType.OPTIONAL_DICT, CollectionType.REQUIRED_DICT} +TWO_PARAMETER_COLLECTION_TYPES: set[CollectionType] = { + CollectionType.OPTIONAL_DICT, + CollectionType.REQUIRED_DICT, +} @dataclass @@ -149,7 +152,9 @@ def get_enum_type(t: Optional[type], include_optional: bool) -> Optional[type]: assert inner_enum_type is not None t_origin = get_origin(t) - is_union = (t_origin == Union or t_origin == UnionType) and len([a for a in get_args(t) if a is not NoneType]) > 1 + is_union = (t_origin == Union or t_origin == UnionType) and len( + [a for a in get_args(t) if a is not NoneType] + ) > 1 if is_union: for union_arg in get_args(t): arg_enum = get_enum_type(union_arg, include_optional) @@ -209,7 +214,9 @@ def get_first_generatable_primitive(t: type, include_optional: bool) -> Optional return enum_t # Check if the type is an extension of a primitive type - if hasattr(t, "__bases__"): # we need this check as types like Optional don't have this property + if hasattr( + t, "__bases__" + ): # we need this check as types like Optional don't have this property for base in inspect.getmro(t): if base in PRIMITIVE_VALUE_GENERATORS: return base @@ -217,7 +224,9 @@ def get_first_generatable_primitive(t: type, include_optional: bool) -> Optional # certain types will just pass through looking at the arguments # eg: Mapped[Optional[int]] is really just Optional[int] for this function's purposes if is_passthrough_type(t): - return get_first_generatable_primitive(remove_passthrough_type(t), include_optional=include_optional) + return get_first_generatable_primitive( + remove_passthrough_type(t), include_optional=include_optional + ) # If we have an Optional[type] (which resolves to Union[NoneType, type]) we need to be careful about how we # extract the type @@ -225,7 +234,9 @@ def get_first_generatable_primitive(t: type, include_optional: bool) -> Optional include_optional_type = include_optional and is_optional_type(t) if origin_type == Union or origin_type == UnionType: for union_arg in get_args(t): - prim_type = get_first_generatable_primitive(union_arg, include_optional=False) + prim_type = get_first_generatable_primitive( + union_arg, include_optional=False + ) if prim_type is not None: return Optional[prim_type] if include_optional_type else prim_type # type: ignore @@ -309,7 +320,9 @@ def is_member_public(member_name: str) -> bool: return len(member_name) > 0 and member_name[0] != "_" -def enumerate_class_properties(t: type) -> Generator[PropertyGenerationDetails, None, None]: # noqa: C901 +def enumerate_class_properties( + t: type, +) -> Generator[PropertyGenerationDetails, None, None]: # noqa: C901 """Iterates through type t's properties returning the PropertyGenerationDetails for each discovered property. Only "public" properties that don't exist on the BaseType will be returned @@ -321,12 +334,13 @@ def enumerate_class_properties(t: type) -> Generator[PropertyGenerationDetails, # We can only generate class instances of classes that inherit from a known base t_generatable_base = get_generatable_class_base(t) if t_generatable_base is None: - raise Exception(f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}") + raise Exception( + f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}" + ) type_hints = TYPE_HINT_FETCHER[t_generatable_base](t) for member_name in CLASS_MEMBER_FETCHERS[t_generatable_base](t): - # Skip members that are private OR that are public members of the base class if not is_member_public(member_name): continue @@ -367,25 +381,40 @@ def enumerate_class_properties(t: type) -> Generator[PropertyGenerationDetails, if collection_type is not None: # Determine second argument (if required) if collection_type in TWO_PARAMETER_COLLECTION_TYPES: - second_member_type = get_args(optional_arg_type)[1] if is_optional else get_args(member_type)[1] - second_optional_arg_type = get_optional_type_argument(second_member_type) + second_member_type = ( + get_args(optional_arg_type)[1] + if is_optional + else get_args(member_type)[1] + ) + second_optional_arg_type = get_optional_type_argument( + second_member_type + ) second_is_optional = second_optional_arg_type is not None - if collection_type in (CollectionType.OPTIONAL_DICT, CollectionType.REQUIRED_DICT): + if collection_type in ( + CollectionType.OPTIONAL_DICT, + CollectionType.REQUIRED_DICT, + ): if is_generatable_type(second_member_type): second_type_to_generate = get_first_generatable_primitive( second_member_type, include_optional=False ) - assert ( - second_type_to_generate is not None - ), f"Error generating member {member_name}. Couldn't find type for {second_member_type}" + assert second_type_to_generate is not None, ( + f"Error generating member {member_name}. Couldn't find type for {second_member_type}" + ) second_is_primitive = True elif get_generatable_class_base(second_member_type) is not None: second_type_to_generate = ( - second_optional_arg_type if second_is_optional else second_member_type + second_optional_arg_type + if second_is_optional + else second_member_type ) # Determine first argument - member_type = get_args(optional_arg_type)[0] if is_optional else get_args(member_type)[0] + member_type = ( + get_args(optional_arg_type)[0] + if is_optional + else get_args(member_type)[0] + ) optional_arg_type = get_optional_type_argument(member_type) is_optional = optional_arg_type is not None @@ -402,10 +431,12 @@ def enumerate_class_properties(t: type) -> Generator[PropertyGenerationDetails, member_type = t.registry._class_registry[member_type] if is_generatable_type(member_type): - type_to_generate = get_first_generatable_primitive(member_type, include_optional=False) - assert ( - type_to_generate is not None - ), f"Error generating member {member_name}. Couldn't find type for {member_type}" + type_to_generate = get_first_generatable_primitive( + member_type, include_optional=False + ) + assert type_to_generate is not None, ( + f"Error generating member {member_name}. Couldn't find type for {member_type}" + ) is_primitive = True elif get_generatable_class_base(member_type) is not None: type_to_generate = optional_arg_type if is_optional else member_type @@ -425,55 +456,42 @@ def enumerate_class_properties(t: type) -> Generator[PropertyGenerationDetails, ) -def generate_class_instance( # noqa: C901 +def _generate_class_instance_with_seed( # noqa: C901 t: type[AnyType], - seed: int = 1, - optional_is_none: bool = False, - generate_relationships: bool = False, - _return_seed: bool = False, - _visited_type_stack: Optional[list[type]] = None, + seed: int, + optional_is_none: bool, + generate_relationships: bool, + visited_type_stack: Optional[list[type]], **kwargs: Any, -) -> Union[AnyType, tuple[AnyType, int]]: - """Given a child class of a key to CLASS_INSTANCE_GENERATORS - generate an instance of that class - with all properties being assigned unique values based off of seed. The values will match type hints - - Any "private" members beginning with '_' will be skipped - - generate_relationships will recursively generate relationships generating instances as required. (SQL ALchemy - will handle assigning backreferences too) - - If the type cannot be instantiated due to missing type hints / other info exceptions will be raised - - Any additional specified "kwargs" will override the generated members. Eg generate_class_instance(Foo, my_arg="123") - will generate a new instance of Foo as per normal but the named member "my_arg" will have value "123". Please note - that this will change the way remaining values are allocated such that: - generate_class_instance(Foo, my_arg="123") != (generate_class_instance(Foo).my_arg = "123") - Specifying an invalid member name will raise an Exception - - _visited_type_stack should not be specified - it's for internal use only""" +) -> tuple[AnyType, int]: + """Internal function - performs the work of generate_class_instance but returns each generated type with an updated + seed value""" t = remove_passthrough_type(t) # stop back references from infinite looping - if _visited_type_stack is None: - _visited_type_stack = [] - if t in _visited_type_stack: + if visited_type_stack is None: + visited_type_stack = [] + if t in visited_type_stack: # This only happens in recursion - the top level object will never be None - return (None, seed) if _return_seed else None # type: ignore - _visited_type_stack.append(t) + return (None, seed) # type: ignore + visited_type_stack.append(t) # We can only generate class instances of classes that inherit from a known base t_generatable_base = get_generatable_class_base(t) if t_generatable_base is None: - raise Exception(f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}") + raise Exception( + f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}" + ) # We will be creating a dict of property names and their generated values # Those values can be basic primitive values or optionally populated current_seed = seed values: dict[str, Any] = {} - kwargs_references: set[str] = set() # For making sure we use all kwargs values to catch typos + kwargs_references: set[str] = ( + set() + ) # For making sure we use all kwargs values to catch typos for member in enumerate_class_properties(t): - # If there is a custom override for a member - apply it before going any further if member.name in kwargs: values[member.name] = kwargs[member.name] @@ -487,26 +505,31 @@ def generate_class_instance( # noqa: C901 f"Type {t} has property {member.name} with type {member.declared_type} that cannot be generated" ) - generated_value: Any = None empty_collection: bool = False collection_type: Optional[CollectionType] = member.collection_type def generate_member( - is_primitive_type: bool, type_to_generate: type, current_seed: int, empty_collection: bool + is_primitive_type: bool, + type_to_generate: type, + current_seed: int, + empty_collection: bool, ) -> tuple[Any, int, bool]: if is_primitive_type: - generated_value = generate_value(type_to_generate, seed=current_seed, optional_is_none=optional_is_none) + generated_value = generate_value( + type_to_generate, + seed=current_seed, + optional_is_none=optional_is_none, + ) current_seed += 1 else: generated_value = None if generate_relationships: - generated_value, current_seed = generate_class_instance( + generated_value, current_seed = _generate_class_instance_with_seed( type_to_generate, seed=current_seed, optional_is_none=optional_is_none, generate_relationships=generate_relationships, - _visited_type_stack=_visited_type_stack, - _return_seed=True, + visited_type_stack=visited_type_stack, ) # None can be generated when Type A has child B that includes a backreference to A. in these @@ -520,12 +543,18 @@ def generate_member( if optional_is_none and ( member.collection_type - in [CollectionType.OPTIONAL_LIST, CollectionType.OPTIONAL_SET, CollectionType.OPTIONAL_DICT] + in [ + CollectionType.OPTIONAL_LIST, + CollectionType.OPTIONAL_SET, + CollectionType.OPTIONAL_DICT, + ] ): # We can short circuit some generation if we know the top level collection should be None # In this case - we just set everything to None generated_value = None - collection_type = None # Don't fill with None - just set the member value to None + collection_type = ( + None # Don't fill with None - just set the member value to None + ) current_seed += 1 elif optional_is_none and member.is_optional: # In this case the parent collection is NOT able to be set to None but does support adding items @@ -540,38 +569,92 @@ def generate_member( empty_collection=empty_collection, ) - if collection_type == CollectionType.REQUIRED_LIST or collection_type == CollectionType.OPTIONAL_LIST: + if ( + collection_type == CollectionType.REQUIRED_LIST + or collection_type == CollectionType.OPTIONAL_LIST + ): values[member.name] = [] if empty_collection else [generated_value] - elif collection_type == CollectionType.REQUIRED_SET or collection_type == CollectionType.OPTIONAL_SET: - values[member.name] = set([]) if empty_collection else set([generated_value]) - elif collection_type == CollectionType.REQUIRED_DICT or collection_type == CollectionType.OPTIONAL_DICT: + elif ( + collection_type == CollectionType.REQUIRED_SET + or collection_type == CollectionType.OPTIONAL_SET + ): + values[member.name] = ( + set([]) if empty_collection else set([generated_value]) + ) + elif ( + collection_type == CollectionType.REQUIRED_DICT + or collection_type == CollectionType.OPTIONAL_DICT + ): if optional_is_none and member.second_is_optional: # In this case the parent collection is NOT able to be set to None but does support adding items # that are None - so we just add a None to the parent collection (or just generate None) second_generated_value = None current_seed += 1 else: - second_generated_value, current_seed, empty_collection = generate_member( - is_primitive_type=member.second_is_primitive_type, # type: ignore - type_to_generate=member.second_type_to_generate, # type: ignore - current_seed=current_seed, - empty_collection=empty_collection, + second_generated_value, current_seed, empty_collection = ( + generate_member( + is_primitive_type=member.second_is_primitive_type, # type: ignore + type_to_generate=member.second_type_to_generate, # type: ignore + current_seed=current_seed, + empty_collection=empty_collection, + ) ) - values[member.name] = {} if empty_collection else {generated_value: second_generated_value} + values[member.name] = ( + {} if empty_collection else {generated_value: second_generated_value} + ) else: values[member.name] = generated_value expected_kwargs_references = set(kwargs.keys()) if kwargs_references != expected_kwargs_references: - raise Exception(f"The following kwargs were unused {expected_kwargs_references.difference(kwargs_references)}") + raise Exception( + f"The following kwargs were unused {expected_kwargs_references.difference(kwargs_references)}" + ) - _visited_type_stack.pop() # When we finish generating a type, allow recursion back into that type + visited_type_stack.pop() # When we finish generating a type, allow recursion back into that type instance = CLASS_INSTANCE_GENERATORS[t_generatable_base](t, values) - return (instance, current_seed) if _return_seed else instance + return (instance, current_seed) + + +def generate_class_instance( # noqa: C901 + t: type[AnyType], + seed: int = 1, + optional_is_none: bool = False, + generate_relationships: bool = False, + _visited_type_stack: Optional[list[type]] = None, + **kwargs: Any, +) -> AnyType: + """Given a child class of a key to CLASS_INSTANCE_GENERATORS - generate an instance of that class + with all properties being assigned unique values based off of seed. The values will match type hints + + Any "private" members beginning with '_' will be skipped + + generate_relationships will recursively generate relationships generating instances as required. (SQL ALchemy + will handle assigning backreferences too) + + If the type cannot be instantiated due to missing type hints / other info exceptions will be raised + + Any additional specified "kwargs" will override the generated members. Eg generate_class_instance(Foo, my_arg="123") + will generate a new instance of Foo as per normal but the named member "my_arg" will have value "123". Please note + that this will change the way remaining values are allocated such that: + generate_class_instance(Foo, my_arg="123") != (generate_class_instance(Foo).my_arg = "123") + Specifying an invalid member name will raise an Exception""" + + result, _ = _generate_class_instance_with_seed( + t, + seed=seed, + optional_is_none=optional_is_none, + generate_relationships=generate_relationships, + visited_type_stack=[], + **kwargs, + ) + return result -def clone_class_instance(obj: AnyType, ignored_properties: Optional[set[str]] = None) -> AnyType: +def clone_class_instance( + obj: AnyType, ignored_properties: Optional[set[str]] = None +) -> AnyType: """Given an instance of a child class of a key to CLASS_INSTANCE_GENERATORS - generate a new instance of that class using references to the values in the current public properties in obj (i.e. a shallow clone). @@ -586,7 +669,9 @@ def clone_class_instance(obj: AnyType, ignored_properties: Optional[set[str]] = # We can only generate class instances of classes that inherit from a known base t_generatable_base = get_generatable_class_base(t) if t_generatable_base is None: - raise Exception(f"Obj {obj} with type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}") + raise Exception( + f"Obj {obj} with type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}" + ) # We will be creating a dict of property names and their generated values # Those values can be basic primitive values or optionally populated @@ -631,7 +716,9 @@ def check_class_instance_equality( # We can only generate class instances of classes that inherit from a known base t_generatable_base = get_generatable_class_base(t) if t_generatable_base is None: - raise Exception(f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}") + raise Exception( + f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}" + ) # We will be creating a dict of property names and their generated values # Those values can be basic primitive values or optionally populated @@ -641,9 +728,13 @@ def check_class_instance_equality( continue if member.declared_type is None: - raise Exception(f"Type {t} has property {member.name} that is missing a type hint") + raise Exception( + f"Type {t} has property {member.name} that is missing a type hint" + ) - if member.type_to_generate is None or not is_generatable_type(member.type_to_generate): + if member.type_to_generate is None or not is_generatable_type( + member.type_to_generate + ): continue expected_val = getattr(expected, member.name) @@ -653,7 +744,9 @@ def check_class_instance_equality( continue if expected_val != actual_val: - error_messages.append(f"{member.name}: {member.declared_type} expected {expected_val} but got {actual_val}") + error_messages.append( + f"{member.name}: {member.declared_type} expected {expected_val} but got {actual_val}" + ) return error_messages @@ -688,7 +781,12 @@ def register_value_generator(t: type, generator: Callable[[int], Any]) -> None: register_value_generator(bool, lambda seed: (seed % 2) == 0) register_value_generator(Decimal, lambda seed: Decimal(seed)) register_value_generator( - datetime, lambda seed: datetime(2010, 1, 1, tzinfo=timezone.utc) + timedelta(days=seed) + timedelta(seconds=seed) + datetime, + lambda seed: ( + datetime(2010, 1, 1, tzinfo=timezone.utc) + + timedelta(days=seed) + + timedelta(seconds=seed) + ), ) register_value_generator(time, lambda seed: time(seed % 24, seed % 60, (seed + 1) % 60)) register_value_generator(timedelta, lambda seed: timedelta(seconds=seed)) @@ -701,8 +799,12 @@ def register_value_generator(t: type, generator: Callable[[int], Any]) -> None: CLASS_MEMBER_FETCHERS: dict[type, Callable[[type], list[str]]] = {} # the set all base class public members keyed by the base class that generated them BASE_CLASS_PUBLIC_MEMBERS: dict[type, set[str]] = {} -DEFAULT_CLASS_INSTANCE_GENERATOR: Callable[[type, dict[str, Any]], Any] = lambda target, kwargs: target(**kwargs) -DEFAULT_MEMBER_FETCHER: Callable[[type], list[str]] = lambda target: [name for (name, _) in inspect.getmembers(target)] +DEFAULT_CLASS_INSTANCE_GENERATOR: Callable[[type, dict[str, Any]], Any] = ( + lambda target, kwargs: target(**kwargs) +) +DEFAULT_MEMBER_FETCHER: Callable[[type], list[str]] = lambda target: [ + name for (name, _) in inspect.getmembers(target) +] DEFAULT_PUBLIC_MEMBER_CHECKER: Callable[[str], bool] = is_member_public TYPE_HINT_FETCHER: dict[type, Callable[[type], dict[str, type]]] = {} @@ -728,7 +830,9 @@ def register_base_type( polluting the global registry""" CLASS_INSTANCE_GENERATORS[base_type] = instance_generator CLASS_MEMBER_FETCHERS[base_type] = member_fetcher - BASE_CLASS_PUBLIC_MEMBERS[base_type] = set([m for m in member_fetcher(base_type) if public_member_checker(m)]) + BASE_CLASS_PUBLIC_MEMBERS[base_type] = set( + [m for m in member_fetcher(base_type) if public_member_checker(m)] + ) TYPE_HINT_FETCHER[base_type] = type_hint_fetcher @@ -764,8 +868,12 @@ def register_base_type( ) if "sqlalchemy" in sys.modules: - register_base_type(DeclarativeBase, DEFAULT_CLASS_INSTANCE_GENERATOR, DEFAULT_MEMBER_FETCHER) - register_base_type(DeclarativeBaseNoMeta, DEFAULT_CLASS_INSTANCE_GENERATOR, DEFAULT_MEMBER_FETCHER) + register_base_type( + DeclarativeBase, DEFAULT_CLASS_INSTANCE_GENERATOR, DEFAULT_MEMBER_FETCHER + ) + register_base_type( + DeclarativeBaseNoMeta, DEFAULT_CLASS_INSTANCE_GENERATOR, DEFAULT_MEMBER_FETCHER + ) # SQL Alchemy does some dynamic class construction that inspect.getmembers() can't pickup # This is our Workaround to ensure that the BASE_CLASS_PUBLIC_MEMBERS is properly populated From 1346db92ade0cf30f92df21c9d481fdbab067fb1 Mon Sep 17 00:00:00 2001 From: Josh Vote Date: Thu, 7 May 2026 17:07:20 +1000 Subject: [PATCH 2/3] Removed superfluous args --- src/assertical/fake/generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/assertical/fake/generator.py b/src/assertical/fake/generator.py index df27a7f..3d66d33 100644 --- a/src/assertical/fake/generator.py +++ b/src/assertical/fake/generator.py @@ -634,7 +634,6 @@ def generate_class_instance( # noqa: C901 seed: int = 1, optional_is_none: bool = False, generate_relationships: bool = False, - _visited_type_stack: Optional[list[type]] = None, **kwargs: Any, ) -> AnyType: """Given a child class of a key to CLASS_INSTANCE_GENERATORS - generate an instance of that class From 290dc32f7b474db393096de33d25f864e3be69aa Mon Sep 17 00:00:00 2001 From: Josh Vote Date: Thu, 7 May 2026 17:08:50 +1000 Subject: [PATCH 3/3] Linting --- src/assertical/fake/generator.py | 164 +++++++++---------------------- 1 file changed, 45 insertions(+), 119 deletions(-) diff --git a/src/assertical/fake/generator.py b/src/assertical/fake/generator.py index 3d66d33..00143f2 100644 --- a/src/assertical/fake/generator.py +++ b/src/assertical/fake/generator.py @@ -152,9 +152,7 @@ def get_enum_type(t: Optional[type], include_optional: bool) -> Optional[type]: assert inner_enum_type is not None t_origin = get_origin(t) - is_union = (t_origin == Union or t_origin == UnionType) and len( - [a for a in get_args(t) if a is not NoneType] - ) > 1 + is_union = (t_origin == Union or t_origin == UnionType) and len([a for a in get_args(t) if a is not NoneType]) > 1 if is_union: for union_arg in get_args(t): arg_enum = get_enum_type(union_arg, include_optional) @@ -214,9 +212,7 @@ def get_first_generatable_primitive(t: type, include_optional: bool) -> Optional return enum_t # Check if the type is an extension of a primitive type - if hasattr( - t, "__bases__" - ): # we need this check as types like Optional don't have this property + if hasattr(t, "__bases__"): # we need this check as types like Optional don't have this property for base in inspect.getmro(t): if base in PRIMITIVE_VALUE_GENERATORS: return base @@ -224,9 +220,7 @@ def get_first_generatable_primitive(t: type, include_optional: bool) -> Optional # certain types will just pass through looking at the arguments # eg: Mapped[Optional[int]] is really just Optional[int] for this function's purposes if is_passthrough_type(t): - return get_first_generatable_primitive( - remove_passthrough_type(t), include_optional=include_optional - ) + return get_first_generatable_primitive(remove_passthrough_type(t), include_optional=include_optional) # If we have an Optional[type] (which resolves to Union[NoneType, type]) we need to be careful about how we # extract the type @@ -234,9 +228,7 @@ def get_first_generatable_primitive(t: type, include_optional: bool) -> Optional include_optional_type = include_optional and is_optional_type(t) if origin_type == Union or origin_type == UnionType: for union_arg in get_args(t): - prim_type = get_first_generatable_primitive( - union_arg, include_optional=False - ) + prim_type = get_first_generatable_primitive(union_arg, include_optional=False) if prim_type is not None: return Optional[prim_type] if include_optional_type else prim_type # type: ignore @@ -320,9 +312,9 @@ def is_member_public(member_name: str) -> bool: return len(member_name) > 0 and member_name[0] != "_" -def enumerate_class_properties( +def enumerate_class_properties( # noqa: C901 t: type, -) -> Generator[PropertyGenerationDetails, None, None]: # noqa: C901 +) -> Generator[PropertyGenerationDetails, None, None]: """Iterates through type t's properties returning the PropertyGenerationDetails for each discovered property. Only "public" properties that don't exist on the BaseType will be returned @@ -334,9 +326,7 @@ def enumerate_class_properties( # We can only generate class instances of classes that inherit from a known base t_generatable_base = get_generatable_class_base(t) if t_generatable_base is None: - raise Exception( - f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}" - ) + raise Exception(f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}") type_hints = TYPE_HINT_FETCHER[t_generatable_base](t) @@ -381,14 +371,8 @@ def enumerate_class_properties( if collection_type is not None: # Determine second argument (if required) if collection_type in TWO_PARAMETER_COLLECTION_TYPES: - second_member_type = ( - get_args(optional_arg_type)[1] - if is_optional - else get_args(member_type)[1] - ) - second_optional_arg_type = get_optional_type_argument( - second_member_type - ) + second_member_type = get_args(optional_arg_type)[1] if is_optional else get_args(member_type)[1] + second_optional_arg_type = get_optional_type_argument(second_member_type) second_is_optional = second_optional_arg_type is not None if collection_type in ( CollectionType.OPTIONAL_DICT, @@ -398,23 +382,17 @@ def enumerate_class_properties( second_type_to_generate = get_first_generatable_primitive( second_member_type, include_optional=False ) - assert second_type_to_generate is not None, ( - f"Error generating member {member_name}. Couldn't find type for {second_member_type}" - ) + assert ( + second_type_to_generate is not None + ), f"Error generating member {member_name}. Couldn't find type for {second_member_type}" second_is_primitive = True elif get_generatable_class_base(second_member_type) is not None: second_type_to_generate = ( - second_optional_arg_type - if second_is_optional - else second_member_type + second_optional_arg_type if second_is_optional else second_member_type ) # Determine first argument - member_type = ( - get_args(optional_arg_type)[0] - if is_optional - else get_args(member_type)[0] - ) + member_type = get_args(optional_arg_type)[0] if is_optional else get_args(member_type)[0] optional_arg_type = get_optional_type_argument(member_type) is_optional = optional_arg_type is not None @@ -431,12 +409,10 @@ def enumerate_class_properties( member_type = t.registry._class_registry[member_type] if is_generatable_type(member_type): - type_to_generate = get_first_generatable_primitive( - member_type, include_optional=False - ) - assert type_to_generate is not None, ( - f"Error generating member {member_name}. Couldn't find type for {member_type}" - ) + type_to_generate = get_first_generatable_primitive(member_type, include_optional=False) + assert ( + type_to_generate is not None + ), f"Error generating member {member_name}. Couldn't find type for {member_type}" is_primitive = True elif get_generatable_class_base(member_type) is not None: type_to_generate = optional_arg_type if is_optional else member_type @@ -479,17 +455,13 @@ def _generate_class_instance_with_seed( # noqa: C901 # We can only generate class instances of classes that inherit from a known base t_generatable_base = get_generatable_class_base(t) if t_generatable_base is None: - raise Exception( - f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}" - ) + raise Exception(f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}") # We will be creating a dict of property names and their generated values # Those values can be basic primitive values or optionally populated current_seed = seed values: dict[str, Any] = {} - kwargs_references: set[str] = ( - set() - ) # For making sure we use all kwargs values to catch typos + kwargs_references: set[str] = set() # For making sure we use all kwargs values to catch typos for member in enumerate_class_properties(t): # If there is a custom override for a member - apply it before going any further @@ -510,8 +482,7 @@ def _generate_class_instance_with_seed( # noqa: C901 CollectionType.OPTIONAL_DICT, ): if member.second_type_to_generate is None and not ( - optional_is_none - and member.collection_type == CollectionType.OPTIONAL_DICT + optional_is_none and member.collection_type == CollectionType.OPTIONAL_DICT ): raise Exception( f"Type {t} has property {member.name} with type {member.declared_type} that cannot be generated" @@ -564,9 +535,7 @@ def generate_member( # We can short circuit some generation if we know the top level collection should be None # In this case - we just set everything to None generated_value = None - collection_type = ( - None # Don't fill with None - just set the member value to None - ) + collection_type = None # Don't fill with None - just set the member value to None current_seed += 1 elif optional_is_none and member.is_optional: # In this case the parent collection is NOT able to be set to None but does support adding items @@ -581,47 +550,30 @@ def generate_member( empty_collection=empty_collection, ) - if ( - collection_type == CollectionType.REQUIRED_LIST - or collection_type == CollectionType.OPTIONAL_LIST - ): + if collection_type == CollectionType.REQUIRED_LIST or collection_type == CollectionType.OPTIONAL_LIST: values[member.name] = [] if empty_collection else [generated_value] - elif ( - collection_type == CollectionType.REQUIRED_SET - or collection_type == CollectionType.OPTIONAL_SET - ): - values[member.name] = ( - set([]) if empty_collection else set([generated_value]) - ) - elif ( - collection_type == CollectionType.REQUIRED_DICT - or collection_type == CollectionType.OPTIONAL_DICT - ): + elif collection_type == CollectionType.REQUIRED_SET or collection_type == CollectionType.OPTIONAL_SET: + values[member.name] = set([]) if empty_collection else set([generated_value]) + elif collection_type == CollectionType.REQUIRED_DICT or collection_type == CollectionType.OPTIONAL_DICT: if optional_is_none and member.second_is_optional: # In this case the parent collection is NOT able to be set to None but does support adding items # that are None - so we just add a None to the parent collection (or just generate None) second_generated_value = None current_seed += 1 else: - second_generated_value, current_seed, empty_collection = ( - generate_member( - is_primitive_type=member.second_is_primitive_type, # type: ignore - type_to_generate=member.second_type_to_generate, # type: ignore - current_seed=current_seed, - empty_collection=empty_collection, - ) + second_generated_value, current_seed, empty_collection = generate_member( + is_primitive_type=member.second_is_primitive_type, # type: ignore + type_to_generate=member.second_type_to_generate, # type: ignore + current_seed=current_seed, + empty_collection=empty_collection, ) - values[member.name] = ( - {} if empty_collection else {generated_value: second_generated_value} - ) + values[member.name] = {} if empty_collection else {generated_value: second_generated_value} else: values[member.name] = generated_value expected_kwargs_references = set(kwargs.keys()) if kwargs_references != expected_kwargs_references: - raise Exception( - f"The following kwargs were unused {expected_kwargs_references.difference(kwargs_references)}" - ) + raise Exception(f"The following kwargs were unused {expected_kwargs_references.difference(kwargs_references)}") visited_type_stack.pop() # When we finish generating a type, allow recursion back into that type @@ -663,9 +615,7 @@ def generate_class_instance( # noqa: C901 return result -def clone_class_instance( - obj: AnyType, ignored_properties: Optional[set[str]] = None -) -> AnyType: +def clone_class_instance(obj: AnyType, ignored_properties: Optional[set[str]] = None) -> AnyType: """Given an instance of a child class of a key to CLASS_INSTANCE_GENERATORS - generate a new instance of that class using references to the values in the current public properties in obj (i.e. a shallow clone). @@ -680,9 +630,7 @@ def clone_class_instance( # We can only generate class instances of classes that inherit from a known base t_generatable_base = get_generatable_class_base(t) if t_generatable_base is None: - raise Exception( - f"Obj {obj} with type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}" - ) + raise Exception(f"Obj {obj} with type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}") # We will be creating a dict of property names and their generated values # Those values can be basic primitive values or optionally populated @@ -727,9 +675,7 @@ def check_class_instance_equality( # We can only generate class instances of classes that inherit from a known base t_generatable_base = get_generatable_class_base(t) if t_generatable_base is None: - raise Exception( - f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}" - ) + raise Exception(f"Type {t} does not inherit from one of {CLASS_INSTANCE_GENERATORS.keys()}") # We will be creating a dict of property names and their generated values # Those values can be basic primitive values or optionally populated @@ -739,13 +685,9 @@ def check_class_instance_equality( continue if member.declared_type is None: - raise Exception( - f"Type {t} has property {member.name} that is missing a type hint" - ) + raise Exception(f"Type {t} has property {member.name} that is missing a type hint") - if member.type_to_generate is None or not is_generatable_type( - member.type_to_generate - ): + if member.type_to_generate is None or not is_generatable_type(member.type_to_generate): continue expected_val = getattr(expected, member.name) @@ -755,9 +697,7 @@ def check_class_instance_equality( continue if expected_val != actual_val: - error_messages.append( - f"{member.name}: {member.declared_type} expected {expected_val} but got {actual_val}" - ) + error_messages.append(f"{member.name}: {member.declared_type} expected {expected_val} but got {actual_val}") return error_messages @@ -793,11 +733,7 @@ def register_value_generator(t: type, generator: Callable[[int], Any]) -> None: register_value_generator(Decimal, lambda seed: Decimal(seed)) register_value_generator( datetime, - lambda seed: ( - datetime(2010, 1, 1, tzinfo=timezone.utc) - + timedelta(days=seed) - + timedelta(seconds=seed) - ), + lambda seed: (datetime(2010, 1, 1, tzinfo=timezone.utc) + timedelta(days=seed) + timedelta(seconds=seed)), ) register_value_generator(time, lambda seed: time(seed % 24, seed % 60, (seed + 1) % 60)) register_value_generator(timedelta, lambda seed: timedelta(seconds=seed)) @@ -810,12 +746,8 @@ def register_value_generator(t: type, generator: Callable[[int], Any]) -> None: CLASS_MEMBER_FETCHERS: dict[type, Callable[[type], list[str]]] = {} # the set all base class public members keyed by the base class that generated them BASE_CLASS_PUBLIC_MEMBERS: dict[type, set[str]] = {} -DEFAULT_CLASS_INSTANCE_GENERATOR: Callable[[type, dict[str, Any]], Any] = ( - lambda target, kwargs: target(**kwargs) -) -DEFAULT_MEMBER_FETCHER: Callable[[type], list[str]] = lambda target: [ - name for (name, _) in inspect.getmembers(target) -] +DEFAULT_CLASS_INSTANCE_GENERATOR: Callable[[type, dict[str, Any]], Any] = lambda target, kwargs: target(**kwargs) +DEFAULT_MEMBER_FETCHER: Callable[[type], list[str]] = lambda target: [name for (name, _) in inspect.getmembers(target)] DEFAULT_PUBLIC_MEMBER_CHECKER: Callable[[str], bool] = is_member_public TYPE_HINT_FETCHER: dict[type, Callable[[type], dict[str, type]]] = {} @@ -841,9 +773,7 @@ def register_base_type( polluting the global registry""" CLASS_INSTANCE_GENERATORS[base_type] = instance_generator CLASS_MEMBER_FETCHERS[base_type] = member_fetcher - BASE_CLASS_PUBLIC_MEMBERS[base_type] = set( - [m for m in member_fetcher(base_type) if public_member_checker(m)] - ) + BASE_CLASS_PUBLIC_MEMBERS[base_type] = set([m for m in member_fetcher(base_type) if public_member_checker(m)]) TYPE_HINT_FETCHER[base_type] = type_hint_fetcher @@ -879,12 +809,8 @@ def register_base_type( ) if "sqlalchemy" in sys.modules: - register_base_type( - DeclarativeBase, DEFAULT_CLASS_INSTANCE_GENERATOR, DEFAULT_MEMBER_FETCHER - ) - register_base_type( - DeclarativeBaseNoMeta, DEFAULT_CLASS_INSTANCE_GENERATOR, DEFAULT_MEMBER_FETCHER - ) + register_base_type(DeclarativeBase, DEFAULT_CLASS_INSTANCE_GENERATOR, DEFAULT_MEMBER_FETCHER) + register_base_type(DeclarativeBaseNoMeta, DEFAULT_CLASS_INSTANCE_GENERATOR, DEFAULT_MEMBER_FETCHER) # SQL Alchemy does some dynamic class construction that inspect.getmembers() can't pickup # This is our Workaround to ensure that the BASE_CLASS_PUBLIC_MEMBERS is properly populated