如何配置hypothesis.strategies为可选字段始终生成非None值
如何配置hypothesis.strategies为可选字段始终生成非None值
我完全懂你的需求——用Hypothesis生成随机实例,但烦可选字段时不时冒出的None,而且还不想因为类结构变动去维护硬编码的实例化逻辑对吧?
先理下你的场景:你在用别人维护的dataclass结构,比如:
from dataclasses import dataclass @dataclass class Child: f1: int f2: int | None @dataclass class Parent: child: Child
实际的类结构比这复杂得多,层级更深、字段更多。你自己写了个生成函数用Hypothesis造实例,但可选字段偶尔会出None,你希望这些可选字段永远是有效类型值(比如Child.f2必须是int)。
你的当前实现是这样的:
from typing import TypeVar import hypothesis import hypothesis.strategies T = TypeVar("T") def generate(cls: type[T], seed: int) -> T: objects = [] @hypothesis.seed(seed) @hypothesis.given(hypothesis.strategies.from_type(cls)) @hypothesis.settings(max_examples=10) def f(o): objects.append(o) f() # 第一个例子太简单,返回最后一个 return objects[-1] print(generate(Parent, 123)) print(generate(Parent, 456))
输出里会出现f2=None的情况,这正是你要避免的。
解决方案:自定义策略,替换可选类型的默认行为
这里有两种靠谱的方式,都不用硬编码字段,能自动适配类结构变动:
方式1:针对Dataclass的字段递归处理(推荐,无全局副作用)
直接解析dataclass的字段,对X | None这种可选类型,强制生成X类型的随机值,递归处理嵌套结构:
from typing import TypeVar, get_origin, get_args import hypothesis import hypothesis.strategies as st T = TypeVar("T") def generate(cls: type[T], seed: int) -> T: objects = [] # 自定义复合策略:递归生成实例,跳过可选类型的None @st.composite def non_none_optional_generator(draw, target_cls): # 处理dataclass类型 if hasattr(target_cls, "__dataclass_fields__"): field_values = {} for field_name, field in target_cls.__dataclass_fields__.items(): field_type = field.type origin = get_origin(field_type) # 判断是否是X | None的可选类型 if origin is Union and len(get_args(field_type)) == 2 and get_args(field_type)[1] is type(None): # 只生成X类型的随机值,跳过None field_values[field_name] = draw(st.from_type(get_args(field_type)[0])) else: # 递归处理嵌套的类(比如Parent里的Child) field_values[field_name] = draw(non_none_optional_generator(field_type)) return target_cls(**field_values) else: # 非dataclass类型用默认策略生成 return draw(st.from_type(target_cls)) @hypothesis.seed(seed) @hypothesis.given(non_none_optional_generator(cls)) @hypothesis.settings(max_examples=10) def f(o): objects.append(o) f() return objects[-1] # 测试输出,f2永远是int不会为None print(generate(Parent, 123)) print(generate(Parent, 456))
方式2:全局临时替换可选类型的策略(代码更简洁,注意副作用)
如果你的场景里不会同时用其他Hypothesis策略,可以临时注册全局的可选类型策略,用完再恢复:
from typing import TypeVar, Union, Optional, get_args import hypothesis import hypothesis.strategies as st T = TypeVar("T") def generate(cls: type[T], seed: int) -> T: objects = [] original_optional_strategy = None try: # 保存原来的可选类型策略,用完恢复 original_optional_strategy = st.type_strategies.get(Optional) # 自定义可选类型策略:只生成X类型,不生成None @st.cacheable def override_optional_strategy(target_type): x_type = get_args(target_type)[0] return st.from_type(x_type) # 注册新的策略 st.register_type_strategy(Optional, override_optional_strategy) @hypothesis.seed(seed) @hypothesis.given(st.from_type(cls)) @hypothesis.settings(max_examples=10) def f(o): objects.append(o) f() finally: # 恢复原来的策略,避免影响其他代码 if original_optional_strategy: st.type_strategies[Optional] = original_optional_strategy else: st.type_strategies.pop(Optional, None) return objects[-1] # 测试输出,所有可选字段都不会有None print(generate(Parent, 123)) print(generate(Parent, 456))
效果验证
两种方式生成的实例里,Child.f2都会是随机int,再也不会出现None了,而且不管类结构怎么变(加字段、改嵌套),只要是X | None的可选类型,都会自动适配。
备注:内容来源于stack exchange,提问作者obk
相关产品推荐
相关产品推荐

