Skip to content

Commit 4403d19

Browse files
committed
Improve mypy typing
1 parent 2a05ba1 commit 4403d19

File tree

5 files changed

+63
-47
lines changed

5 files changed

+63
-47
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ test-integration:
2424
python3 ./tests/integration_tests.py --no-modules
2525

2626
typecheck:
27-
mypy aw_qt --ignore-missing-imports
27+
mypy aw_qt --strict
2828

2929
precommit:
3030
make typecheck

aw_qt/main.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,41 @@
11
import sys
22
import logging
33
import argparse
4+
from typing import Set
5+
from typing_extensions import TypedDict
46

57
from aw_core.log import setup_logging
68

79
from .manager import Manager
8-
910
from . import trayicon
1011

1112
logger = logging.getLogger(__name__)
1213

1314

14-
def main():
15+
def main() -> None:
1516
args = parse_args()
16-
setup_logging("aw-qt", testing=args.testing, verbose=args.testing, log_file=True)
17+
setup_logging("aw-qt", testing=args['testing'], verbose=args['testing'], log_file=True)
1718

18-
_manager = Manager(testing=args.testing)
19-
_manager.autostart(args.autostart_modules)
19+
_manager = Manager(testing=args['testing'])
20+
_manager.autostart(args['autostart_modules'])
2021

21-
error_code = trayicon.run(_manager, testing=args.testing)
22+
error_code = trayicon.run(_manager, testing=args['testing'])
2223
_manager.stop_all()
2324

2425
sys.exit(error_code)
2526

2627

27-
def parse_args():
28+
CommandLineArgs = TypedDict('CommandLineArgs', {'testing': bool, 'autostart_modules': Set[str]}, total=False)
29+
30+
31+
def parse_args() -> CommandLineArgs:
2832
parser = argparse.ArgumentParser(prog="aw-qt", description='A trayicon and service manager for ActivityWatch')
2933
parser.add_argument('--testing', action='store_true',
3034
help='Run the trayicon and services in testing mode')
3135
parser.add_argument('--autostart-modules', dest='autostart_modules',
3236
type=lambda s: [m for m in s.split(',') if m and m.lower() != "none"],
3337
default=None,
3438
help='A comma-separated list of modules to autostart, or just `none` to not autostart anything')
35-
36-
return parser.parse_args()
39+
parsed_args = parser.parse_args()
40+
dict: CommandLineArgs = {'autostart_modules': parsed_args.autostart_modules, 'testing': parsed_args.testing}
41+
return dict

aw_qt/manager.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def _locate_bundled_executable(name: str) -> Optional[str]:
3030
return None
3131

3232

33-
def _is_system_module(name) -> bool:
33+
def _is_system_module(name: str) -> bool:
3434
"""Checks if a module with a particular name exists in PATH"""
3535
return shutil.which(name) is not None
3636

@@ -88,20 +88,20 @@ def __init__(self, name: str, testing: bool = False) -> None:
8888
self.started = False # Should be True if module is supposed to be running, else False
8989
self.testing = testing
9090
self.location = "system" if _is_system_module(name) else "bundled"
91-
self._process = None # type: Optional[subprocess.Popen]
92-
self._last_process = None # type: Optional[subprocess.Popen]
91+
self._process: Optional[subprocess.Popen[str]] = None
92+
self._last_process: Optional[subprocess.Popen[str]] = None
9393

9494
def start(self) -> None:
9595
logger.info("Starting module {}".format(self.name))
9696

9797
# Create a process group, become its leader
9898
# TODO: This shouldn't go here
9999
if platform.system() != "Windows":
100-
os.setpgrp() # type: ignore
100+
os.setpgrp()
101101

102102
exec_path = _locate_executable(self.name)
103103
if exec_path is None:
104-
return
104+
logger.error("Tried to start nonexistent module {}".format(self.name))
105105
else:
106106
exec_cmd = [exec_path]
107107
if self.testing:
@@ -112,8 +112,8 @@ def start(self) -> None:
112112
# See: https://github.com/ActivityWatch/activitywatch/issues/212
113113
startupinfo = None
114114
if platform.system() == "Windows":
115-
startupinfo = subprocess.STARTUPINFO() # type: ignore
116-
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
115+
startupinfo = subprocess.STARTUPINFO() #type: ignore
116+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW #type: ignore
117117
elif platform.system() == "Darwin":
118118
logger.info("Macos: Disable dock icon")
119119
import AppKit
@@ -122,7 +122,6 @@ def start(self) -> None:
122122
# There is a very good reason stdout and stderr is not PIPE here
123123
# See: https://github.com/ActivityWatch/aw-server/issues/27
124124
self._process = subprocess.Popen(exec_cmd, universal_newlines=True, startupinfo=startupinfo)
125-
126125
self.started = True
127126

