Skip to content
Open
9 changes: 9 additions & 0 deletions crates/pyrefly_config/src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ pub struct ConfigBase {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spec_compliant_overloads: Option<bool>,

/// Whether to infer injected pytest fixture parameter types from fixture definitions.
/// When false (the default), injected fixture parameters fall back to `Any` unless explicitly annotated.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub infer_pytest_fixture_types: Option<bool>,

/// Any unknown config items
#[serde(default, flatten)]
pub(crate) extras: ExtraConfigs,
Expand Down Expand Up @@ -275,4 +280,8 @@ impl ConfigBase {
pub fn get_spec_compliant_overloads(base: &Self) -> Option<bool> {
base.spec_compliant_overloads
}

pub fn get_infer_pytest_fixture_types(base: &Self) -> Option<bool> {
base.infer_pytest_fixture_types
}
}
20 changes: 20 additions & 0 deletions crates/pyrefly_config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,14 @@ impl ConfigFile {
self.root.spec_compliant_overloads.unwrap())
}

pub fn infer_pytest_fixture_types(&self, path: &Path) -> bool {
self.get_from_sub_configs(ConfigBase::get_infer_pytest_fixture_types, path)
.unwrap_or_else(||
// we can use unwrap here, because the value in the root config must
// be set in `ConfigFile::configure()`.
Comment on lines +918 to +922
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new infer_pytest_fixture_types config is wired into ConfigFile, but it doesn’t appear to be exposed via CLI config overrides (unlike similar booleans such as --spec-compliant-overloads / --strict-callable-subtyping). Consider adding a corresponding flag in crates/pyrefly_config/src/args.rs so users can enable/disable this without editing config files.

Copilot uses AI. Check for mistakes.
self.root.infer_pytest_fixture_types.unwrap())
}

