From e7fcd3c1202343c4f82e2fdb3f54b620efb0816e Mon Sep 17 00:00:00 2001 From: tsudol Date: Wed, 3 Jan 2024 11:22:35 -0800 Subject: [PATCH] Make sure dataclasses.replace actually has args to unpack. PiperOrigin-RevId: 595455184 --- pytype/overlays/dataclass_overlay.py | 12 ++++++++++++ pytype/tests/test_dataclasses.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/pytype/overlays/dataclass_overlay.py b/pytype/overlays/dataclass_overlay.py index 8d45f2ad0..e046d99e2 100644 --- a/pytype/overlays/dataclass_overlay.py +++ b/pytype/overlays/dataclass_overlay.py @@ -234,6 +234,18 @@ def _match_args_sequentially(self, node, args, alias_map, match_all_views): ret = super()._match_args_sequentially( node, args, alias_map, match_all_views ) + if not args.posargs: + # This is a weird case where pytype thinks the call can succeed, but + # there's no concrete `__obj` in the posargs. + # This can happen when `dataclasses.replace` is called with **kwargs: + # @dataclasses.dataclass + # class A: + # replace = dataclasses.replace + # def do(self, **kwargs): + # return self.replace(**kwargs) + # (Yes, this is a simplified example of real code.) + # Since **kwargs is opaque magic, we can't do more type checking. + return ret # _match_args_sequentially has succeeded, so we know we have 1 posarg (the # object) and some number of named args (the new fields). (obj,) = args.posargs diff --git a/pytype/tests/test_dataclasses.py b/pytype/tests/test_dataclasses.py index 30120c797..f5d6e847d 100644 --- a/pytype/tests/test_dataclasses.py +++ b/pytype/tests/test_dataclasses.py @@ -1333,5 +1333,20 @@ class Z: name: str """) + def test_replace_as_method_with_kwargs(self): + # This is a weird case where replace is added as a method, then called + # with kwargs. This makes pytype unable to see that `self` is the object + # being modified, and also caused a crash when the dataclass overlay tries + # to unpack the object being modified from the args. + self.Check(""" + import dataclasses + @dataclasses.dataclass + class WithKwargs: + replace = dataclasses.replace + def do(self, **kwargs): + return self.replace(**kwargs) + """) + + if __name__ == "__main__": test_base.main()