128127
def stop(self) -> None:
@@ -137,7 +136,7 @@ def stop(self) -> None:
137136
logger.warning("Tried to stop module {}, but it wasn't running".format(self.name))
138137
else:
139138
if not self._process:
140-
logger.error("No reference to process object")
139+
logger.error("")
141140
logger.debug("Stopping module {}".format(self.name))
142141
if self._process:
143142
self._process.terminate()
@@ -180,6 +179,7 @@ def __init__(self, testing: bool = False) -> None:
180179
self.settings: AwQtSettings = AwQtSettings(testing)
181180
self.modules: Dict[str, Module] = {}
182181
self.autostart_modules: Set[str] = set(self.settings.autostart_modules)
182+
self.testing = testing
183183

184184
for name in self.settings.possible_modules:
185185
if _locate_executable(name):
@@ -189,7 +189,7 @@ def __init__(self, testing: bool = False) -> None:
189189
# Is this actually a good way to do this? merged from dev/autodetect-modules
190190
self.discover_modules()
191191

192-
def discover_modules(self):
192+
def discover_modules(self) -> None:
193193
# These should always be bundled with aw-qt
194194
found_modules = set(_discover_modules_bundled())
195195
found_modules |= set(_discover_modules_system())
@@ -199,17 +199,19 @@ def discover_modules(self):
199199
if m_name not in self.modules:
200200
self.modules[m_name] = Module(m_name, testing=self.testing)
201201

202-
def get_unexpected_stops(self):
202+
def get_unexpected_stops(self) -> List[Module]:
203203
return list(filter(lambda x: x.started and not x.is_alive(), self.modules.values()))
204204

205-
def start(self, module_name):
205+
def start(self, module_name: str) -> None:
206206
if module_name in self.modules.keys():
207207
self.modules[module_name].start()
208+
else:
209+
logger.debug("Manager tried to start nonexistent module {}".format(module_name))
208210

209-
def autostart(self, autostart_modules):
210-
if autostart_modules is None:
211+
def autostart(self, autostart_modules: Set[str] = set()) -> None:
212+
if autostart_modules and len(autostart_modules) > 0:
211213
logger.info("Modules to start weren't specified in CLI arguments. Falling back to configuration.")
212-
autostart_modules = self.settings.autostart_modules
214+
autostart_modules = set(self.settings.autostart_modules)
213215

214216
# We only want to autostart modules that are both in found modules and are asked to autostart.
215217
autostart_modules = autostart_modules.intersection(set(self.modules.keys()))
@@ -223,7 +225,7 @@ def autostart(self, autostart_modules):
223225
for module_name in autostart_modules:
224226
self.start(module_name)
225227

226-
def stop_all(self):
228+
def stop_all(self) -> None:
227229
for module in filter(lambda m: m.is_alive(), self.modules.values()):
228230
module.stop()
229231

aw_qt/trayicon.py

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,30 @@
55
import os
66
import subprocess
77
from collections import defaultdict
8+
from typing import Any, DefaultDict, List, Optional
89

910
from PyQt5 import QtCore
1011
from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMessageBox, QMenu, QWidget, QPushButton
1112
from PyQt5.QtGui import QIcon
1213

1314
import aw_core
1415

15-
from .manager import Manager
16+
from .manager import Manager, Module
1617

1718
logger = logging.getLogger(__name__)
1819

1920

20-
def open_webui(root_url):
21+
def open_webui(root_url: str) -> None:
2122
print("Opening dashboard")
2223
webbrowser.open(root_url)
2324

2425

25-
def open_apibrowser(root_url):
26+
def open_apibrowser(root_url: str) -> None:
2627
print("Opening api browser")
2728
webbrowser.open(root_url + "/api")
2829

2930

30-
def open_dir(d):
31+
def open_dir(d: str)-> None:
3132
"""From: http://stackoverflow.com/a/1795849/965332"""
3233
if sys.platform == 'win32':
3334
os.startfile(d)
@@ -38,17 +39,18 @@ def open_dir(d):
3839

3940

4041
class TrayIcon(QSystemTrayIcon):
41-
def __init__(self, manager: Manager, icon, parent=None, testing=False) -> None:
42+
def __init__(self, manager: Manager, icon: QIcon, parent: Optional[QWidget]=None, testing: bool=False) -> None:
4243
QSystemTrayIcon.__init__(self, icon, parent)
44+
self._parent = parent # QSystemTrayIcon also tries to save parent info but it screws up the type info
4345
self.setToolTip("ActivityWatch" + (" (testing)" if testing else ""))
4446

