From 53f7dd0ffc4aa4481eeb2ed9701b1383afc03f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 30 Aug 2017 20:44:59 +0200 Subject: [PATCH 01/25] added basic module autodetection --- aw_qt/manager.py | 128 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 37 deletions(-) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index b8f3e68..300e4f6 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -1,37 +1,80 @@ import os import platform +from glob import glob from time import sleep import logging import subprocess +import shutil from typing import Optional, List import aw_core logger = logging.getLogger(__name__) +_module_dir = os.path.dirname(os.path.realpath(__file__)) +_parent_dir = os.path.abspath(os.path.join(_module_dir, os.pardir)) +_search_paths = [_module_dir, _parent_dir] -def _locate_executable(name: str) -> List[str]: - """ - Will start module from localdir if present there, - otherwise will try to call what is available in PATH. - Returns it as a Popen cmd list. - """ - curr_filepath = os.path.realpath(__file__) - curr_dir = os.path.dirname(curr_filepath) - search_paths = [curr_dir, os.path.abspath(os.path.join(curr_dir, os.pardir))] - exec_paths = [os.path.join(path, name) for path in search_paths] +def _locate_bundled_executable(name: str) -> Optional[str]: + """Returns the path to the module executable if it exists in the bundle, else None.""" + _exec_paths = [os.path.join(path, name) for path in _search_paths] - for exec_path in exec_paths: + # Look for it in the installation path + for exec_path in _exec_paths: if os.path.isfile(exec_path): # logger.debug("Found executable for {} in: {}".format(name, exec_path)) - return [exec_path] - break # this break is redundant, but kept due to for-else semantics + return exec_path + + +def _is_system_module(name) -> bool: + """Checks if a module with a particular name exists in PATH""" + return shutil.which(name) is not None + + +def _locate_executable(name: str) -> Optional[str]: + """ + Will return the path to the executable if bundled, + otherwise returns the name if it is available in PATH. + + Used when calling Popen. + """ + exec_path = _locate_bundled_executable(name) + if exec_path is not None: # Check if it exists in bundle + return exec_path + elif _is_system_module(name): # Check if it's in PATH + return name else: - # TODO: Actually check if it is in PATH - # logger.debug("Trying to start {} using PATH (executable not found in: {})" - # .format(name, exec_paths)) - return [name] + logger.warning("Could not find module '{}' in installation directory or PATH".format(name)) + return None + + +def _discover_modules_bundled() -> List[str]: + # Look for modules in source dir and parent dir + modules = [] + for path in _search_paths: + matches = glob(os.path.join(path, "aw-*")) + for match in matches: + if os.path.isfile(match) and os.access(match, os.X_OK): + modules.append(match) + else: + logger.warning("Found matching file but was not executable: {}".format(path)) + + logger.info("Found bundled modules: {}".format(set(modules))) + return modules + + +def _discover_modules_system() -> List[str]: + search_paths = os.environ["PATH"].split(":") + modules = [] + for path in search_paths: + files = os.listdir(path) + for filename in files: + if "aw-" in filename: + modules.append(filename) + + logger.info("Found system modules: {}".format(set(modules))) + return modules class Module: @@ -46,20 +89,25 @@ def start(self) -> None: logger.info("Starting module {}".format(self.name)) # Create a process group, become its leader + # TODO: This shouldn't go here if platform.system() != "Windows": os.setpgrp() - exec_cmd = _locate_executable(self.name) - if self.testing: - exec_cmd.append("--testing") - # logger.debug("Running: {}".format(exec_cmd)) + exec_path = _locate_executable(self.name) + if exec_path is None: + return + else: + exec_cmd = [exec_path] + if self.testing: + exec_cmd.append("--testing") + # logger.debug("Running: {}".format(exec_cmd)) - # There is a very good reason stdout and stderr is not PIPE here - # See: https://github.com/ActivityWatch/aw-server/issues/27 - self._process = subprocess.Popen(exec_cmd, universal_newlines=True) + # There is a very good reason stdout and stderr is not PIPE here + # See: https://github.com/ActivityWatch/aw-server/issues/27 + self._process = subprocess.Popen(exec_cmd, universal_newlines=True) - # Should be True if module is supposed to be running, else False - self.started = True + # Should be True if module is supposed to be running, else False + self.started = True def stop(self) -> None: """ @@ -108,19 +156,25 @@ def read_log(self) -> str: class Manager: - def __init__(self, testing: bool=False): - # TODO: Fetch these from somewhere appropriate (auto detect or a config file) - # Save to config wether they should autostart or not. - _possible_modules = [ + def __init__(self, testing: bool = False) -> None: + self.testing = testing + self.modules = {} # type: Dict[str, Module] + + self.discover_modules() + + def discover_modules(self): + # These should always be bundled with aw-qt + found_modules = { "aw-server", "aw-watcher-afk", - "aw-watcher-window", - # "aw-watcher-spotify", - # "aw-watcher-network" - ] - - # TODO: Filter away all modules not available on system - self.modules = {name: Module(name, testing=testing) for name in _possible_modules} + "aw-watcher-window" + } + found_modules |= set(_discover_modules_bundled()) + found_modules |= set(_discover_modules_system()) + + for m_name in found_modules: + if m_name not in self.modules: + self.modules[m_name] = Module(m_name, testing=self.testing) def get_unexpected_stops(self): return list(filter(lambda x: x.started and not x.is_alive(), self.modules.values())) From fbe35198428f17aac7817bef2f2497b285c9b4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 30 Aug 2017 20:50:33 +0200 Subject: [PATCH 02/25] fixed bundled modules --- aw_qt/manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 300e4f6..bcd5b4b 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -56,7 +56,8 @@ def _discover_modules_bundled() -> List[str]: matches = glob(os.path.join(path, "aw-*")) for match in matches: if os.path.isfile(match) and os.access(match, os.X_OK): - modules.append(match) + name = os.path.basename(match) + modules.append(name) else: logger.warning("Found matching file but was not executable: {}".format(path)) @@ -171,6 +172,7 @@ def discover_modules(self): } found_modules |= set(_discover_modules_bundled()) found_modules |= set(_discover_modules_system()) + found_modules ^= {"aw-qt"} # Exclude self for m_name in found_modules: if m_name not in self.modules: From ba88995c1c5d21a3a4b176fee3cf7dd245cedd0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 30 Aug 2017 20:55:21 +0200 Subject: [PATCH 03/25] fixed crash when module couldn't be started --- aw_qt/manager.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index bcd5b4b..5764c3b 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -81,7 +81,7 @@ def _discover_modules_system() -> List[str]: class Module: def __init__(self, name: str, testing: bool = False) -> None: self.name = name - self.started = False + self.started = False # Should be True if module is supposed to be running, else False self.testing = testing self._process = None # type: Optional[subprocess.Popen] self._last_process = None # type: Optional[subprocess.Popen] @@ -105,9 +105,11 @@ def start(self) -> None: # There is a very good reason stdout and stderr is not PIPE here # See: https://github.com/ActivityWatch/aw-server/issues/27 - self._process = subprocess.Popen(exec_cmd, universal_newlines=True) + try: + self._process = subprocess.Popen(exec_cmd, universal_newlines=True) + except OSError as e: + logger.error("Couldn't start module with command {} ({})".format(exec_cmd, e)) - # Should be True if module is supposed to be running, else False self.started = True def stop(self) -> None: From 4078647c457ab26248a4da157aa0c10a37b7fb73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 31 Aug 2017 16:45:56 +0200 Subject: [PATCH 04/25] fixed issue when location in PATH does not exist --- aw_qt/manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 5764c3b..211a2bc 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -69,10 +69,11 @@ def _discover_modules_system() -> List[str]: search_paths = os.environ["PATH"].split(":") modules = [] for path in search_paths: - files = os.listdir(path) - for filename in files: - if "aw-" in filename: - modules.append(filename) + if os.path.isdir(path): + files = os.listdir(path) + for filename in files: + if "aw-" in filename: + modules.append(filename) logger.info("Found system modules: {}".format(set(modules))) return modules From 70cca98061bf542f3bd4380ba1f417bfbe9131dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Fri, 1 Sep 2017 12:31:59 +0200 Subject: [PATCH 05/25] categorized module menu by location of module --- aw_qt/manager.py | 8 ++------ aw_qt/trayicon.py | 43 +++++++++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 211a2bc..94aa209 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -84,6 +84,7 @@ def __init__(self, name: str, testing: bool = False) -> None: self.name = name self.started = False # Should be True if module is supposed to be running, else False self.testing = testing + self.location = "system" if _is_system_module(name) else "bundled" self._process = None # type: Optional[subprocess.Popen] self._last_process = None # type: Optional[subprocess.Popen] @@ -168,12 +169,7 @@ def __init__(self, testing: bool = False) -> None: def discover_modules(self): # These should always be bundled with aw-qt - found_modules = { - "aw-server", - "aw-watcher-afk", - "aw-watcher-window" - } - found_modules |= set(_discover_modules_bundled()) + found_modules = set(_discover_modules_bundled()) found_modules |= set(_discover_modules_system()) found_modules ^= {"aw-qt"} # Exclude self diff --git a/aw_qt/trayicon.py b/aw_qt/trayicon.py index 5938ac3..5024977 100644 --- a/aw_qt/trayicon.py +++ b/aw_qt/trayicon.py @@ -4,6 +4,7 @@ import webbrowser import os import subprocess +from collections import defaultdict from PyQt5 import QtCore from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMessageBox, QMenu, QWidget, QPushButton @@ -93,12 +94,18 @@ def show_module_failed_dialog(module): box.show() def rebuild_modules_menu(): - for module in modulesMenu.actions(): - name = module.text() - alive = self.manager.modules[name].is_alive() - module.setChecked(alive) - # print(module.text(), alive) + for action in modulesMenu.actions(): + if action.isEnabled(): + name = action.module.name + alive = self.manager.modules[name].is_alive() + action.setChecked(alive) + # print(module.text(), alive) + # TODO: Do it in a better way, singleShot isn't pretty... + QtCore.QTimer.singleShot(2000, rebuild_modules_menu) + QtCore.QTimer.singleShot(2000, rebuild_modules_menu) + + def check_module_status(): unexpected_exits = self.manager.get_unexpected_stops() if unexpected_exits: for module in unexpected_exits: @@ -107,22 +114,34 @@ def rebuild_modules_menu(): # TODO: Do it in a better way, singleShot isn't pretty... QtCore.QTimer.singleShot(2000, rebuild_modules_menu) - - QtCore.QTimer.singleShot(2000, rebuild_modules_menu) + QtCore.QTimer.singleShot(2000, check_module_status) def _build_modulemenu(self, moduleMenu): moduleMenu.clear() def add_module_menuitem(module): - ac = moduleMenu.addAction(module.name, lambda: module.toggle()) + title = module.name + ac = moduleMenu.addAction(title, lambda: module.toggle()) + # Kind of nasty, but a quick way to affiliate the module to the menu action for when it needs updating + ac.module = module ac.setCheckable(True) ac.setChecked(module.is_alive()) - add_module_menuitem(self.manager.modules["aw-server"]) + modules_by_location = defaultdict(lambda: list()) + for module in sorted(self.manager.modules.values(), key=lambda m: m.name): + modules_by_location[module.location].append(module) + + for location, modules in sorted(modules_by_location.items(), key=lambda kv: kv[0]): + header = moduleMenu.addAction(location) + header.setEnabled(False) + + # Always show aw-server first + if "aw-server" in (m.name for m in modules): + add_module_menuitem(self.manager.modules["aw-server"]) - for module_name in sorted(self.manager.modules.keys()): - if module_name != "aw-server": - add_module_menuitem(self.manager.modules[module_name]) + for module in sorted(modules, key=lambda m: m.name): + if module.name != "aw-server": + add_module_menuitem(self.manager.modules[module.name]) def exit_dialog(): From 6db5435f78368ff8c34a18e15aeca5d6dd08b49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Fri, 1 Sep 2017 12:49:07 +0200 Subject: [PATCH 06/25] fixed typechecking and enabled on Travis --- .travis.yml | 3 ++- aw_qt/manager.py | 3 ++- aw_qt/trayicon.py | 6 +----- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 66c3e40..4918751 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,12 +27,13 @@ before_install: - python -V - pip -V - pip3 install --upgrade pip - - pip3 install pyinstaller + - pip3 install mypy pyinstaller install: - make build script: + - make typecheck - make test - make test-integration - make package diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 94aa209..dda13cc 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -5,7 +5,7 @@ import logging import subprocess import shutil -from typing import Optional, List +from typing import Optional, List, Dict import aw_core @@ -25,6 +25,7 @@ def _locate_bundled_executable(name: str) -> Optional[str]: if os.path.isfile(exec_path): # logger.debug("Found executable for {} in: {}".format(name, exec_path)) return exec_path + return None def _is_system_module(name) -> bool: diff --git a/aw_qt/trayicon.py b/aw_qt/trayicon.py index 5024977..d5525c1 100644 --- a/aw_qt/trayicon.py +++ b/aw_qt/trayicon.py @@ -38,7 +38,7 @@ def open_dir(d): class TrayIcon(QSystemTrayIcon): - def __init__(self, manager: Manager, icon, parent=None, testing=False): + def __init__(self, manager: Manager, icon, parent=None, testing=False) -> None: QSystemTrayIcon.__init__(self, icon, parent) self.setToolTip("ActivityWatch" + (" (testing)" if testing else "")) @@ -196,7 +196,3 @@ def run(manager, testing=False): # Run the application, blocks until quit return app.exec_() - - -if __name__ == "__main__": - run() From b7ed55ab69b26485eff56c84591249de4b118851 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Mon, 2 Mar 2020 13:27:25 +0200 Subject: [PATCH 07/25] actually check for module existence in manager --- aw_qt/manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 5630621..b8346f5 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -138,8 +138,10 @@ def __init__(self, testing: bool=False) -> None: # "aw-watcher-network" ] - # TODO: Filter away all modules not available on system - self.modules = {name: Module(name, testing=testing) for name in _possible_modules} + self.modules = [] + for name in _possible_modules: + if not _locate_executable(name): + self.modules.append(Module(name, testing=testing)) def get_unexpected_stops(self): return list(filter(lambda x: x.started and not x.is_alive(), self.modules.values())) From a974e2c908cf2f7fe708f058496b5b281d3f271d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Kro=C4=8D=C3=A1k?= Date: Mon, 4 Nov 2019 21:54:51 +0100 Subject: [PATCH 08/25] Store available and autostart modules lists as configuration. See #47 NOTE: `ConfigParser` does not correctly store array values. Store module lists as json. --- aw_qt/config.py | 36 ++++++++++++++++++++++++++++++++++++ aw_qt/main.py | 2 +- aw_qt/manager.py | 31 +++++++++++++++---------------- 3 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 aw_qt/config.py diff --git a/aw_qt/config.py b/aw_qt/config.py new file mode 100644 index 0000000..1d54bec --- /dev/null +++ b/aw_qt/config.py @@ -0,0 +1,36 @@ +from configparser import ConfigParser + +from aw_core.config import load_config +import json + +default_settings = { + "possible_modules": json.dumps(["aw-server", + "aw-watcher-afk", + "aw-watcher-window", ]), + "autostart_modules": json.dumps(["aw-server", + "aw-watcher-afk", + "aw-watcher-window", ]), +} +default_testing_settings = { + "possible_modules": json.dumps(["aw-server", + "aw-watcher-afk", + "aw-watcher-window", ]), + "autostart_modules": json.dumps(["aw-server", + "aw-watcher-afk", + "aw-watcher-window", ]), +} + +default_config = ConfigParser() +default_config['aw-qt'] = default_settings +default_config['aw-qt-testing'] = default_testing_settings +qt_config = load_config("aw-qt", default_config) + + +class QTSettings: + def __init__(self, testing: bool): + config_section = qt_config["aw-qt" if not testing else "aw-qt-testing"] + + # TODO: Resolved available modules automatically. + # TODO: Filter away all modules not available on system + self.possible_modules = json.loads(config_section["possible_modules"]) + self.autostart_modules = json.loads(config_section["autostart_modules"]) diff --git a/aw_qt/main.py b/aw_qt/main.py index aafc135..366a6c5 100644 --- a/aw_qt/main.py +++ b/aw_qt/main.py @@ -30,7 +30,7 @@ def parse_args(): help='Run the trayicon and services in testing mode') parser.add_argument('--autostart-modules', dest='autostart_modules', type=lambda s: [m for m in s.split(',') if m and m.lower() != "none"], - default=["aw-server", "aw-watcher-afk", "aw-watcher-window"], + default=None, help='A comma-separated list of modules to autostart, or just `none` to not autostart anything') return parser.parse_args() diff --git a/aw_qt/manager.py b/aw_qt/manager.py index b8346f5..078f937 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -7,6 +7,8 @@ import aw_core +from .config import QTSettings + logger = logging.getLogger(__name__) @@ -126,22 +128,14 @@ def read_log(self) -> str: class Manager: - def __init__(self, testing: bool=False) -> None: - # TODO: Fetch these from somewhere appropriate (auto detect or a config file) - # Save to config wether they should autostart or not. - _possible_modules = [ - "aw-server", - "aw-server-rust", - "aw-watcher-afk", - "aw-watcher-window", - # "aw-watcher-spotify", - # "aw-watcher-network" - ] - - self.modules = [] - for name in _possible_modules: - if not _locate_executable(name): - self.modules.append(Module(name, testing=testing)) + def __init__(self, testing: bool = False) -> None: + self.settings = QTSettings(testing) + self.modules = {} + for name in self.settings.possible_modules: + if _locate_executable(name): + self.modules[name] = Module(name, testing=testing) + else: + print("Module '{}' not found".format(name)) def get_unexpected_stops(self): return list(filter(lambda x: x.started and not x.is_alive(), self.modules.values())) @@ -153,6 +147,11 @@ def start(self, module_name): logger.error("Unable to start module '{}': No such module".format(module_name)) def autostart(self, autostart_modules): + + if autostart_modules is None: + # Modules to start are not specified. Fallback on configuration. + autostart_modules = self.settings.autostart_modules + # Always start aw-server first if "aw-server" in autostart_modules: self.start("aw-server") From 9dfea4b33e149e2e6a0e432bf937fe6566b3c6f6 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Mon, 2 Mar 2020 16:54:14 +0200 Subject: [PATCH 09/25] rename QTSettings to AwQtSettings and add aw-server-rust --- aw_qt/config.py | 19 ++++++------------- aw_qt/manager.py | 14 ++++++-------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/aw_qt/config.py b/aw_qt/config.py index 1d54bec..addb002 100644 --- a/aw_qt/config.py +++ b/aw_qt/config.py @@ -5,32 +5,25 @@ default_settings = { "possible_modules": json.dumps(["aw-server", + "aw-server-rust", "aw-watcher-afk", "aw-watcher-window", ]), - "autostart_modules": json.dumps(["aw-server", - "aw-watcher-afk", - "aw-watcher-window", ]), -} -default_testing_settings = { - "possible_modules": json.dumps(["aw-server", - "aw-watcher-afk", - "aw-watcher-window", ]), - "autostart_modules": json.dumps(["aw-server", + "autostart_modules": json.dumps(["aw-server-rust", + "aw-server", "aw-watcher-afk", "aw-watcher-window", ]), } default_config = ConfigParser() default_config['aw-qt'] = default_settings -default_config['aw-qt-testing'] = default_testing_settings +# Currently there's no reason to make them differ +default_config['aw-qt-testing'] = default_settings qt_config = load_config("aw-qt", default_config) -class QTSettings: +class AwQtSettings: def __init__(self, testing: bool): config_section = qt_config["aw-qt" if not testing else "aw-qt-testing"] - # TODO: Resolved available modules automatically. - # TODO: Filter away all modules not available on system self.possible_modules = json.loads(config_section["possible_modules"]) self.autostart_modules = json.loads(config_section["autostart_modules"]) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 078f937..1902a53 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -7,7 +7,7 @@ import aw_core -from .config import QTSettings +from .config import AwQtSettings logger = logging.getLogger(__name__) @@ -129,22 +129,20 @@ def read_log(self) -> str: class Manager: def __init__(self, testing: bool = False) -> None: - self.settings = QTSettings(testing) + self.settings = AwQtSettings(testing) self.modules = {} for name in self.settings.possible_modules: if _locate_executable(name): self.modules[name] = Module(name, testing=testing) else: - print("Module '{}' not found".format(name)) + logger.warning("Module '{}' not found".format(name)) def get_unexpected_stops(self): - return list(filter(lambda x: x.started and not x.is_alive(), self.modules.values())) + return list(filter(lambda x: x.started and not x.is_alive(), self.modules)) def start(self, module_name): - if module_name in self.modules.keys(): + if module_name in self.modules: self.modules[module_name].start() - else: - logger.error("Unable to start module '{}': No such module".format(module_name)) def autostart(self, autostart_modules): @@ -163,7 +161,7 @@ def autostart(self, autostart_modules): self.start(module_name) def stop_all(self): - for module in filter(lambda m: m.is_alive(), self.modules.values()): + for module in filter(lambda m: m.is_alive(), self.modules): module.stop() From 94dc1dfac74ddd5772173bcb9fd9584ccdae3bd6 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Tue, 3 Mar 2020 21:07:55 +0200 Subject: [PATCH 10/25] Add type hints to new code --- aw_qt/config.py | 5 +++-- aw_qt/manager.py | 21 ++++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/aw_qt/config.py b/aw_qt/config.py index addb002..e6a0613 100644 --- a/aw_qt/config.py +++ b/aw_qt/config.py @@ -1,4 +1,5 @@ from configparser import ConfigParser +from typing import ClassVar, List from aw_core.config import load_config import json @@ -25,5 +26,5 @@ class AwQtSettings: def __init__(self, testing: bool): config_section = qt_config["aw-qt" if not testing else "aw-qt-testing"] - self.possible_modules = json.loads(config_section["possible_modules"]) - self.autostart_modules = json.loads(config_section["autostart_modules"]) + self.possible_modules: List[str] = json.loads(config_section["possible_modules"]) + self.autostart_modules: List[str] = json.loads(config_section["autostart_modules"]) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 1902a53..4226555 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -3,7 +3,7 @@ from time import sleep import logging import subprocess -from typing import Optional, List +from typing import Optional, List, Dict, Set import aw_core @@ -129,27 +129,30 @@ def read_log(self) -> str: class Manager: def __init__(self, testing: bool = False) -> None: - self.settings = AwQtSettings(testing) - self.modules = {} + self.settings: AwQtSettings = AwQtSettings(testing) + self.modules: Dict[str, Module] = {} + self.autostart_modules: Set[str] = set(self.settings.autostart_modules) + for name in self.settings.possible_modules: if _locate_executable(name): self.modules[name] = Module(name, testing=testing) else: - logger.warning("Module '{}' not found".format(name)) + logger.warning("Module '{}' not found but was in possible modules".format(name)) def get_unexpected_stops(self): - return list(filter(lambda x: x.started and not x.is_alive(), self.modules)) + return list(filter(lambda x: x.started and not x.is_alive(), self.modules.values())) def start(self, module_name): - if module_name in self.modules: + if module_name in self.modules.keys(): self.modules[module_name].start() def autostart(self, autostart_modules): - if autostart_modules is None: - # Modules to start are not specified. Fallback on configuration. + logger.info("Modules to start weren't specified in CLI arguments. Falling back to configuration.") autostart_modules = self.settings.autostart_modules + # We only want to autostart modules that are both in found modules and are asked to autostart. + autostart_modules = autostart_modules.intersection(set(self.modules.keys())) # Always start aw-server first if "aw-server" in autostart_modules: self.start("aw-server") @@ -161,7 +164,7 @@ def autostart(self, autostart_modules): self.start(module_name) def stop_all(self): - for module in filter(lambda m: m.is_alive(), self.modules): + for module in filter(lambda m: m.is_alive(), self.modules.values()): module.stop() From 7a674230641b053c677a88e5d32c4c9b0c46b886 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Wed, 4 Mar 2020 11:41:45 +0200 Subject: [PATCH 11/25] remove 1 unused import, don't treat aw-server as a special case in creating module menu --- aw_qt/config.py | 2 +- aw_qt/trayicon.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/aw_qt/config.py b/aw_qt/config.py index e6a0613..b5eb44d 100644 --- a/aw_qt/config.py +++ b/aw_qt/config.py @@ -1,5 +1,5 @@ from configparser import ConfigParser -from typing import ClassVar, List +from typing import List from aw_core.config import load_config import json diff --git a/aw_qt/trayicon.py b/aw_qt/trayicon.py index 69cb5b3..171ed7d 100644 --- a/aw_qt/trayicon.py +++ b/aw_qt/trayicon.py @@ -118,11 +118,8 @@ def add_module_menuitem(module): ac.setCheckable(True) ac.setChecked(module.is_alive()) - add_module_menuitem(self.manager.modules["aw-server"]) - for module_name in sorted(self.manager.modules.keys()): - if module_name != "aw-server": - add_module_menuitem(self.manager.modules[module_name]) + add_module_menuitem(self.manager.modules[module_name]) def exit(manager: Manager): From 4403d1937a0d13a973482f3acfd919cb4ffb1f9b Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Wed, 4 Mar 2020 16:39:24 +0200 Subject: [PATCH 12/25] Improve mypy typing --- Makefile | 2 +- aw_qt/main.py | 23 ++++++++++++++--------- aw_qt/manager.py | 34 ++++++++++++++++++---------------- aw_qt/trayicon.py | 45 ++++++++++++++++++++++++--------------------- mypy.ini | 6 ++++++ 5 files changed, 63 insertions(+), 47 deletions(-) create mode 100644 mypy.ini diff --git a/Makefile b/Makefile index 636bb97..b79c12a 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ test-integration: python3 ./tests/integration_tests.py --no-modules typecheck: - mypy aw_qt --ignore-missing-imports + mypy aw_qt --strict precommit: make typecheck diff --git a/aw_qt/main.py b/aw_qt/main.py index 366a6c5..cf212b1 100644 --- a/aw_qt/main.py +++ b/aw_qt/main.py @@ -1,30 +1,34 @@ import sys import logging import argparse +from typing import Set +from typing_extensions import TypedDict from aw_core.log import setup_logging from .manager import Manager - from . import trayicon logger = logging.getLogger(__name__) -def main(): +def main() -> None: args = parse_args() - setup_logging("aw-qt", testing=args.testing, verbose=args.testing, log_file=True) + setup_logging("aw-qt", testing=args['testing'], verbose=args['testing'], log_file=True) - _manager = Manager(testing=args.testing) - _manager.autostart(args.autostart_modules) + _manager = Manager(testing=args['testing']) + _manager.autostart(args['autostart_modules']) - error_code = trayicon.run(_manager, testing=args.testing) + error_code = trayicon.run(_manager, testing=args['testing']) _manager.stop_all() sys.exit(error_code) -def parse_args(): +CommandLineArgs = TypedDict('CommandLineArgs', {'testing': bool, 'autostart_modules': Set[str]}, total=False) + + +def parse_args() -> CommandLineArgs: parser = argparse.ArgumentParser(prog="aw-qt", description='A trayicon and service manager for ActivityWatch') parser.add_argument('--testing', action='store_true', help='Run the trayicon and services in testing mode') @@ -32,5 +36,6 @@ def parse_args(): type=lambda s: [m for m in s.split(',') if m and m.lower() != "none"], default=None, help='A comma-separated list of modules to autostart, or just `none` to not autostart anything') - - return parser.parse_args() + parsed_args = parser.parse_args() + dict: CommandLineArgs = {'autostart_modules': parsed_args.autostart_modules, 'testing': parsed_args.testing} + return dict diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 4efbf6c..4c518ed 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -30,7 +30,7 @@ def _locate_bundled_executable(name: str) -> Optional[str]: return None -def _is_system_module(name) -> bool: +def _is_system_module(name: str) -> bool: """Checks if a module with a particular name exists in PATH""" return shutil.which(name) is not None @@ -88,8 +88,8 @@ def __init__(self, name: str, testing: bool = False) -> None: self.started = False # Should be True if module is supposed to be running, else False self.testing = testing self.location = "system" if _is_system_module(name) else "bundled" - self._process = None # type: Optional[subprocess.Popen] - self._last_process = None # type: Optional[subprocess.Popen] + self._process: Optional[subprocess.Popen[str]] = None + self._last_process: Optional[subprocess.Popen[str]] = None def start(self) -> None: logger.info("Starting module {}".format(self.name)) @@ -97,11 +97,11 @@ def start(self) -> None: # Create a process group, become its leader # TODO: This shouldn't go here if platform.system() != "Windows": - os.setpgrp() # type: ignore + os.setpgrp() exec_path = _locate_executable(self.name) if exec_path is None: - return + logger.error("Tried to start nonexistent module {}".format(self.name)) else: exec_cmd = [exec_path] if self.testing: @@ -112,8 +112,8 @@ def start(self) -> None: # See: https://github.com/ActivityWatch/activitywatch/issues/212 startupinfo = None if platform.system() == "Windows": - startupinfo = subprocess.STARTUPINFO() # type: ignore - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore + startupinfo = subprocess.STARTUPINFO() #type: ignore + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW #type: ignore elif platform.system() == "Darwin": logger.info("Macos: Disable dock icon") import AppKit @@ -122,7 +122,6 @@ def start(self) -> None: # There is a very good reason stdout and stderr is not PIPE here # See: https://github.com/ActivityWatch/aw-server/issues/27 self._process = subprocess.Popen(exec_cmd, universal_newlines=True, startupinfo=startupinfo) - self.started = True def stop(self) -> None: @@ -137,7 +136,7 @@ def stop(self) -> None: logger.warning("Tried to stop module {}, but it wasn't running".format(self.name)) else: if not self._process: - logger.error("No reference to process object") + logger.error("") logger.debug("Stopping module {}".format(self.name)) if self._process: self._process.terminate() @@ -180,6 +179,7 @@ def __init__(self, testing: bool = False) -> None: self.settings: AwQtSettings = AwQtSettings(testing) self.modules: Dict[str, Module] = {} self.autostart_modules: Set[str] = set(self.settings.autostart_modules) + self.testing = testing for name in self.settings.possible_modules: if _locate_executable(name): @@ -189,7 +189,7 @@ def __init__(self, testing: bool = False) -> None: # Is this actually a good way to do this? merged from dev/autodetect-modules self.discover_modules() - def discover_modules(self): + def discover_modules(self) -> None: # These should always be bundled with aw-qt found_modules = set(_discover_modules_bundled()) found_modules |= set(_discover_modules_system()) @@ -199,17 +199,19 @@ def discover_modules(self): if m_name not in self.modules: self.modules[m_name] = Module(m_name, testing=self.testing) - def get_unexpected_stops(self): + def get_unexpected_stops(self) -> List[Module]: return list(filter(lambda x: x.started and not x.is_alive(), self.modules.values())) - def start(self, module_name): + def start(self, module_name: str) -> None: if module_name in self.modules.keys(): self.modules[module_name].start() + else: + logger.debug("Manager tried to start nonexistent module {}".format(module_name)) - def autostart(self, autostart_modules): - if autostart_modules is None: + def autostart(self, autostart_modules: Set[str] = set()) -> None: + if autostart_modules and len(autostart_modules) > 0: logger.info("Modules to start weren't specified in CLI arguments. Falling back to configuration.") - autostart_modules = self.settings.autostart_modules + autostart_modules = set(self.settings.autostart_modules) # We only want to autostart modules that are both in found modules and are asked to autostart. autostart_modules = autostart_modules.intersection(set(self.modules.keys())) @@ -223,7 +225,7 @@ def autostart(self, autostart_modules): for module_name in autostart_modules: self.start(module_name) - def stop_all(self): + def stop_all(self) -> None: for module in filter(lambda m: m.is_alive(), self.modules.values()): module.stop() diff --git a/aw_qt/trayicon.py b/aw_qt/trayicon.py index 8842be9..b36b565 100644 --- a/aw_qt/trayicon.py +++ b/aw_qt/trayicon.py @@ -5,6 +5,7 @@ import os import subprocess from collections import defaultdict +from typing import Any, DefaultDict, List, Optional from PyQt5 import QtCore from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMessageBox, QMenu, QWidget, QPushButton @@ -12,22 +13,22 @@ import aw_core -from .manager import Manager +from .manager import Manager, Module logger = logging.getLogger(__name__) -def open_webui(root_url): +def open_webui(root_url: str) -> None: print("Opening dashboard") webbrowser.open(root_url) -def open_apibrowser(root_url): +def open_apibrowser(root_url: str) -> None: print("Opening api browser") webbrowser.open(root_url + "/api") -def open_dir(d): +def open_dir(d: str)-> None: """From: http://stackoverflow.com/a/1795849/965332""" if sys.platform == 'win32': os.startfile(d) @@ -38,8 +39,9 @@ def open_dir(d): class TrayIcon(QSystemTrayIcon): - def __init__(self, manager: Manager, icon, parent=None, testing=False) -> None: + def __init__(self, manager: Manager, icon: QIcon, parent: Optional[QWidget]=None, testing: bool=False) -> None: QSystemTrayIcon.__init__(self, icon, parent) + self._parent = parent # QSystemTrayIcon also tries to save parent info but it screws up the type info self.setToolTip("ActivityWatch" + (" (testing)" if testing else "")) self.manager = manager @@ -47,8 +49,8 @@ def __init__(self, manager: Manager, icon, parent=None, testing=False) -> None: self._build_rootmenu() - def _build_rootmenu(self): - menu = QMenu(self.parent()) + def _build_rootmenu(self) -> None: + menu = QMenu(self._parent) root_url = "http://localhost:{port}".format(port=5666 if self.testing else 5600) @@ -80,8 +82,8 @@ def _build_rootmenu(self): self.setContextMenu(menu) - def show_module_failed_dialog(module): - box = QMessageBox(self.parent()) + def show_module_failed_dialog(module: Module) -> None: + box = QMessageBox(self._parent) box.setIcon(QMessageBox.Warning) box.setText("Module {} quit unexpectedly".format(module.name)) box.setDetailedText(module.read_log()) @@ -93,10 +95,10 @@ def show_module_failed_dialog(module): box.show() - def rebuild_modules_menu(): + def rebuild_modules_menu() -> None: for action in modulesMenu.actions(): if action.isEnabled(): - name = action.module.name + name = action.data().name alive = self.manager.modules[name].is_alive() action.setChecked(alive) # print(module.text(), alive) @@ -105,7 +107,7 @@ def rebuild_modules_menu(): QtCore.QTimer.singleShot(2000, rebuild_modules_menu) QtCore.QTimer.singleShot(2000, rebuild_modules_menu) - def check_module_status(): + def check_module_status() -> None: unexpected_exits = self.manager.get_unexpected_stops() if unexpected_exits: for module in unexpected_exits: @@ -116,19 +118,19 @@ def check_module_status(): QtCore.QTimer.singleShot(2000, rebuild_modules_menu) QtCore.QTimer.singleShot(2000, check_module_status) - def _build_modulemenu(self, moduleMenu): + def _build_modulemenu(self, moduleMenu: QMenu) -> None: moduleMenu.clear() - def add_module_menuitem(module): + def add_module_menuitem(module: Module) -> None: title = module.name ac = moduleMenu.addAction(title, lambda: module.toggle()) - # Kind of nasty, but a quick way to affiliate the module to the menu action for when it needs updating - ac.module = module + + ac.setData(module) ac.setCheckable(True) ac.setChecked(module.is_alive()) # Merged from branch dev/autodetect-modules, still kind of in progress with making this actually work - modules_by_location = defaultdict(lambda: list()) + modules_by_location: DefaultDict[str, List[Module]] = defaultdict(lambda: list()) for module in sorted(self.manager.modules.values(), key=lambda m: m.name): modules_by_location[module.location].append(module) @@ -140,7 +142,7 @@ def add_module_menuitem(module): add_module_menuitem(self.manager.modules[module.name]) -def exit(manager: Manager): +def exit(manager: Manager) -> None: # TODO: Do cleanup actions # TODO: Save state for resume print("Shutdown initiated, stopping all services...") @@ -151,16 +153,17 @@ def exit(manager: Manager): QApplication.quit() -def run(manager, testing=False): +def run(manager: Manager, testing: bool = False) -> Any: logger.info("Creating trayicon...") # print(QIcon.themeSearchPaths()) app = QApplication(sys.argv) + # FIXME: remove ignores after https://github.com/python/mypy/issues/2955 has been fixed # Without this, Ctrl+C will have no effect - signal.signal(signal.SIGINT, lambda: exit(manager)) + signal.signal(signal.SIGINT, lambda: exit(manager)) #type: ignore # Ensure cleanup happens on SIGTERM - signal.signal(signal.SIGTERM, lambda: exit(manager)) + signal.signal(signal.SIGTERM, lambda: exit(manager)) #type: ignore timer = QtCore.QTimer() timer.start(100) # You may change this if you wish. diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..039233b --- /dev/null +++ b/mypy.ini @@ -0,0 +1,6 @@ +[mypy] +python_version = 3.6 +ignore_missing_imports = True + +[mypy-*.resources] +ignore_errors = True From d4e33cf49e10ecc6c7dc9fa27908407787a9c78d Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Wed, 4 Mar 2020 17:48:55 +0200 Subject: [PATCH 13/25] Improve type safety and add some info logs / comments --- aw_qt/manager.py | 9 ++++++--- aw_qt/trayicon.py | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 4c518ed..578aa46 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -62,8 +62,9 @@ def _discover_modules_bundled() -> List[str]: name = os.path.basename(match) modules.append(name) else: - logger.warning("Found matching file but was not executable: {}".format(path)) + logger.warning("Found matching file but was not executable: {}".format(match)) + # This prints "Found... set()" if we found 0 bundled modules. Can be a bit misleading. logger.info("Found bundled modules: {}".format(set(modules))) return modules @@ -208,8 +209,10 @@ def start(self, module_name: str) -> None: else: logger.debug("Manager tried to start nonexistent module {}".format(module_name)) - def autostart(self, autostart_modules: Set[str] = set()) -> None: - if autostart_modules and len(autostart_modules) > 0: + def autostart(self, autostart_modules: Optional[Set[str]]) -> None: + if autostart_modules is None: + autostart_modules = set() + if len(autostart_modules) > 0: logger.info("Modules to start weren't specified in CLI arguments. Falling back to configuration.") autostart_modules = set(self.settings.autostart_modules) diff --git a/aw_qt/trayicon.py b/aw_qt/trayicon.py index b36b565..56e4c37 100644 --- a/aw_qt/trayicon.py +++ b/aw_qt/trayicon.py @@ -189,5 +189,6 @@ def run(manager: Manager, testing: bool = False) -> Any: QApplication.setQuitOnLastWindowClosed(False) + logger.info("Initialized aw-qt and trayicon succesfully") # Run the application, blocks until quit return app.exec_() From ea9d1964ea0bd8a7ddfb00e42d1c12ad5d9937b6 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Wed, 4 Mar 2020 17:53:13 +0200 Subject: [PATCH 14/25] Fix lambda parameter amount to allow termination on sigint / sigkill --- aw_qt/trayicon.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/aw_qt/trayicon.py b/aw_qt/trayicon.py index 56e4c37..e2bea0c 100644 --- a/aw_qt/trayicon.py +++ b/aw_qt/trayicon.py @@ -159,11 +159,10 @@ def run(manager: Manager, testing: bool = False) -> Any: app = QApplication(sys.argv) - # FIXME: remove ignores after https://github.com/python/mypy/issues/2955 has been fixed - # Without this, Ctrl+C will have no effect - signal.signal(signal.SIGINT, lambda: exit(manager)) #type: ignore - # Ensure cleanup happens on SIGTERM - signal.signal(signal.SIGTERM, lambda: exit(manager)) #type: ignore + # Ensure cleanup happens on SIGTERM and SIGINT (kill and ctrl+c etc) + # The 2 un-used variables are necessary + signal.signal(signal.SIGINT, lambda _, __: exit(manager)) + signal.signal(signal.SIGTERM, lambda _, __: exit(manager)) timer = QtCore.QTimer() timer.start(100) # You may change this if you wish. From a7c098cbe870cd3ebaa992bef6cd3b63c05222ce Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Wed, 4 Mar 2020 19:38:31 +0200 Subject: [PATCH 15/25] Add pyqt type stubs to dev dependencies, nicer mypy output by default Update poetry lockfile to include stubs --- Makefile | 2 +- poetry.lock | 145 ++++++++++++++++++++++++------------------------- pyproject.toml | 1 + 3 files changed, 74 insertions(+), 74 deletions(-) diff --git a/Makefile b/Makefile index b79c12a..b2dc389 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ test-integration: python3 ./tests/integration_tests.py --no-modules typecheck: - mypy aw_qt --strict + mypy aw_qt --strict --pretty precommit: make typecheck diff --git a/poetry.lock b/poetry.lock index f2d2d0d..3075736 100644 --- a/poetry.lock +++ b/poetry.lock @@ -41,7 +41,7 @@ strict-rfc3339 = "^0.7" mongo = ["pymongo (^3.10.0)"] [package.source] -reference = "c7d74e729bc0cd4d7f7326e0fbc5ae1528d8d2f7" +reference = "f43bbf47d2f192e1d104babff94e2aa8f9f5f226" type = "git" url = "https://github.com/ActivityWatch/aw-core.git" [[package]] @@ -51,7 +51,7 @@ marker = "python_version < \"3.8\"" name = "importlib-metadata" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.3.0" +version = "1.5.0" [package.dependencies] zipp = ">=0.5" @@ -90,15 +90,6 @@ version = "*" format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] -[[package]] -category = "main" -description = "More routines for operating on iterables, beyond itertools" -marker = "python_version < \"3.8\"" -name = "more-itertools" -optional = false -python-versions = ">=3.5" -version = "8.0.2" - [[package]] category = "dev" description = "Optional static typing for Python" @@ -262,7 +253,7 @@ pyobjc-framework-libdispatch = "6.1" [[package]] category = "main" description = "Python<->ObjC Interoperability Module" -marker = "sys_platform == \"darwin\" and platform_release < \"12.0\" or sys_platform == \"darwin\" or sys_platform == \"darwin\" and platform_release < \"13.0\" or sys_platform == \"darwin\" and platform_release >= \"10.0\" or platform_release >= \"10.0\" and platform_release < \"13.0\" and sys_platform == \"darwin\" or sys_platform == \"darwin\" and platform_release >= \"11.0\" or sys_platform == \"darwin\" and platform_release >= \"12.0\" or sys_platform == \"darwin\" and platform_release >= \"13.0\" or sys_platform == \"darwin\" and platform_release >= \"14.0\" or sys_platform == \"darwin\" and platform_release >= \"15.0\" or sys_platform == \"darwin\" and platform_release >= \"16.0\" or sys_platform == \"darwin\" and platform_release >= \"17.0\" or sys_platform == \"darwin\" and platform_release >= \"18.0\" or sys_platform == \"darwin\" and platform_release >= \"19.0\" or sys_platform == \"darwin\" and platform_release >= \"9.0\" or platform_release >= \"9.0\" and platform_release < \"11.0\" and sys_platform == \"darwin\" or platform_release >= \"9.0\" and platform_release < \"19.0\" and sys_platform == \"darwin\"" +marker = "sys_platform == \"darwin\" and platform_release >= \"9.0\" or sys_platform == \"darwin\" or sys_platform == \"darwin\" and platform_release < \"12.0\" or sys_platform == \"darwin\" and platform_release < \"13.0\" or sys_platform == \"darwin\" and platform_release >= \"10.0\" or platform_release >= \"10.0\" and platform_release < \"13.0\" and sys_platform == \"darwin\" or sys_platform == \"darwin\" and platform_release >= \"11.0\" or sys_platform == \"darwin\" and platform_release >= \"12.0\" or sys_platform == \"darwin\" and platform_release >= \"13.0\" or sys_platform == \"darwin\" and platform_release >= \"14.0\" or sys_platform == \"darwin\" and platform_release >= \"15.0\" or sys_platform == \"darwin\" and platform_release >= \"16.0\" or sys_platform == \"darwin\" and platform_release >= \"17.0\" or sys_platform == \"darwin\" and platform_release >= \"18.0\" or sys_platform == \"darwin\" and platform_release >= \"19.0\" or platform_release >= \"9.0\" and platform_release < \"11.0\" and sys_platform == \"darwin\" or platform_release >= \"9.0\" and platform_release < \"19.0\" and sys_platform == \"darwin\"" name = "pyobjc-core" optional = false python-versions = ">=3.6" @@ -460,7 +451,7 @@ pyobjc-framework-CoreLocation = ">=6.1" [[package]] category = "main" description = "Wrappers for the Cocoa frameworks on macOS" -marker = "sys_platform == \"darwin\" and platform_release < \"12.0\" or sys_platform == \"darwin\" or sys_platform == \"darwin\" and platform_release < \"13.0\" or sys_platform == \"darwin\" and platform_release >= \"10.0\" or platform_release >= \"10.0\" and platform_release < \"13.0\" and sys_platform == \"darwin\" or sys_platform == \"darwin\" and platform_release >= \"11.0\" or sys_platform == \"darwin\" and platform_release >= \"12.0\" or sys_platform == \"darwin\" and platform_release >= \"13.0\" or sys_platform == \"darwin\" and platform_release >= \"14.0\" or sys_platform == \"darwin\" and platform_release >= \"15.0\" or sys_platform == \"darwin\" and platform_release >= \"16.0\" or sys_platform == \"darwin\" and platform_release >= \"17.0\" or sys_platform == \"darwin\" and platform_release >= \"18.0\" or sys_platform == \"darwin\" and platform_release >= \"19.0\" or sys_platform == \"darwin\" and platform_release >= \"9.0\" or platform_release >= \"9.0\" and platform_release < \"11.0\" and sys_platform == \"darwin\" or platform_release >= \"9.0\" and platform_release < \"19.0\" and sys_platform == \"darwin\"" +marker = "sys_platform == \"darwin\" and platform_release >= \"9.0\" or sys_platform == \"darwin\" or sys_platform == \"darwin\" and platform_release < \"12.0\" or sys_platform == \"darwin\" and platform_release < \"13.0\" or sys_platform == \"darwin\" and platform_release >= \"10.0\" or platform_release >= \"10.0\" and platform_release < \"13.0\" and sys_platform == \"darwin\" or sys_platform == \"darwin\" and platform_release >= \"11.0\" or sys_platform == \"darwin\" and platform_release >= \"12.0\" or sys_platform == \"darwin\" and platform_release >= \"13.0\" or sys_platform == \"darwin\" and platform_release >= \"14.0\" or sys_platform == \"darwin\" and platform_release >= \"15.0\" or sys_platform == \"darwin\" and platform_release >= \"16.0\" or sys_platform == \"darwin\" and platform_release >= \"17.0\" or sys_platform == \"darwin\" and platform_release >= \"18.0\" or sys_platform == \"darwin\" and platform_release >= \"19.0\" or platform_release >= \"9.0\" and platform_release < \"11.0\" and sys_platform == \"darwin\" or platform_release >= \"9.0\" and platform_release < \"19.0\" and sys_platform == \"darwin\"" name = "pyobjc-framework-cocoa" optional = false python-versions = ">=3.6" @@ -1823,7 +1814,18 @@ description = "The sip module support for PyQt5" name = "pyqt5-sip" optional = false python-versions = ">=3.5" -version = "12.7.0" +version = "12.7.1" + +[[package]] +category = "dev" +description = "PEP561 stub files for the PyQt5 framework" +name = "pyqt5-stubs" +optional = false +python-versions = "*" +version = "5.13.1.4" + +[package.dependencies] +PyQt5 = ">=5.13.0,<5.14.0" [[package]] category = "main" @@ -1831,7 +1833,7 @@ description = "Persistent/Functional/Immutable data structures" name = "pyrsistent" optional = false python-versions = "*" -version = "0.15.6" +version = "0.15.7" [package.dependencies] six = "*" @@ -1849,8 +1851,8 @@ category = "main" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*" -version = "1.13.0" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.14.0" [[package]] category = "main" @@ -1874,7 +1876,7 @@ description = "a fork of Python 2 and 3 ast modules with type comment support" name = "typed-ast" optional = false python-versions = "*" -version = "1.4.0" +version = "1.4.1" [[package]] category = "dev" @@ -1890,18 +1892,15 @@ description = "Backport of pathlib-compatible object wrapper for zip files" marker = "python_version < \"3.8\"" name = "zipp" optional = false -python-versions = ">=2.7" -version = "0.6.0" - -[package.dependencies] -more-itertools = "*" +python-versions = ">=3.6" +version = "3.1.0" [package.extras] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pathlib2", "contextlib2", "unittest2"] +testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "3bdefe463f45342bec4935839c49cb251bc28ca48cf3426fcf251a489448bdb5" +content-hash = "443f596a850b8f20ef0e3ed9029ff4a9db74240c89f2622421c9946a2ccb11f1" python-versions = "^3.6" [metadata.files] @@ -1915,8 +1914,8 @@ attrs = [ ] aw-core = [] importlib-metadata = [ - {file = "importlib_metadata-1.3.0-py2.py3-none-any.whl", hash = "sha256:d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"}, - {file = "importlib_metadata-1.3.0.tar.gz", hash = "sha256:073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45"}, + {file = "importlib_metadata-1.5.0-py2.py3-none-any.whl", hash = "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"}, + {file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"}, ] iso8601 = [ {file = "iso8601-0.1.12-py2.py3-none-any.whl", hash = "sha256:210e0134677cc0d02f6028087fee1df1e1d76d372ee1db0bf30bf66c5c1c89a3"}, @@ -1927,10 +1926,6 @@ jsonschema = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, ] -more-itertools = [ - {file = "more-itertools-8.0.2.tar.gz", hash = "sha256:b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d"}, - {file = "more_itertools-8.0.2-py3-none-any.whl", hash = "sha256:c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"}, -] mypy = [ {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, {file = "mypy-0.761-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36"}, @@ -2559,33 +2554,36 @@ pyqt5 = [ {file = "PyQt5-5.13.2-5.13.2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:509daab1c5aca22e3cf9508128abf38e6e5ae311d7426b21f4189ffd66b196e9"}, ] pyqt5-sip = [ - {file = "PyQt5_sip-12.7.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:1910c1cb5a388d4e59ebb2895d7015f360f3f6eeb1700e7e33e866c53137eb9e"}, - {file = "PyQt5_sip-12.7.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b43ba2f18999d41c3df72f590348152e14cd4f6dcea2058c734d688dfb1ec61f"}, - {file = "PyQt5_sip-12.7.0-cp35-cp35m-win32.whl", hash = "sha256:fabff832046643cdb93920ddaa8f77344df90768930fbe6bb33d211c4dcd0b5e"}, - {file = "PyQt5_sip-12.7.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8274ed50f4ffbe91d0f4cc5454394631edfecd75dc327aa01be8bc5818a57e88"}, - {file = "PyQt5_sip-12.7.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:ef3c7a0bf78674b0dda86ff5809d8495019903a096c128e1f160984b37848f73"}, - {file = "PyQt5_sip-12.7.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3c330ff1f70b3eaa6f63dce9274df996dffea82ad9726aa8e3d6cbe38e986b2f"}, - {file = "PyQt5_sip-12.7.0-cp36-cp36m-win32.whl", hash = "sha256:9047d887d97663790d811ac4e0d2e895f1bf2ecac4041691487de40c30239480"}, - {file = "PyQt5_sip-12.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9f6ab1417ecfa6c1ce6ce941e0cebc03e3ec9cd9925058043229a5f003ae5e40"}, - {file = "PyQt5_sip-12.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06bc66b50556fb949f14875a4c224423dbf03f972497ccb883fb19b7b7c3b346"}, - {file = "PyQt5_sip-12.7.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:482a910fa73ee0e36c258d7646ef38f8061774bbc1765a7da68c65056b573341"}, - {file = "PyQt5_sip-12.7.0-cp37-cp37m-win32.whl", hash = "sha256:1c7ad791ec86247f35243bbbdd29cd59989afbe0ab678e0a41211f4407f21dd8"}, - {file = "PyQt5_sip-12.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:da69ba17f6ece9a85617743cb19de689f2d63025bf8001e2facee2ec9bcff18f"}, - {file = "PyQt5_sip-12.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:02d94786bada670ab17a2b62ce95b3cf8e3b40c99d36007593a6334d551840bb"}, - {file = "PyQt5_sip-12.7.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c3ab9ea1bc3f4ce8c57ebc66fb25cd044ef92ed1ca2afa3729854ecc59658905"}, - {file = "PyQt5_sip-12.7.0-cp38-cp38-win32.whl", hash = "sha256:7695dfafb4f5549ce1290ae643d6508dfc2646a9003c989218be3ce42a1aa422"}, - {file = "PyQt5_sip-12.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:091fbbe10a7aebadc0e8897a9449cda08d3c3f663460d812eca3001ca1ed3526"}, - {file = "PyQt5_sip-12.7.0.tar.gz", hash = "sha256:0a067ade558befe4d46335b13d8b602b5044363bfd601419b556d4ec659bca18"}, + {file = "PyQt5_sip-12.7.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:f314f31f5fd39b06897f013f425137e511d45967150eb4e424a363d8138521c6"}, + {file = "PyQt5_sip-12.7.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b42021229424aa44e99b3b49520b799fd64ff6ae8b53f79f903bbd85719a28e4"}, + {file = "PyQt5_sip-12.7.1-cp35-cp35m-win32.whl", hash = "sha256:6b4860c4305980db509415d0af802f111d15f92016c9422eb753bc8883463456"}, + {file = "PyQt5_sip-12.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:d46b0f8effc554de52a1466b1bd80e5cb4bce635a75ac4e7ad6247c965dec5b9"}, + {file = "PyQt5_sip-12.7.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3f665376d9e52faa9855c3736a66ce6d825f85c86d7774d3c393f09da23f4f86"}, + {file = "PyQt5_sip-12.7.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1115728644bbadcde5fc8a16e7918bd31915a42dd6fb36b10d4afb78c582753e"}, + {file = "PyQt5_sip-12.7.1-cp36-cp36m-win32.whl", hash = "sha256:cbeeae6b45234a1654657f79943f8bccd3d14b4e7496746c62cf6fbce69442c7"}, + {file = "PyQt5_sip-12.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8da842d3d7bf8931d1093105fb92702276b6dbb7e801abbaaa869405d616171a"}, + {file = "PyQt5_sip-12.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f4289276d355b6521dc2cc956189315da6f13adfb6bbab8f25ebd15e3bce1d4"}, + {file = "PyQt5_sip-12.7.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c1e730a9eb2ec3869ed5d81b0f99f6e2460fb4d77750444c0ec183b771d798f7"}, + {file = "PyQt5_sip-12.7.1-cp37-cp37m-win32.whl", hash = "sha256:b5b4906445fe980aee76f20400116b6904bf5f30d0767489c13370e42a764020"}, + {file = "PyQt5_sip-12.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7ffa39763097f64de129cf5cc770a651c3f65d2466b4fe05bef2bd2efbaa38e6"}, + {file = "PyQt5_sip-12.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:288c6dc18a8d6a20981c07b715b5695d9b66880778565f3792bc6e38f14f20fb"}, + {file = "PyQt5_sip-12.7.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ee1a12f09d5af2304273bfd2f6b43835c1467d5ed501a6c95f5405637fa7750a"}, + {file = "PyQt5_sip-12.7.1-cp38-cp38-win32.whl", hash = "sha256:8a18e6f45d482ddfe381789979d09ee13aa6450caa3a0476503891bccb3ac709"}, + {file = "PyQt5_sip-12.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:e28c3abc9b62a1b7e796891648b9f14f8167b31c8e7990fae79654777252bb4d"}, + {file = "PyQt5_sip-12.7.1.tar.gz", hash = "sha256:e6078f5ee7d31c102910d0c277a110e1c2a20a3fc88cd017a39e170120586d3f"}, +] +pyqt5-stubs = [ + {file = "PyQt5-stubs-5.13.1.4.tar.gz", hash = "sha256:c396d3a6de0e92b3d9465aa318e945464b7fa35bceeae1e68e5233715c8a0c90"}, ] pyrsistent = [ - {file = "pyrsistent-0.15.6.tar.gz", hash = "sha256:f3b280d030afb652f79d67c5586157c5c1355c9a58dfc7940566e28d28f3df1b"}, + {file = "pyrsistent-0.15.7.tar.gz", hash = "sha256:cdc7b5e3ed77bed61270a47d35434a30617b9becdf2478af76ad2c6ade307280"}, ] python-json-logger = [ {file = "python-json-logger-0.1.11.tar.gz", hash = "sha256:b7a31162f2a01965a5efb94453ce69230ed208468b0bbc7fdfc56e6d8df2e281"}, ] six = [ - {file = "six-1.13.0-py2.py3-none-any.whl", hash = "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd"}, - {file = "six-1.13.0.tar.gz", hash = "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"}, + {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, + {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, ] strict-rfc3339 = [ {file = "strict-rfc3339-0.7.tar.gz", hash = "sha256:5cad17bedfc3af57b399db0fed32771f18fc54bbd917e85546088607ac5e1277"}, @@ -2594,26 +2592,27 @@ takethetime = [ {file = "TakeTheTime-0.3.1.tar.gz", hash = "sha256:dbe30453a1b596a38f9e2e3fa8e1adc5af2dbf646ca0837ad5c2059a16fe2ff9"}, ] typed-ast = [ - {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e"}, - {file = "typed_ast-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b"}, - {file = "typed_ast-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4"}, - {file = "typed_ast-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"}, - {file = "typed_ast-1.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631"}, - {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233"}, - {file = "typed_ast-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1"}, - {file = "typed_ast-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a"}, - {file = "typed_ast-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c"}, - {file = "typed_ast-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a"}, - {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e"}, - {file = "typed_ast-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d"}, - {file = "typed_ast-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36"}, - {file = "typed_ast-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0"}, - {file = "typed_ast-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66"}, - {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2"}, - {file = "typed_ast-1.4.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47"}, - {file = "typed_ast-1.4.0-cp38-cp38-win32.whl", hash = "sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161"}, - {file = "typed_ast-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e"}, - {file = "typed_ast-1.4.0.tar.gz", hash = "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, + {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, + {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, + {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, + {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, + {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, + {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, + {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, + {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, + {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, + {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, + {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, + {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, + {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, + {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, ] typing-extensions = [ {file = "typing_extensions-3.7.4.1-py2-none-any.whl", hash = "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d"}, @@ -2621,6 +2620,6 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.1.tar.gz", hash = "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2"}, ] zipp = [ - {file = "zipp-0.6.0-py2.py3-none-any.whl", hash = "sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"}, - {file = "zipp-0.6.0.tar.gz", hash = "sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e"}, + {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, + {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, ] diff --git a/pyproject.toml b/pyproject.toml index d92e73c..00a2ca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ aw-core = {git = "https://github.com/ActivityWatch/aw-core.git"} [tool.poetry.dev-dependencies] mypy = "^0.761" +PyQt5-stubs = "^5.13" [build-system] requires = ["poetry>=0.12"] From 8808f7c73d6ce2d539748b474c76c68a9a1ebc2c Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Wed, 4 Mar 2020 19:54:59 +0200 Subject: [PATCH 16/25] Improve autostart() input typing --- aw_qt/main.py | 4 ++-- aw_qt/manager.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/aw_qt/main.py b/aw_qt/main.py index cf212b1..2280517 100644 --- a/aw_qt/main.py +++ b/aw_qt/main.py @@ -1,7 +1,7 @@ import sys import logging import argparse -from typing import Set +from typing import List from typing_extensions import TypedDict from aw_core.log import setup_logging @@ -25,7 +25,7 @@ def main() -> None: sys.exit(error_code) -CommandLineArgs = TypedDict('CommandLineArgs', {'testing': bool, 'autostart_modules': Set[str]}, total=False) +CommandLineArgs = TypedDict('CommandLineArgs', {'testing': bool, 'autostart_modules': List[str]}, total=False) def parse_args() -> CommandLineArgs: diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 578aa46..759409e 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -209,23 +209,23 @@ def start(self, module_name: str) -> None: else: logger.debug("Manager tried to start nonexistent module {}".format(module_name)) - def autostart(self, autostart_modules: Optional[Set[str]]) -> None: + def autostart(self, autostart_modules: Optional[List[str]]) -> None: if autostart_modules is None: - autostart_modules = set() + autostart_modules = [] if len(autostart_modules) > 0: logger.info("Modules to start weren't specified in CLI arguments. Falling back to configuration.") - autostart_modules = set(self.settings.autostart_modules) - + autostart_modules = self.settings.autostart_modules # We only want to autostart modules that are both in found modules and are asked to autostart. - autostart_modules = autostart_modules.intersection(set(self.modules.keys())) - # Always start aw-server first - if "aw-server" in autostart_modules: - self.start("aw-server") - elif "aw-server-rust" in autostart_modules: + modules_to_start = set(autostart_modules).intersection(set(self.modules.keys())) + + # Start aw-server-rust first + if "aw-server-rust" in modules_to_start: self.start("aw-server-rust") + elif "aw-server" in modules_to_start: + self.start("aw-server") - autostart_modules = set(autostart_modules) - {"aw-server", "aw-server-rust"} - for module_name in autostart_modules: + modules_to_start = set(autostart_modules) - {"aw-server", "aw-server-rust"} + for module_name in modules_to_start: self.start(module_name) def stop_all(self) -> None: From b14b1be57fef5bf6b6f93f16e274c92a15d41585 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Wed, 4 Mar 2020 20:11:31 +0200 Subject: [PATCH 17/25] Add pip cache for macOS, should make pyobjc etc. installation significantly faster --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index d10c59c..cac9158 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,10 @@ dist: xenial language: python + +cache: + directories: + - $HOME/.cache/pip + matrix: include: - python: "3.6" From 9e4b15ebfe05ebd631e7cebe7c353b5e7a7ad104 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Thu, 5 Mar 2020 11:04:36 +0200 Subject: [PATCH 18/25] Remove unnecessary possible_modules config option --- aw_qt/config.py | 5 ----- aw_qt/manager.py | 6 ------ 2 files changed, 11 deletions(-) diff --git a/aw_qt/config.py b/aw_qt/config.py index b5eb44d..b33e187 100644 --- a/aw_qt/config.py +++ b/aw_qt/config.py @@ -5,10 +5,6 @@ import json default_settings = { - "possible_modules": json.dumps(["aw-server", - "aw-server-rust", - "aw-watcher-afk", - "aw-watcher-window", ]), "autostart_modules": json.dumps(["aw-server-rust", "aw-server", "aw-watcher-afk", @@ -26,5 +22,4 @@ class AwQtSettings: def __init__(self, testing: bool): config_section = qt_config["aw-qt" if not testing else "aw-qt-testing"] - self.possible_modules: List[str] = json.loads(config_section["possible_modules"]) self.autostart_modules: List[str] = json.loads(config_section["autostart_modules"]) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 759409e..c2251d0 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -182,12 +182,6 @@ def __init__(self, testing: bool = False) -> None: self.autostart_modules: Set[str] = set(self.settings.autostart_modules) self.testing = testing - for name in self.settings.possible_modules: - if _locate_executable(name): - self.modules[name] = Module(name, testing=testing) - else: - logger.warning("Module '{}' not found but was in possible modules".format(name)) - # Is this actually a good way to do this? merged from dev/autodetect-modules self.discover_modules() def discover_modules(self) -> None: From 9a849caf8e215153a99b35feff16ff456f679495 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Thu, 5 Mar 2020 11:21:57 +0200 Subject: [PATCH 19/25] Recursively find submodules in subdirectories --- aw_qt/manager.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index c2251d0..a77e3d0 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -52,20 +52,30 @@ def _locate_executable(name: str) -> Optional[str]: return None +""" +Look for modules in given directory path and recursively in subdirs matching aw-* +""" +def _discover_modules_in_directory(modules: List[str], search_path: str) -> None: + matches = glob(os.path.join(search_path, "aw-*")) + for match in matches: + if os.path.isfile(match) and os.access(match, os.X_OK): + name = os.path.basename(match) + modules.append(name) + elif os.path.isdir(match) and os.access(match, os.X_OK): + _discover_modules_in_directory(modules, match) + else: + logger.warning("Found matching file but was not executable: {}".format(match)) + + def _discover_modules_bundled() -> List[str]: - # Look for modules in source dir and parent dir - modules = [] - for path in _search_paths: - matches = glob(os.path.join(path, "aw-*")) - for match in matches: - if os.path.isfile(match) and os.access(match, os.X_OK): - name = os.path.basename(match) - modules.append(name) - else: - logger.warning("Found matching file but was not executable: {}".format(match)) - - # This prints "Found... set()" if we found 0 bundled modules. Can be a bit misleading. - logger.info("Found bundled modules: {}".format(set(modules))) + modules: List[str] = [] + cwd = os.getcwd() + _discover_modules_in_directory(modules, cwd) + + if len(modules) > 0: + logger.info("Found bundled modules: {}".format(set(modules))) + else: + logger.info("Found no bundles modules") return modules From 2a6a8daa7acd5a29fe647b5112d8c3e618cf2cd9 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Thu, 5 Mar 2020 11:34:27 +0200 Subject: [PATCH 20/25] Add some informative pydoc comments --- aw_qt/config.py | 5 ++++- aw_qt/manager.py | 8 +++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/aw_qt/config.py b/aw_qt/config.py index b33e187..7cc6980 100644 --- a/aw_qt/config.py +++ b/aw_qt/config.py @@ -17,7 +17,10 @@ default_config['aw-qt-testing'] = default_settings qt_config = load_config("aw-qt", default_config) - +""" +An instance of loaded settings, containing a list of modules to autostart. +Constructor takes a `testing` boolean as an argument +""" class AwQtSettings: def __init__(self, testing: bool): config_section = qt_config["aw-qt" if not testing else "aw-qt-testing"] diff --git a/aw_qt/manager.py b/aw_qt/manager.py index a77e3d0..f1f99fc 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -52,9 +52,7 @@ def _locate_executable(name: str) -> Optional[str]: return None -""" -Look for modules in given directory path and recursively in subdirs matching aw-* -""" +""" Look for modules in given directory path and recursively in subdirs matching aw-* """ def _discover_modules_in_directory(modules: List[str], search_path: str) -> None: matches = glob(os.path.join(search_path, "aw-*")) for match in matches: @@ -66,7 +64,7 @@ def _discover_modules_in_directory(modules: List[str], search_path: str) -> None else: logger.warning("Found matching file but was not executable: {}".format(match)) - +""" Use _discover_modules_in_directory to find all bundled modules """ def _discover_modules_bundled() -> List[str]: modules: List[str] = [] cwd = os.getcwd() @@ -78,7 +76,7 @@ def _discover_modules_bundled() -> List[str]: logger.info("Found no bundles modules") return modules - +""" Find all aw- modules in PATH """ def _discover_modules_system() -> List[str]: search_paths = os.environ["PATH"].split(":") modules = [] From 68314f04a1e074534643da883fae4aaf8bf45239 Mon Sep 17 00:00:00 2001 From: Kerkko Pelttari Date: Sat, 25 Apr 2020 09:03:50 +0300 Subject: [PATCH 21/25] Get PATH using python's os module instead of getting environment variable and manually splitting --- aw_qt/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index f1f99fc..3c36c41 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -78,7 +78,7 @@ def _discover_modules_bundled() -> List[str]: """ Find all aw- modules in PATH """ def _discover_modules_system() -> List[str]: - search_paths = os.environ["PATH"].split(":") + search_paths = os.get_exec_path() modules = [] for path in search_paths: if os.path.isdir(path): From f6d26c7ba0e239a35968620e706e5897aed7c3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sat, 25 Jul 2020 13:02:46 +0200 Subject: [PATCH 22/25] fix: minor cleanup --- aw_qt/config.py | 26 +++++++++++++----------- aw_qt/manager.py | 51 +++++++++++++++++++++++++++++++++-------------- aw_qt/trayicon.py | 6 +++--- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/aw_qt/config.py b/aw_qt/config.py index 7cc6980..8d12325 100644 --- a/aw_qt/config.py +++ b/aw_qt/config.py @@ -5,24 +5,26 @@ import json default_settings = { - "autostart_modules": json.dumps(["aw-server-rust", - "aw-server", - "aw-watcher-afk", - "aw-watcher-window", ]), + "autostart_modules": json.dumps( + ["aw-server", "aw-watcher-afk", "aw-watcher-window",] + ), } default_config = ConfigParser() -default_config['aw-qt'] = default_settings +default_config["aw-qt"] = default_settings # Currently there's no reason to make them differ -default_config['aw-qt-testing'] = default_settings -qt_config = load_config("aw-qt", default_config) +default_config["aw-qt-testing"] = default_settings + -""" -An instance of loaded settings, containing a list of modules to autostart. -Constructor takes a `testing` boolean as an argument -""" class AwQtSettings: def __init__(self, testing: bool): + """ + An instance of loaded settings, containing a list of modules to autostart. + Constructor takes a `testing` boolean as an argument + """ + qt_config = load_config("aw-qt", default_config) config_section = qt_config["aw-qt" if not testing else "aw-qt-testing"] - self.autostart_modules: List[str] = json.loads(config_section["autostart_modules"]) + self.autostart_modules: List[str] = json.loads( + config_section["autostart_modules"] + ) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 3c36c41..ef96938 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -48,12 +48,14 @@ def _locate_executable(name: str) -> Optional[str]: elif _is_system_module(name): # Check if it's in PATH return name else: - logger.warning("Could not find module '{}' in installation directory or PATH".format(name)) + logger.warning( + "Could not find module '{}' in installation directory or PATH".format(name) + ) return None -""" Look for modules in given directory path and recursively in subdirs matching aw-* """ def _discover_modules_in_directory(modules: List[str], search_path: str) -> None: + """Look for modules in given directory path and recursively in subdirs matching aw-*""" matches = glob(os.path.join(search_path, "aw-*")) for match in matches: if os.path.isfile(match) and os.access(match, os.X_OK): @@ -62,10 +64,13 @@ def _discover_modules_in_directory(modules: List[str], search_path: str) -> None elif os.path.isdir(match) and os.access(match, os.X_OK): _discover_modules_in_directory(modules, match) else: - logger.warning("Found matching file but was not executable: {}".format(match)) + logger.warning( + "Found matching file but was not executable: {}".format(match) + ) + -""" Use _discover_modules_in_directory to find all bundled modules """ def _discover_modules_bundled() -> List[str]: + """Use ``_discover_modules_in_directory`` to find all bundled modules """ modules: List[str] = [] cwd = os.getcwd() _discover_modules_in_directory(modules, cwd) @@ -76,8 +81,9 @@ def _discover_modules_bundled() -> List[str]: logger.info("Found no bundles modules") return modules -""" Find all aw- modules in PATH """ + def _discover_modules_system() -> List[str]: + """Find all aw- modules in PATH""" search_paths = os.get_exec_path() modules = [] for path in search_paths: @@ -94,7 +100,9 @@ def _discover_modules_system() -> List[str]: class Module: def __init__(self, name: str, testing: bool = False) -> None: self.name = name - self.started = False # Should be True if module is supposed to be running, else False + self.started = ( + False # Should be True if module is supposed to be running, else False + ) self.testing = testing self.location = "system" if _is_system_module(name) else "bundled" self._process: Optional[subprocess.Popen[str]] = None @@ -121,16 +129,19 @@ def start(self) -> None: # See: https://github.com/ActivityWatch/activitywatch/issues/212 startupinfo = None if platform.system() == "Windows": - startupinfo = subprocess.STARTUPINFO() #type: ignore - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW #type: ignore + startupinfo = subprocess.STARTUPINFO() # type: ignore + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore elif platform.system() == "Darwin": logger.info("Macos: Disable dock icon") import AppKit + AppKit.NSBundle.mainBundle().infoDictionary()["LSBackgroundOnly"] = "1" # There is a very good reason stdout and stderr is not PIPE here # See: https://github.com/ActivityWatch/aw-server/issues/27 - self._process = subprocess.Popen(exec_cmd, universal_newlines=True, startupinfo=startupinfo) + self._process = subprocess.Popen( + exec_cmd, universal_newlines=True, startupinfo=startupinfo + ) self.started = True def stop(self) -> None: @@ -139,13 +150,17 @@ def stop(self) -> None: """ # TODO: What if a module doesn't stop? Add timeout to p.wait() and then do a p.kill() if timeout is hit if not self.started: - logger.warning("Tried to stop module {}, but it hasn't been started".format(self.name)) + logger.warning( + "Tried to stop module {}, but it hasn't been started".format(self.name) + ) return elif not self.is_alive(): - logger.warning("Tried to stop module {}, but it wasn't running".format(self.name)) + logger.warning( + "Tried to stop module {}, but it wasn't running".format(self.name) + ) else: if not self._process: - logger.error("") + logger.error("No reference to process object") logger.debug("Stopping module {}".format(self.name)) if self._process: self._process.terminate() @@ -203,19 +218,25 @@ def discover_modules(self) -> None: self.modules[m_name] = Module(m_name, testing=self.testing) def get_unexpected_stops(self) -> List[Module]: - return list(filter(lambda x: x.started and not x.is_alive(), self.modules.values())) + return list( + filter(lambda x: x.started and not x.is_alive(), self.modules.values()) + ) def start(self, module_name: str) -> None: if module_name in self.modules.keys(): self.modules[module_name].start() else: - logger.debug("Manager tried to start nonexistent module {}".format(module_name)) + logger.debug( + "Manager tried to start nonexistent module {}".format(module_name) + ) def autostart(self, autostart_modules: Optional[List[str]]) -> None: if autostart_modules is None: autostart_modules = [] if len(autostart_modules) > 0: - logger.info("Modules to start weren't specified in CLI arguments. Falling back to configuration.") + logger.info( + "Modules to start weren't specified in CLI arguments. Falling back to configuration." + ) autostart_modules = self.settings.autostart_modules # We only want to autostart modules that are both in found modules and are asked to autostart. modules_to_start = set(autostart_modules).intersection(set(self.modules.keys())) diff --git a/aw_qt/trayicon.py b/aw_qt/trayicon.py index d13f551..ce3ad47 100644 --- a/aw_qt/trayicon.py +++ b/aw_qt/trayicon.py @@ -4,7 +4,7 @@ import os import subprocess from collections import defaultdict -from typing import Any, DefaultDict, List, Optional +from typing import Any, DefaultDict, List, Optional, Dict import webbrowser from PyQt5 import QtCore @@ -25,7 +25,7 @@ logger = logging.getLogger(__name__) -def get_env(): +def get_env() -> Dict[str, str]: """ Necessary for xdg-open to work properly when PyInstaller overrides LD_LIBRARY_PATH @@ -43,7 +43,7 @@ def get_env(): return env -def open_url(url): +def open_url(url: str) -> None: if sys.platform == "linux": env = get_env() subprocess.Popen(["xdg-open", url], env=env) From 682a73f142e3af311ee6a63419f971f9f96e8b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sat, 25 Jul 2020 13:37:08 +0200 Subject: [PATCH 23/25] feat: switched to using click for the CLI, fixed bugs --- aw_qt/config.py | 1 + aw_qt/main.py | 49 ++++++++++++++++++------------------ aw_qt/manager.py | 64 +++++++++++++++++++++++------------------------- poetry.lock | 14 ++++++++++- pyproject.toml | 1 + 5 files changed, 69 insertions(+), 60 deletions(-) diff --git a/aw_qt/config.py b/aw_qt/config.py index 8d12325..1047350 100644 --- a/aw_qt/config.py +++ b/aw_qt/config.py @@ -4,6 +4,7 @@ from aw_core.config import load_config import json +# NOTE: Updating this won't update the defaults for users, this is an issue with how aw_core.config works default_settings = { "autostart_modules": json.dumps( ["aw-server", "aw-watcher-afk", "aw-watcher-window",] diff --git a/aw_qt/main.py b/aw_qt/main.py index 2280517..e5c030b 100644 --- a/aw_qt/main.py +++ b/aw_qt/main.py @@ -1,41 +1,40 @@ import sys import logging import argparse -from typing import List +import click +from typing import List, Optional from typing_extensions import TypedDict from aw_core.log import setup_logging from .manager import Manager from . import trayicon +from .config import AwQtSettings logger = logging.getLogger(__name__) -def main() -> None: - args = parse_args() - setup_logging("aw-qt", testing=args['testing'], verbose=args['testing'], log_file=True) - - _manager = Manager(testing=args['testing']) - _manager.autostart(args['autostart_modules']) - - error_code = trayicon.run(_manager, testing=args['testing']) +@click.command("aw-qt", help="A trayicon and service manager for ActivityWatch") +@click.option( + "--testing", is_flag=True, help="Run the trayicon and services in testing mode" +) +@click.option( + "--autostart-modules", + help="A comma-separated list of modules to autostart, or just `none` to not autostart anything.", +) +def main(testing: bool, autostart_modules: Optional[str]) -> None: + config = AwQtSettings(testing=testing) + _autostart_modules = ( + [m.strip() for m in autostart_modules.split(",") if m and m.lower() != "none"] + if autostart_modules + else config.autostart_modules + ) + setup_logging("aw-qt", testing=testing, verbose=testing, log_file=True) + + _manager = Manager(testing=testing) + _manager.autostart(_autostart_modules) + + error_code = trayicon.run(_manager, testing=testing) _manager.stop_all() sys.exit(error_code) - - -CommandLineArgs = TypedDict('CommandLineArgs', {'testing': bool, 'autostart_modules': List[str]}, total=False) - - -def parse_args() -> CommandLineArgs: - parser = argparse.ArgumentParser(prog="aw-qt", description='A trayicon and service manager for ActivityWatch') - parser.add_argument('--testing', action='store_true', - help='Run the trayicon and services in testing mode') - parser.add_argument('--autostart-modules', dest='autostart_modules', - type=lambda s: [m for m in s.split(',') if m and m.lower() != "none"], - default=None, - help='A comma-separated list of modules to autostart, or just `none` to not autostart anything') - parsed_args = parser.parse_args() - dict: CommandLineArgs = {'autostart_modules': parsed_args.autostart_modules, 'testing': parsed_args.testing} - return dict diff --git a/aw_qt/manager.py b/aw_qt/manager.py index ef96938..6a85dd5 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -5,7 +5,7 @@ import logging import subprocess import shutil -from typing import Optional, List, Dict, Set +from typing import Optional, List, Dict, Set, Tuple import aw_core @@ -35,7 +35,7 @@ def _is_system_module(name: str) -> bool: return shutil.which(name) is not None -def _locate_executable(name: str) -> Optional[str]: +def _locate_executable(name: str) -> Tuple[Optional[str], Optional[str]]: """ Will return the path to the executable if bundled, otherwise returns the name if it is available in PATH. @@ -44,41 +44,38 @@ def _locate_executable(name: str) -> Optional[str]: """ exec_path = _locate_bundled_executable(name) if exec_path is not None: # Check if it exists in bundle - return exec_path + return exec_path, "bundle" elif _is_system_module(name): # Check if it's in PATH - return name + return name, "system" else: logger.warning( "Could not find module '{}' in installation directory or PATH".format(name) ) - return None + return None, None -def _discover_modules_in_directory(modules: List[str], search_path: str) -> None: +def _discover_modules_in_directory(search_path: str) -> List[str]: """Look for modules in given directory path and recursively in subdirs matching aw-*""" + modules = [] matches = glob(os.path.join(search_path, "aw-*")) for match in matches: if os.path.isfile(match) and os.access(match, os.X_OK): name = os.path.basename(match) modules.append(name) elif os.path.isdir(match) and os.access(match, os.X_OK): - _discover_modules_in_directory(modules, match) + modules.extend(_discover_modules_in_directory(match)) else: logger.warning( "Found matching file but was not executable: {}".format(match) ) + return modules def _discover_modules_bundled() -> List[str]: """Use ``_discover_modules_in_directory`` to find all bundled modules """ - modules: List[str] = [] cwd = os.getcwd() - _discover_modules_in_directory(modules, cwd) - - if len(modules) > 0: - logger.info("Found bundled modules: {}".format(set(modules))) - else: - logger.info("Found no bundles modules") + modules = _discover_modules_in_directory(cwd) + logger.info("Found bundled modules: {}".format(set(modules))) return modules @@ -90,7 +87,7 @@ def _discover_modules_system() -> List[str]: if os.path.isdir(path): files = os.listdir(path) for filename in files: - if "aw-" in filename: + if filename.startswith("aw-"): modules.append(filename) logger.info("Found system modules: {}".format(set(modules))) @@ -116,9 +113,10 @@ def start(self) -> None: if platform.system() != "Windows": os.setpgrp() - exec_path = _locate_executable(self.name) + exec_path, location = _locate_executable(self.name) if exec_path is None: logger.error("Tried to start nonexistent module {}".format(self.name)) + return else: exec_cmd = [exec_path] if self.testing: @@ -132,7 +130,7 @@ def start(self) -> None: startupinfo = subprocess.STARTUPINFO() # type: ignore startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore elif platform.system() == "Darwin": - logger.info("Macos: Disable dock icon") + logger.info("macOS: Disable dock icon") import AppKit AppKit.NSBundle.mainBundle().infoDictionary()["LSBackgroundOnly"] = "1" @@ -200,9 +198,7 @@ def read_log(self) -> str: class Manager: def __init__(self, testing: bool = False) -> None: - self.settings: AwQtSettings = AwQtSettings(testing) self.modules: Dict[str, Module] = {} - self.autostart_modules: Set[str] = set(self.settings.autostart_modules) self.testing = testing self.discover_modules() @@ -211,7 +207,7 @@ def discover_modules(self) -> None: # These should always be bundled with aw-qt found_modules = set(_discover_modules_bundled()) found_modules |= set(_discover_modules_system()) - found_modules ^= {"aw-qt"} # Exclude self + found_modules ^= {"aw-qt", "aw-client"} # Exclude self for m_name in found_modules: if m_name not in self.modules: @@ -230,26 +226,26 @@ def start(self, module_name: str) -> None: "Manager tried to start nonexistent module {}".format(module_name) ) - def autostart(self, autostart_modules: Optional[List[str]]) -> None: - if autostart_modules is None: - autostart_modules = [] - if len(autostart_modules) > 0: - logger.info( - "Modules to start weren't specified in CLI arguments. Falling back to configuration." - ) - autostart_modules = self.settings.autostart_modules + def autostart(self, autostart_modules: List[str]) -> None: # We only want to autostart modules that are both in found modules and are asked to autostart. - modules_to_start = set(autostart_modules).intersection(set(self.modules.keys())) + not_found = [] + for name in autostart_modules: + if name not in self.modules.keys(): + logger.error(f"Module {name} not found") + not_found.append(name) + autostart_modules = list(set(autostart_modules) - set(not_found)) # Start aw-server-rust first - if "aw-server-rust" in modules_to_start: + if "aw-server-rust" in autostart_modules: self.start("aw-server-rust") - elif "aw-server" in modules_to_start: + elif "aw-server" in autostart_modules: self.start("aw-server") - modules_to_start = set(autostart_modules) - {"aw-server", "aw-server-rust"} - for module_name in modules_to_start: - self.start(module_name) + autostart_modules = list( + set(autostart_modules) - {"aw-server", "aw-server-rust"} + ) + for name in autostart_modules: + self.start(name) def stop_all(self) -> None: for module in filter(lambda m: m.is_alive(), self.modules.values()): diff --git a/poetry.lock b/poetry.lock index 64ff80b..5d1cf23 100644 --- a/poetry.lock +++ b/poetry.lock @@ -44,6 +44,14 @@ mongo = ["pymongo (^3.10.0)"] reference = "548f4fcecb2704f64cf8cc9a35f377f68f0d3bf2" type = "git" url = "https://github.com/ActivityWatch/aw-core.git" +[[package]] +category = "main" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "7.1.2" + [[package]] category = "main" description = "Read metadata from Python packages" @@ -1923,7 +1931,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "92e440cb36d336fd84d76996a4dafab97451ee22af238b4bd629fb478dc8a51e" +content-hash = "fbb1a5688e32082aadc59ccbcc93c06865e3a574c37019900172e952306f1c34" python-versions = "^3.6, <3.8" [metadata.files] @@ -1936,6 +1944,10 @@ attrs = [ {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, ] aw-core = [] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] importlib-metadata = [ {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, diff --git a/pyproject.toml b/pyproject.toml index 049f59a..5dd0dc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ PyQt5 = "5.10.1" sip = "4.19.8" pyobjc = { version = "^6.1", platform = "darwin" } aw-core = {git = "https://github.com/ActivityWatch/aw-core.git"} +click = "^7.1.2" [tool.poetry.dev-dependencies] mypy = "^0.761" From 01378f18ad74c5209c1fe873dc9edc6dc96c1bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sat, 25 Jul 2020 14:13:07 +0200 Subject: [PATCH 24/25] fix: fixed typing checks --- aw_qt/manager.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 6a85dd5..4eec958 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -1,10 +1,11 @@ import os import platform -from glob import glob -from time import sleep +import sys import logging import subprocess import shutil +from glob import glob +from time import sleep from typing import Optional, List, Dict, Set, Tuple import aw_core @@ -110,7 +111,7 @@ def start(self) -> None: # Create a process group, become its leader # TODO: This shouldn't go here - if platform.system() != "Windows": + if sys.platform != "win32": os.setpgrp() exec_path, location = _locate_executable(self.name) @@ -126,10 +127,10 @@ def start(self) -> None: # Don't display a console window on Windows # See: https://github.com/ActivityWatch/activitywatch/issues/212 startupinfo = None - if platform.system() == "Windows": - startupinfo = subprocess.STARTUPINFO() # type: ignore - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore - elif platform.system() == "Darwin": + if sys.platform == "win32" or sys.platform == "cygwin": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + elif sys.platform == "darwin": logger.info("macOS: Disable dock icon") import AppKit From 157a54640268dec10a4225a23b864e4d2623160a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sat, 25 Jul 2020 14:19:51 +0200 Subject: [PATCH 25/25] style: removed unused imports --- Makefile | 5 +++- aw_qt/main.py | 4 +--- aw_qt/manager.py | 5 +--- poetry.lock | 59 +++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 84dc69a..ee2346c 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,11 @@ test: test-integration: python ./tests/integration_tests.py --no-modules +lint: + poetry run flake8 aw_qt --ignore=E501,E302,E305,E231 --per-file-ignores="__init__.py:F401" + typecheck: - mypy aw_qt --strict --pretty + poetry run mypy aw_qt --strict --pretty precommit: make typecheck diff --git a/aw_qt/main.py b/aw_qt/main.py index e5c030b..97b1fc4 100644 --- a/aw_qt/main.py +++ b/aw_qt/main.py @@ -1,9 +1,7 @@ import sys import logging -import argparse import click -from typing import List, Optional -from typing_extensions import TypedDict +from typing import Optional from aw_core.log import setup_logging diff --git a/aw_qt/manager.py b/aw_qt/manager.py index 4eec958..e9bf26a 100644 --- a/aw_qt/manager.py +++ b/aw_qt/manager.py @@ -1,17 +1,14 @@ import os -import platform import sys import logging import subprocess import shutil from glob import glob from time import sleep -from typing import Optional, List, Dict, Set, Tuple +from typing import Optional, List, Dict, Tuple import aw_core -from .config import AwQtSettings - logger = logging.getLogger(__name__) _module_dir = os.path.dirname(os.path.realpath(__file__)) diff --git a/poetry.lock b/poetry.lock index 5d1cf23..3725c3f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -52,6 +52,23 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "7.1.2" +[[package]] +category = "dev" +description = "the modular source code checker: pep8 pyflakes and co" +name = "flake8" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +version = "3.8.3" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.6.0a1,<2.7.0" +pyflakes = ">=2.2.0,<2.3.0" + +[package.dependencies.importlib-metadata] +python = "<3.8" +version = "*" + [[package]] category = "main" description = "Read metadata from Python packages" @@ -98,6 +115,14 @@ version = "*" format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] +[[package]] +category = "dev" +description = "McCabe checker, plugin for flake8" +name = "mccabe" +optional = false +python-versions = "*" +version = "0.6.1" + [[package]] category = "dev" description = "Optional static typing for Python" @@ -130,6 +155,22 @@ optional = false python-versions = "*" version = "3.13.3" +[[package]] +category = "dev" +description = "Python style guide checker" +name = "pycodestyle" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.6.0" + +[[package]] +category = "dev" +description = "passive checker of Python programs" +name = "pyflakes" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.2.0" + [[package]] category = "main" description = "Python<->ObjC Interoperability Module" @@ -1931,7 +1972,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "fbb1a5688e32082aadc59ccbcc93c06865e3a574c37019900172e952306f1c34" +content-hash = "322de59b6eb412a254705ec393108b71416f56a61dc4494b78df210f856bbb79" python-versions = "^3.6, <3.8" [metadata.files] @@ -1948,6 +1989,10 @@ click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] +flake8 = [ + {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, + {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, +] importlib-metadata = [ {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, @@ -1961,6 +2006,10 @@ jsonschema = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, ] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] mypy = [ {file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, {file = "mypy-0.761-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36"}, @@ -1984,6 +2033,14 @@ mypy-extensions = [ peewee = [ {file = "peewee-3.13.3.tar.gz", hash = "sha256:1269a9736865512bd4056298003aab190957afe07d2616cf22eaf56cb6398369"}, ] +pycodestyle = [ + {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, + {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, +] +pyflakes = [ + {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, + {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, +] pyobjc = [ {file = "pyobjc-6.2.2-py3-none-any.whl", hash = "sha256:dc2e4fb96bc48770c1b5810eb65bf446c50fbf2faf6a6a398c31556b404e015c"}, {file = "pyobjc-6.2.2.tar.gz", hash = "sha256:d5b87e9fa4cc9b51bf37f9a461887e2d8b9ae7e6bb45675f8edbe35ea6770455"}, diff --git a/pyproject.toml b/pyproject.toml index 5dd0dc1..1d951f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ click = "^7.1.2" [tool.poetry.dev-dependencies] mypy = "^0.761" PyQt5-stubs = "~5.13" # Should ideally be ~5.10, but no such version exists +flake8 = "^3.8.3" [build-system] requires = ["poetry>=0.12"]