pub fn enabled_ignores(&self, path: &Path) -> &SmallSet<Tool> {
self.get_from_sub_configs(ConfigBase::get_enabled_ignores, path)
.unwrap_or_else(||
Expand Down Expand Up @@ -1216,6 +1224,10 @@ impl ConfigFile {
self.root.spec_compliant_overloads = Some(false);
}

if self.root.infer_pytest_fixture_types.is_none() {
self.root.infer_pytest_fixture_types = Some(false);
}

let tools_from_permissive_ignores = match self.root.permissive_ignores {
Some(true) => Some(Tool::all()),
Some(false) => Some(Tool::default_enabled()),
Expand Down Expand Up @@ -1578,6 +1590,7 @@ mod tests {
ignore_errors_in_generated_code: Some(true),
infer_with_first_use: None,
strict_callable_subtyping: None,
infer_pytest_fixture_types: None,
tensor_shapes: None,
replace_imports_with_any: Some(vec![ModuleWildcard::new("fibonacci").unwrap()]),
ignore_missing_imports: Some(vec![ModuleWildcard::new("sprout").unwrap()]),
Expand All @@ -1603,6 +1616,7 @@ mod tests {
ignore_errors_in_generated_code: Some(false),
infer_with_first_use: Some(false),
strict_callable_subtyping: Some(false),
infer_pytest_fixture_types: None,
tensor_shapes: None,
replace_imports_with_any: Some(Vec::new()),
ignore_missing_imports: Some(Vec::new()),
Expand Down Expand Up @@ -2020,6 +2034,7 @@ output-format = "omit-errors"
ignore_errors_in_generated_code: Some(false),
infer_with_first_use: Some(true),
strict_callable_subtyping: Some(false),
infer_pytest_fixture_types: Some(false),
tensor_shapes: None,
extras: Default::default(),
permissive_ignores: Some(false),
Expand All @@ -2036,6 +2051,7 @@ output-format = "omit-errors"
ModuleWildcard::new("highest").unwrap(),
]),
ignore_errors_in_generated_code: None,
infer_pytest_fixture_types: Some(true),
..Default::default()
},
},
Expand Down Expand Up @@ -2067,6 +2083,8 @@ output-format = "omit-errors"

// test empty value falls back to next
assert!(config.ignore_errors_in_generated_code(Path::new("this/is/highest/priority")));
assert!(config.infer_pytest_fixture_types(Path::new("this/is/highest/priority")));
assert!(!config.infer_pytest_fixture_types(Path::new("this/is/second/priority")));

// test no pattern match
assert!(config.replace_imports_with_any(
Expand Down Expand Up @@ -2443,6 +2461,7 @@ output-format = "omit-errors"
ignore_errors_in_generated_code: Some(false),
infer_with_first_use: Some(true),
strict_callable_subtyping: Some(false),
infer_pytest_fixture_types: Some(false),
tensor_shapes: None,
extras: Default::default(),
permissive_ignores: Some(false),
Expand Down Expand Up @@ -2482,6 +2501,7 @@ output-format = "omit-errors"
ignore_errors_in_generated_code: Some(false),
infer_with_first_use: Some(true),
strict_callable_subtyping: Some(false),
infer_pytest_fixture_types: Some(false),
tensor_shapes: None,
extras: Default::default(),
permissive_ignores: Some(false),
Expand Down
35 changes: 32 additions & 3 deletions pyrefly/lib/alt/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ use crate::binding::binding::KeyClass;
use crate::binding::binding::KeyClassMetadata;
use crate::binding::binding::KeyDecorator;
use crate::binding::binding::KeyLegacyTypeParam;
use crate::binding::bindings::PytestFixtureParamHint;
use crate::config::error_kind::ErrorKind;
use crate::error::collector::ErrorCollector;
use crate::error::context::ErrorInfo;
Expand Down Expand Up @@ -538,6 +539,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
&mut self_type,
&mut decorator_param_hints,
&mut parent_param_hints,
class_key,
errors,
);
let mut tparams = self.scoped_type_params(def.type_params.as_deref(), errors);
Expand Down Expand Up @@ -1015,12 +1017,36 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
self_type: &mut Option<Type>,
decorator_param_hints: &mut Option<DecoratorParamHints>,
parent_param_hints: &mut Option<ParentParamHints>,
class_key: Option<&Idx<KeyClass>>,
errors: &ErrorCollector,
) -> FunctionParamsResult {
let mut paramspec_args = None;
let mut paramspec_kwargs = None;
let mut resolved_param_types = SmallMap::new();
let mut params = Vec::with_capacity(def.parameters.len());
let fixture_hint = |name: &Identifier| {
self.bindings()
.pytest_fixture_param_hint(def, class_key, name)
.map(|hint| match hint {
PytestFixtureParamHint::Any => Type::any_explicit(),
PytestFixtureParamHint::ReturnType(key) => {
let return_ty = self.get(&Key::ReturnType(key)).arc_clone_ty();
if let Some((yield_ty, _, _)) = self.unwrap_generator(&return_ty) {
yield_ty
} else if let Some((yield_ty, _)) =
self.decompose_async_generator(&return_ty)
{
yield_ty
} else if let Some((_, _, coroutine_return_ty)) =
self.unwrap_coroutine(&return_ty)
{
coroutine_return_ty
} else {
return_ty
}
}
})
};
params.extend(def.parameters.posonlyargs.iter().map(|x| {
let decorator_hint = decorator_param_hints
.as_mut()
Expand All @@ -1032,6 +1058,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
.as_mut()
.and_then(|hint| hint.take_posonly())
};
let fixture_hint = fixture_hint(&x.parameter.name);
let ParamTypeResult {
ty,
required,
Expand All @@ -1041,7 +1068,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
x.default.as_deref(),
stub_or_impl,
self_type,
decorator_hint.or(parent_hint),
decorator_hint.or(parent_hint).or(fixture_hint),
errors,
);
if is_unannotated {
Expand All @@ -1066,6 +1093,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
.as_mut()
.and_then(|hint| hint.take_positional())
};
let fixture_hint = fixture_hint(&x.parameter.name);
let ParamTypeResult {
ty,
required,
Expand All @@ -1075,7 +1103,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
x.default.as_deref(),
stub_or_impl,
self_type,
decorator_hint.or(parent_hint),
decorator_hint.or(parent_hint).or(fixture_hint),
errors,
);
if is_unannotated {
Expand Down Expand Up @@ -1147,6 +1175,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
let parent_hint = parent_param_hints
.as_mut()
.and_then(|hint| hint.take_kwonly(&x.parameter.name));
let fixture_hint = fixture_hint(&x.parameter.name);
let ParamTypeResult {
ty,
required,
Expand All @@ -1156,7 +1185,7 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
x.default.as_deref(),
stub_or_impl,
self_type,
parent_hint,
parent_hint.or(fixture_hint),
errors,
);
if is_unannotated {
Expand Down
Loading
Loading