Skip to content

Commit 4d2aaa3

Browse files
fix
1 parent a9ea569 commit 4d2aaa3

20 files changed

Lines changed: 623 additions & 103 deletions

File tree

‎crates/pyrefly_config/src/error_kind.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ pub enum ErrorKind {
295295
InvalidInheritance,
296296
/// Attempting to use a value that is not a valid kind of Literal.
297297
InvalidLiteral,
298+
/// Expected arguments in a mock assertion do not match the patched target's signature.
299+
InvalidMockAssertion,
298300
/// An error caused by incorrect usage of the @overload decorator.
299301
/// e.g. not defining multiple variants for an overloaded function.
300302
InvalidOverload,
@@ -664,6 +666,7 @@ impl ErrorKind {
664666
ErrorKind::InvalidAbstractMethod => Severity::Ignore,
665667
ErrorKind::InvalidCast => Severity::Ignore,
666668
ErrorKind::InvalidDecorator => Severity::Warn,
669+
ErrorKind::InvalidMockAssertion => Severity::Ignore,
667670
ErrorKind::MethodAssign => Severity::Ignore,
668671
ErrorKind::MisplacedIgnore => Severity::Warn,
669672
ErrorKind::MissingAttributePatchTarget => Severity::Warn,

‎crates/pyrefly_types/src/display.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,9 @@ impl<'a> TypeDisplayContext<'a> {
11071107
// Display as the class name (e.g., MaxPool2d)
11081108
self.fmt_helper_generic(&Type::ClassType(module.class.clone()), false, output)
11091109
}
1110+
Type::UnittestMock(mock) => {
1111+
self.fmt_helper_generic(&Type::ClassType(mock.class.clone()), false, output)
1112+
}
11101113
Type::DataFrame(schema) if self.strip_library_schemas => {
11111114
self.fmt_helper_generic(&schema.underlying_type(), false, output)
11121115
}

‎crates/pyrefly_types/src/function.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ pub struct FuncFlags {
244244
/// A method directly inside a `Protocol` class.
245245
pub is_in_protocol_class: bool,
246246
pub is_in_type_checking_block: bool,
247+
/// A mock assertion checks expected arguments without invoking the target function.
248+
pub is_unittest_mock_assertion: bool,
247249
/// Set when the definition has both `*args` and `**kwargs` and both are typed
248250
/// `Any` — explicitly, or implicitly because they are unannotated. Per the
249251
/// typing spec such a signature is equivalent to `...`. Captured at definition

‎crates/pyrefly_types/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,4 @@ pub mod type_var;
6363
pub mod type_var_tuple;
6464
pub mod typed_dict;
6565
pub mod types;
66+
pub mod unittest_mock;

‎crates/pyrefly_types/src/types.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ use crate::type_var::Restriction;
7575
use crate::type_var::TypeVar;
7676
use crate::type_var_tuple::TypeVarTuple;
7777
use crate::typed_dict::TypedDict;
78+
use crate::unittest_mock::UnittestMock;
7879

7980
/// An introduced synthetic variable to range over as yet unknown types.
8081
#[derive(Debug, Copy, Clone, Dupe, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -961,6 +962,8 @@ pub enum Type {
961962
/// Instances of classes have this type, and a term of the form `C[arg1, arg2]`
962963
/// would have the form `Type::Type(box Type::ClassType(C, [arg1, arg2]))`.
963964
ClassType(ClassType),
965+
/// A generated `unittest.mock` instance with a retained assertion signature.
966+
UnittestMock(Box<UnittestMock>),
964967
/// Instances of TypedDicts have this type, and a term of the form `TD[arg1, arg2]`
965968
/// would have the form `Type::Type(box Type::TypedDict(TD, [arg1, arg2]))`. Note
966969
/// that TypedDict class definitions are still represented as `ClassDef(TD)`, just
@@ -1093,6 +1096,7 @@ impl Visit for Type {
10931096
Type::Intersect(x) => x.visit(f),
10941097
Type::ClassDef(x) => x.visit(f),
10951098
Type::ClassType(x) => x.visit(f),
1099+
Type::UnittestMock(x) => x.visit(f),
10961100
Type::TypedDict(x) => x.visit(f),
10971101
Type::PartialTypedDict(x) => x.visit(f),
10981102
Type::ShapedArray(x) => x.visit(f),
@@ -1157,6 +1161,7 @@ impl VisitMut for Type {
11571161
Type::Intersect(x) => x.visit_mut(f),
11581162
Type::ClassDef(x) => x.visit_mut(f),
11591163
Type::ClassType(x) => x.visit_mut(f),
1164+
Type::UnittestMock(x) => x.visit_mut(f),
11601165
Type::TypedDict(x) => x.visit_mut(f),
11611166
Type::PartialTypedDict(x) => x.visit_mut(f),
11621167
Type::ShapedArray(x) => x.visit_mut(f),
@@ -2470,6 +2475,7 @@ impl Type {
24702475
| Type::Union(_)
24712476
| Type::ClassDef(_)
24722477
| Type::ClassType(_)
2478+
| Type::UnittestMock(_)
24732479
| Type::TypedDict(_)
24742480
| Type::PartialTypedDict(_)
24752481
| Type::ShapedArray(_)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
use pyrefly_derive::TypeEq;
9+
use pyrefly_derive::Visit;
10+
use pyrefly_derive::VisitMut;
11+
12+
use crate::class::ClassType;
13+
use crate::types::Type;
14+
15+
/// A generated mock retains its own behavior and a separate signature for call assertions.
16+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
17+
#[derive(Visit, VisitMut, TypeEq)]
18+
pub struct UnittestMock {
19+
pub class: ClassType,
20+
pub assertion_signature: Type,
21+
}

‎pyrefly/lib/alt.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ pub mod subscript;
4040
pub mod traits;
4141
pub mod type_level_dsl;
4242
pub mod types;
43+
pub mod unittest_mock;
4344
pub mod unwrap;

‎pyrefly/lib/alt/attr.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,9 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
635635
context: Option<&dyn Fn() -> ErrorContext>,
636636
todo_ctx: &str,
637637
) -> Type {
638+
if let Some(signature) = self.unittest_mock_assertion_signature(base, attr_name) {
639+
return signature;
640+
}
638641
let attr_base = self.as_attribute_base(base.clone());
639642
let lookup_result = attr_base.clone().map_or_else(
640643
|| LookupResult::internal_error(InternalError::AttributeBaseUndefined(base.clone())),
@@ -2662,6 +2665,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
26622665

26632666
fn as_attribute_base1(&self, ty: Type, acc: &mut Vec<AttributeBase1>) {
26642667
match ty {
2668+
Type::UnittestMock(mock) => self.as_attribute_base1(Type::ClassType(mock.class), acc),
26652669
Type::ClassType(class_type) => {
26662670
if let Some(shaped_array) =
26672671
self.try_reproject_class_type_to_shaped_array(&class_type)

‎pyrefly/lib/alt/call.rs‎

Lines changed: 14 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use std::sync::Arc;
1010

1111
use itertools::Itertools;
1212
use pyrefly_python::dunder;
13-
use pyrefly_python::module_name::ModuleName;
1413
use pyrefly_python::module_path::ModuleStyle;
1514
use pyrefly_types::dimension::ShapeError;
1615
use pyrefly_types::function::BodyKind;
@@ -29,7 +28,6 @@ use pyrefly_util::visit::Visit;
2928
use ruff_python_ast::Arguments;
3029
use ruff_python_ast::Expr;
3130
use ruff_python_ast::ExprCall;
32-
use ruff_python_ast::ExprStringLiteral;
3331
use ruff_python_ast::name::Name;
3432
use ruff_text_size::Ranged;
3533
use ruff_text_size::TextRange;
@@ -75,7 +73,6 @@ use crate::types::function::FunctionKind;
7573
use crate::types::keywords::KwCall;
7674
use crate::types::keywords::TypeMap;
7775
use crate::types::literal::Lit;
78-
use crate::types::module::ModuleType;
7976
use crate::types::type_var::Restriction;
8077
use crate::types::typed_dict::TypedDict;
8178
use crate::types::types::AnyStyle;
@@ -404,6 +401,9 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
404401

405402
fn as_call_target_impl(&self, ty: Type, quantified: Option<Quantified>) -> CallTargetLookup {
406403
match ty {
404+
Type::UnittestMock(mock) => {
405+
self.as_call_target_impl(Type::ClassType(mock.class), quantified)
406+
}
407407
Type::Callable(c) => {
408408
CallTargetLookup::Ok(Box::new(CallTarget::Callable(TargetWithTParams(None, *c))))
409409
}
@@ -3109,7 +3109,8 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
31093109
)
31103110
} else {
31113111
self.expand_mut(&mut callee.ty);
3112-
self.check_unittest_mock_patch_target(&callee.ty, &callee.expr.arguments, errors);
3112+
let unittest_mock_patch_target =
3113+
self.check_unittest_mock_patch_target(&callee.ty, &callee.expr.arguments, errors);
31133114

31143115
let call = CallWithTypes::new();
31153116
let (args, kws) = if callee.ty.is_union() {
@@ -3151,6 +3152,8 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
31513152
errors,
31523153
);
31533154
match ty.callee_kind() {
3155+
_ if ty.toplevel_func_metadata().is_some_and(|metadata| metadata.flags.is_unittest_mock_assertion) =>
3156+
self.call_unittest_mock_assertion(ty, &args, &kws, callee.expr, errors),
31543157
Some(CalleeKind::Function(FunctionKind::AssertType)) => self
31553158
.call_assert_type(
31563159
&callee.expr.arguments.args,
@@ -3428,95 +3431,24 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
34283431
}
34293432
}});
34303433
// TypeIs and TypeGuard functions return bool at runtime
3431-
match result {
3434+
let result = match result {
34323435
Type::TypeIs(_) | Type::TypeGuard(_) => {
34333436
self.heap.mk_class_type(self.stdlib.bool().clone())
34343437
}
34353438
other => other,
3439+
};
3440+
match unittest_mock_patch_target {
3441+
Some(target) => {
3442+
self.specialize_unittest_mock_patch(result, target, &callee.expr.arguments)
3443+
}
3444+
None => result,
34363445
}
34373446
};
34383447

34393448
let result = self.apply_django_annotate_call(result, django_annotate_call);
34403449
self.apply_polars_call_specialization(result, polars_call)
34413450
}
34423451

3443-
fn check_unittest_mock_patch_target(
3444-
&self,
3445-
callee_ty: &Type,
3446-
arguments: &Arguments,
3447-
errors: &ErrorCollector,
3448-
) {
3449-
let Type::ClassType(cls) = callee_ty else {
3450-
return;
3451-
};
3452-
if !cls.has_qname("unittest.mock", "_patcher") {
3453-
return;
3454-
}
3455-
if arguments.args.len() > 1 || arguments.keywords.iter().any(|kw| kw.arg.is_none()) {
3456-
return;
3457-
}
3458-
if arguments
3459-
.find_keyword("create")
3460-
.is_some_and(|kw| !matches!(&kw.value, Expr::BooleanLiteral(lit) if !lit.value))
3461-
{
3462-
return;
3463-
}
3464-
let Some(target_expr) = arguments.find_argument_value("target", 0) else {
3465-
return;
3466-
};
3467-
let Expr::StringLiteral(ExprStringLiteral { value, .. }) = target_expr else {
3468-
return;
3469-
};
3470-
let range = target_expr.range();
3471-
let target = value.to_str();
3472-
let parts: Vec<&str> = target.split('.').collect();
3473-
if parts.len() < 2 || parts.iter().any(|p| p.is_empty()) {
3474-
return;
3475-
}
3476-
3477-
let Some((module_prefix_len, module)) = (1..parts.len()).rev().find_map(|i| {
3478-
let candidate = ModuleName::from_str(&parts[..i].join("."));
3479-
(candidate == self.module().name()
3480-
|| self.exports.module_exists(candidate).finding().is_some())
3481-
.then_some((i, candidate))
3482-
}) else {
3483-
// The target may be resolved dynamically at runtime, so only check paths rooted in a
3484-
// module that is available in the current environment.
3485-
return;
3486-
};
3487-
3488-
let attrs = &parts[module_prefix_len..];
3489-
let mut base_ty = ModuleType::new_as(module).to_type(self.heap);
3490-
for (i, attr) in attrs.iter().enumerate() {
3491-
let name = Name::new(*attr);
3492-
// Patching a public builtin name on a module always succeeds: `mock.patch` sets
3493-
// `create=True` itself in that case, overriding whatever the caller passed. So
3494-
// `patch("mod.open")` is legal even when `mod` defines no `open` of its own.
3495-
// Documented at
3496-
// https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch ("If you
3497-
// are patching builtins in a module then you don't need to pass `create=True`").
3498-
if i + 1 == attrs.len()
3499-
&& base_ty.as_module().is_some()
3500-
&& !attr.starts_with('_')
3501-
&& self.exports.export_exists(ModuleName::builtins(), &name)
3502-
&& !self
3503-
.exports
3504-
.is_implicit_reexport(ModuleName::builtins(), &name)
3505-
{
3506-
return;
3507-
}
3508-
base_ty = self.type_of_attr_get(
3509-
&base_ty,
3510-
&name,
3511-
range,
3512-
errors,
3513-
ErrorKind::MissingAttributePatchTarget,
3514-
None,
3515-
"unittest.mock.patch target",
3516-
);
3517-
}
3518-
}
3519-
35203452
fn call_inspect_unwrap(&self, x: &ExprCall, errors: &ErrorCollector) -> Option<Type> {
35213453
if x.arguments.args.len() != 1 || matches!(x.arguments.args[0], Expr::Starred(_)) {
35223454
return None;
@@ -3591,7 +3523,6 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> {
35913523
result
35923524
})
35933525
}
3594-
35953526
pub fn freeform_call_infer(
35963527
&self,
35973528
ty: Type,

0 commit comments

Comments
 (0)