4547
self.manager = manager
4648
self.testing = testing
4749

4850
self._build_rootmenu()
4951

50-
def _build_rootmenu(self):
51-
menu = QMenu(self.parent())
52+
def _build_rootmenu(self) -> None:
53+
menu = QMenu(self._parent)
5254

5355
root_url = "http://localhost:{port}".format(port=5666 if self.testing else 5600)
5456

@@ -80,8 +82,8 @@ def _build_rootmenu(self):
8082

8183
self.setContextMenu(menu)
8284

83-
def show_module_failed_dialog(module):
84-
box = QMessageBox(self.parent())
85+
def show_module_failed_dialog(module: Module) -> None:
86+
box = QMessageBox(self._parent)
8587
box.setIcon(QMessageBox.Warning)
8688
box.setText("Module {} quit unexpectedly".format(module.name))
8789
box.setDetailedText(module.read_log())
@@ -93,10 +95,10 @@ def show_module_failed_dialog(module):
9395

9496
box.show()
9597

96-
def rebuild_modules_menu():
98+
def rebuild_modules_menu() -> None:
9799
for action in modulesMenu.actions():
98100
if action.isEnabled():
99-
name = action.module.name
101+
name = action.data().name
100102
alive = self.manager.modules[name].is_alive()
101103
action.setChecked(alive)
102104
# print(module.text(), alive)
@@ -105,7 +107,7 @@ def rebuild_modules_menu():
105107
QtCore.QTimer.singleShot(2000, rebuild_modules_menu)
106108
QtCore.QTimer.singleShot(2000, rebuild_modules_menu)
107109

108-
def check_module_status():
110+
def check_module_status() -> None:
109111
unexpected_exits = self.manager.get_unexpected_stops()
110112
if unexpected_exits:
111113
for module in unexpected_exits:
@@ -116,19 +118,19 @@ def check_module_status():
116118
QtCore.QTimer.singleShot(2000, rebuild_modules_menu)
117119
QtCore.QTimer.singleShot(2000, check_module_status)
118120

119-
def _build_modulemenu(self, moduleMenu):
121+
def _build_modulemenu(self, moduleMenu: QMenu) -> None:
120122
moduleMenu.clear()
121123

122-
def add_module_menuitem(module):
124+
def add_module_menuitem(module: Module) -> None:
123125
title = module.name
124126
ac = moduleMenu.addAction(title, lambda: module.toggle())
125-
# Kind of nasty, but a quick way to affiliate the module to the menu action for when it needs updating
126-
ac.module = module
127+
128+
ac.setData(module)
127129
ac.setCheckable(True)
128130
ac.setChecked(module.is_alive())
129131

130132
# Merged from branch dev/autodetect-modules, still kind of in progress with making this actually work
131-
modules_by_location = defaultdict(lambda: list())
133+
modules_by_location: DefaultDict[str, List[Module]] = defaultdict(lambda: list())
132134
for module in sorted(self.manager.modules.values(), key=lambda m: m.name):
133135
modules_by_location[module.location].append(module)
134136

@@ -140,7 +142,7 @@ def add_module_menuitem(module):
140142
add_module_menuitem(self.manager.modules[module.name])
141143

142144

143-
def exit(manager: Manager):
145+
def exit(manager: Manager) -> None:
144146
# TODO: Do cleanup actions
145147
# TODO: Save state for resume
146148
print("Shutdown initiated, stopping all services...")
@@ -151,16 +153,17 @@ def exit(manager: Manager):
151153
QApplication.quit()
152154

153155

154-
def run(manager, testing=False):
156+
def run(manager: Manager, testing: bool = False) -> Any:
155157
logger.info("Creating trayicon...")
156158
# print(QIcon.themeSearchPaths())
157159

158160
app = QApplication(sys.argv)
159161

162+
# FIXME: remove ignores after https://github.com/python/mypy/issues/2955 has been fixed
160163
# Without this, Ctrl+C will have no effect
161-
signal.signal(signal.SIGINT, lambda: exit(manager))
164+
signal.signal(signal.SIGINT, lambda: exit(manager)) #type: ignore
162165
# Ensure cleanup happens on SIGTERM
163-
signal.signal(signal.SIGTERM, lambda: exit(manager))
166+
signal.signal(signal.SIGTERM, lambda: exit(manager)) #type: ignore
164167

165168
timer = QtCore.QTimer()
166169
timer.start(100) # You may change this if you wish.

mypy.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[mypy]
2+
python_version = 3.6
3+
ignore_missing_imports = True
4+
5+
[mypy-*.resources]
6+
ignore_errors = True

0 commit comments

Comments
 (0)