From b52cff26feba79191cf8c34f4dac8716ea1ef17d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 13 Jun 2023 18:29:18 +0100 Subject: [PATCH 1/3] Fix dmypy run on Windows --- mypy/dmypy/client.py | 3 ++- mypy/dmypy_server.py | 10 ++++++++-- mypy/errorcodes.py | 8 ++++++++ test-data/unit/daemon.test | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/mypy/dmypy/client.py b/mypy/dmypy/client.py index 0e91206085096..6116a9a8cd8f7 100644 --- a/mypy/dmypy/client.py +++ b/mypy/dmypy/client.py @@ -623,8 +623,9 @@ def do_daemon(args: argparse.Namespace) -> None: options = options_obj.apply_changes(options_dict) else: options = process_start_options(args.flags, allow_sources=False) + options_dict = options.snapshot() - Server(options, args.status_file, timeout=args.timeout).serve() + Server(options, args.status_file, timeout=args.timeout, options_snapshot=options_dict).serve() @action(help_parser) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index c742f31164025..d763f7cc9b84b 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -170,11 +170,17 @@ class Server: # NOTE: the instance is constructed in the parent process but # serve() is called in the grandchild (by daemonize()). - def __init__(self, options: Options, status_file: str, timeout: int | None = None) -> None: + def __init__( + self, + options: Options, + status_file: str, + timeout: int | None = None, + options_snapshot: dict[str, object] | None = None, + ) -> None: """Initialize the server with the desired mypy flags.""" self.options = options # Snapshot the options info before we muck with it, to detect changes - self.options_snapshot = options.snapshot() + self.options_snapshot = options_snapshot or options.snapshot() self.timeout = timeout self.fine_grained_manager: FineGrainedBuildManager | None = None diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 50a82be9816d5..e87b04b6f4738 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -37,6 +37,14 @@ def __init__( def __str__(self) -> str: return f"" + def __eq__(self, other: object) -> bool: + if not isinstance(other, ErrorCode): + return False + return self.code == other.code + + def __hash__(self) -> int: + return hash((self.code,)) + ATTR_DEFINED: Final = ErrorCode("attr-defined", "Check that attribute exists", "General") NAME_DEFINED: Final = ErrorCode("name-defined", "Check that name is defined", "General") diff --git a/test-data/unit/daemon.test b/test-data/unit/daemon.test index c60068a44bece..62a354db3aba3 100644 --- a/test-data/unit/daemon.test +++ b/test-data/unit/daemon.test @@ -28,6 +28,24 @@ Daemon stopped [file foo.py] def f(): pass +[case testDaemonRunIgnoreMissingImports] +$ dmypy run -- foo.py --follow-imports=error --ignore-missing-imports +Daemon started +Success: no issues found in 1 source file +$ dmypy stop +Daemon stopped +[file foo.py] +def f(): pass + +[case testDaemonRunErrorCodes] +$ dmypy run -- foo.py --follow-imports=error --disable-error-code=type-abstract +Daemon started +Success: no issues found in 1 source file +$ dmypy stop +Daemon stopped +[file foo.py] +def f(): pass + [case testDaemonIgnoreConfigFiles] $ dmypy start -- --follow-imports=error Daemon started From 632efecd10c9e967b1081aec21112bd6307550d5 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 13 Jun 2023 18:34:55 +0100 Subject: [PATCH 2/3] Add test case with both problematic options --- test-data/unit/daemon.test | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test-data/unit/daemon.test b/test-data/unit/daemon.test index 62a354db3aba3..424fc6d2b167b 100644 --- a/test-data/unit/daemon.test +++ b/test-data/unit/daemon.test @@ -46,6 +46,15 @@ Daemon stopped [file foo.py] def f(): pass +[case testDaemonRunCombinedOptions] +$ dmypy run -- foo.py --follow-imports=error --ignore-missing-imports --disable-error-code=type-abstract +Daemon started +Success: no issues found in 1 source file +$ dmypy stop +Daemon stopped +[file foo.py] +def f(): pass + [case testDaemonIgnoreConfigFiles] $ dmypy start -- --follow-imports=error Daemon started From 58c21c280e7aced3d14e39b093bba4ec070ce5e7 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 14 Jun 2023 11:16:05 +0100 Subject: [PATCH 3/3] Use stable comparison instead --- mypy/dmypy/client.py | 3 +-- mypy/dmypy_server.py | 12 +++--------- mypy/options.py | 14 +++++++++++++- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/mypy/dmypy/client.py b/mypy/dmypy/client.py index 6116a9a8cd8f7..0e91206085096 100644 --- a/mypy/dmypy/client.py +++ b/mypy/dmypy/client.py @@ -623,9 +623,8 @@ def do_daemon(args: argparse.Namespace) -> None: options = options_obj.apply_changes(options_dict) else: options = process_start_options(args.flags, allow_sources=False) - options_dict = options.snapshot() - Server(options, args.status_file, timeout=args.timeout, options_snapshot=options_dict).serve() + Server(options, args.status_file, timeout=args.timeout).serve() @action(help_parser) diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index d763f7cc9b84b..631aab4ccbc82 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -170,17 +170,11 @@ class Server: # NOTE: the instance is constructed in the parent process but # serve() is called in the grandchild (by daemonize()). - def __init__( - self, - options: Options, - status_file: str, - timeout: int | None = None, - options_snapshot: dict[str, object] | None = None, - ) -> None: + def __init__(self, options: Options, status_file: str, timeout: int | None = None) -> None: """Initialize the server with the desired mypy flags.""" self.options = options # Snapshot the options info before we muck with it, to detect changes - self.options_snapshot = options_snapshot or options.snapshot() + self.options_snapshot = options.snapshot() self.timeout = timeout self.fine_grained_manager: FineGrainedBuildManager | None = None @@ -336,7 +330,7 @@ def cmd_run( header=argparse.SUPPRESS, ) # Signal that we need to restart if the options have changed - if self.options_snapshot != options.snapshot(): + if not options.compare_stable(self.options_snapshot): return {"restart": "configuration changed"} if __version__ != version: return {"restart": "mypy version changed"} diff --git a/mypy/options.py b/mypy/options.py index 45591597ba69e..2785d2034c540 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -373,7 +373,7 @@ def use_or_syntax(self) -> bool: def new_semantic_analyzer(self) -> bool: return True - def snapshot(self) -> object: + def snapshot(self) -> dict[str, object]: """Produce a comparable snapshot of this Option""" # Under mypyc, we don't have a __dict__, so we need to do worse things. d = dict(getattr(self, "__dict__", ())) @@ -388,6 +388,7 @@ def __repr__(self) -> str: return f"Options({pprint.pformat(self.snapshot())})" def apply_changes(self, changes: dict[str, object]) -> Options: + # Note: effects of this method *must* be idempotent. new_options = Options() # Under mypyc, we don't have a __dict__, so we need to do worse things. replace_object_state(new_options, self, copy_dict=True) @@ -413,6 +414,17 @@ def apply_changes(self, changes: dict[str, object]) -> Options: return new_options + def compare_stable(self, other_snapshot: dict[str, object]) -> bool: + """Compare options in a way that is stable for snapshot() -> apply_changes() roundtrip. + + This is needed because apply_changes() has non-trivial effects for some flags, so + Options().apply_changes(options.snapshot()) may result in a (slightly) different object. + """ + return ( + Options().apply_changes(self.snapshot()).snapshot() + == Options().apply_changes(other_snapshot).snapshot() + ) + def build_per_module_cache(self) -> None: self._per_module_cache = {}