diff --git a/.gitignore b/.gitignore index af7e0a22088e133df1c0c025b13bae90463c9ccb..835ab56e4300811c29dae5bcd7cccf475c2c17ef 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .cache/ .coverage +.mypy_cache/ /flamenco_worker.egg-info/ /flamenco-worker.db diff --git a/Pipfile b/Pipfile index 994ac947c2120ba55a724d421a4341a027a68362..531fd7ca5ed0d4fb1999db0fced2ff38bdf5c0c0 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,7 @@ wheel = "*" pyinstaller = "*" ipython = "*" pathlib2 = {version = "*", markers="python_version < '3.6'"} +mypy = "*" [requires] python_version = "3.5" diff --git a/Pipfile.lock b/Pipfile.lock index 31e2c28ccca6c60a1b5ece07f2e7214b6ae850cd..cbb38b1488283f7eee45dbd3afca6a9503ed87f4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ef7dc91825f95fb13543a1da77acb02ef4066d89922eed4d7e962b0a57032307" + "sha256": "e426596954522d58f3248f5f4651067486da5aff3799d5568f8843289fdc4de8" }, "pipfile-spec": 6, "requires": { @@ -192,6 +192,21 @@ ], "version": "==4.3.0" }, + "mypy": { + "hashes": [ + "sha256:8e071ec32cc226e948a34bbb3d196eb0fd96f3ac69b6843a5aff9bd4efa14455", + "sha256:fb90c804b84cfd8133d3ddfbd630252694d11ccc1eb0166a1b2efb5da37ecab2" + ], + "index": "pypi", + "version": "==0.641" + }, + "mypy-extensions": { + "hashes": [ + "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", + "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e" + ], + "version": "==0.4.1" + }, "parso": { "hashes": [ "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", @@ -274,11 +289,11 @@ }, "pytest": { "hashes": [ - "sha256:3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec", - "sha256:e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660" + "sha256:488c842647bbeb350029da10325cb40af0a9c7a2fdda45aeb1dda75b60048ffb", + "sha256:c055690dfefa744992f563e8c3a654089a6aa5b8092dded9b6fafbd70b2e45a7" ], "index": "pypi", - "version": "==3.10.1" + "version": "==4.0.0" }, "pytest-cov": { "hashes": [ @@ -302,6 +317,34 @@ ], "version": "==4.3.2" }, + "typed-ast": { + "hashes": [ + "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", + "sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", + "sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", + "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", + "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", + "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", + "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", + "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", + "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", + "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", + "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", + "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", + "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", + "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", + "sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", + "sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", + "sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", + "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", + "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", + "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", + "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", + "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", + "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" + ], + "version": "==1.1.0" + }, "wcwidth": { "hashes": [ "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", diff --git a/flamenco_worker/commands.py b/flamenco_worker/commands.py index 33f7f6dcda93ef5921a30414536759adbd29f7d8..52309aa5eea8df7bb6969962618bbf318922c0e9 100644 --- a/flamenco_worker/commands.py +++ b/flamenco_worker/commands.py @@ -20,7 +20,7 @@ import psutil from . import worker -command_handlers = {} +command_handlers = {} # type: typing.Mapping[str, typing.Type['AbstractCommand']] # Timeout of subprocess.stdout.readline() call. SUBPROC_READLINE_TIMEOUT = 3600 # seconds @@ -82,8 +82,8 @@ class AbstractCommand(metaclass=abc.ABCMeta): # Set by __attr_post_init__() identifier = attr.ib(default=None, init=False, validator=attr.validators.optional(attr.validators.instance_of(str))) - _log = attr.ib(default=None, init=False, - validator=attr.validators.optional(attr.validators.instance_of(logging.Logger))) + _log = attr.ib(init=False, default=logging.getLogger('AbstractCommand'), + validator=attr.validators.instance_of(logging.Logger)) def __attrs_post_init__(self): self.identifier = '%s.(task_id=%s, command_idx=%s)' % ( @@ -174,8 +174,8 @@ class AbstractCommand(metaclass=abc.ABCMeta): return None - def _setting(self, settings: dict, key: str, is_required: bool, valtype: typing.Type = str) -> ( - typing.Any, typing.Optional[str]): + def _setting(self, settings: dict, key: str, is_required: bool, valtype: typing.Type = str) \ + -> typing.Tuple[typing.Any, typing.Optional[str]]: """Parses a setting, returns either (value, None) or (None, errormsg)""" try: @@ -274,10 +274,10 @@ def _unique_path(path: Path) -> Path: suffix = m.group(1) try: - suffix = int(suffix) + suffix_value = int(suffix) except ValueError: continue - max_nr = max(max_nr, suffix) + max_nr = max(max_nr, suffix_value) return path.with_name(path.name + '~%i' % (max_nr + 1)) @@ -472,20 +472,22 @@ class AbstractSubprocessCommand(AbstractCommand): pidfile.write(str(pid)) try: + assert self.proc.stdout is not None + while not self.proc.stdout.at_eof(): try: - line = await asyncio.wait_for(self.proc.stdout.readline(), - self.readline_timeout) + line_bytes = await asyncio.wait_for(self.proc.stdout.readline(), + self.readline_timeout) except asyncio.TimeoutError: raise CommandExecutionError('Command pid=%d timed out after %i seconds' % (pid, self.readline_timeout)) - if len(line) == 0: + if len(line_bytes) == 0: # EOF received, so let's bail. break try: - line = line.decode('utf8') + line = line_bytes.decode('utf8') except UnicodeDecodeError as ex: await self.abort() raise CommandExecutionError( @@ -493,9 +495,9 @@ class AbstractSubprocessCommand(AbstractCommand): line = line.rstrip() self._log.debug('Read line pid=%d: %s', pid, line) - line = await self.process_line(line) - if line is not None: - await self.worker.register_log(line) + processed_line = await self.process_line(line) + if processed_line is not None: + await self.worker.register_log(processed_line) retcode = await self.proc.wait() self._log.info('Command %r (pid=%d) stopped with status code %s', args, pid, retcode) diff --git a/flamenco_worker/config.py b/flamenco_worker/config.py index a8523c3061210b720b047ec01c0fe7e5bf2c7b19..6b75d9fad29b85e2643327f997a4aab43152531c 100644 --- a/flamenco_worker/config.py +++ b/flamenco_worker/config.py @@ -5,6 +5,7 @@ import configparser import datetime import pathlib import logging +import typing from . import worker @@ -28,7 +29,7 @@ DEFAULT_CONFIG = { ('push_act_max_interval_seconds', str(worker.PUSH_ACT_MAX_INTERVAL.total_seconds())), ]), 'pre_task_check': collections.OrderedDict([]), -} +} # type: typing.Mapping[str, typing.Mapping[str, typing.Any]] # Will be assigned to the config key 'task_types' when started with --test CLI arg. TESTING_TASK_TYPES = 'test-blender-render' @@ -53,8 +54,8 @@ class ConfigParser(configparser.ConfigParser): secs = self.value(key, float) return datetime.timedelta(seconds=secs) - def erase(self, key: str) -> bool: - return self.set(CONFIG_SECTION, key, '') + def erase(self, key: str) -> None: + self.set(CONFIG_SECTION, key, '') def merge_with_home_config(new_conf: dict): @@ -93,12 +94,12 @@ def load_config(config_file: pathlib.Path = None, if config_file: log.info('Loading configuration from %s', config_file) if not config_file.exists(): - log.fatal('Config file %s does not exist', config_file) + log.error('Config file %s does not exist', config_file) raise SystemExit(47) loaded = confparser.read(str(config_file), encoding='utf8') else: if not GLOBAL_CONFIG_FILE.exists(): - log.fatal('Config file %s does not exist', GLOBAL_CONFIG_FILE) + log.error('Config file %s does not exist', GLOBAL_CONFIG_FILE) raise SystemExit(47) config_files = [GLOBAL_CONFIG_FILE, HOME_CONFIG_FILE] diff --git a/flamenco_worker/ssdp_discover.py b/flamenco_worker/ssdp_discover.py index c1f6c18db00391004069d23bb1db559a89527fd5..29e6c9a80b7db289b84860811a494cf8b608fdce 100644 --- a/flamenco_worker/ssdp_discover.py +++ b/flamenco_worker/ssdp_discover.py @@ -30,9 +30,16 @@ class Response(HTTPResponse): self.fp = BytesIO(payload) self.debuglevel = 0 self.strict = 0 - self.headers = self.msg = None + + # This is also done in the HTTPResponse __init__ function, but + # MyPy still doesn't like it. + self.headers = self.msg = None # type: ignore + self._method = None - self.begin() + + # This function is available on the superclass, but still + # MyPy doesn't think it is. + self.begin() # type: ignore def interface_addresses(): diff --git a/flamenco_worker/tz.py b/flamenco_worker/tz.py index 25ea8c9bed931d50b9001075f38f7c322a2d1f74..4bc360d8e525d59b23be351915f5b0d6b66c4fac 100644 --- a/flamenco_worker/tz.py +++ b/flamenco_worker/tz.py @@ -22,7 +22,9 @@ class tzutc(datetime.tzinfo): return True - __hash__ = None + # Assigning a different type than object.__hash__ (None resp. Callable) + # is not allowed by MyPy. Here it's intentional, though. + __hash__ = None # type: ignore def __ne__(self, other): return not (self == other) diff --git a/flamenco_worker/upstream_update_queue.py b/flamenco_worker/upstream_update_queue.py index 18999f555d09af767eb9c0f4496acd2ab962ab30..e33e3ea57363b99110169ef79a6aa310c4b0cf8d 100644 --- a/flamenco_worker/upstream_update_queue.py +++ b/flamenco_worker/upstream_update_queue.py @@ -98,6 +98,7 @@ class TaskUpdateQueue: if self._db is None: self._connect_db() + assert self._db is not None result = self._db.execute(''' SELECT rowid, url, payload @@ -114,6 +115,7 @@ class TaskUpdateQueue: """Return the number of items queued.""" if self._db is None: self._connect_db() + assert self._db is not None result = self._db.execute('SELECT count(*) FROM fworker_queue') count = next(result)[0] diff --git a/flamenco_worker/worker.py b/flamenco_worker/worker.py index 02fc46e7b024091f2203a09a066f062bf36e0999..4f10e5bcb45ce62e26ce71f4f8d17a9ddb9e3a85 100644 --- a/flamenco_worker/worker.py +++ b/flamenco_worker/worker.py @@ -4,9 +4,11 @@ import enum import itertools import pathlib import tempfile +import traceback import typing import attr +import requests.exceptions from . import attrs_extra from . import documents @@ -184,8 +186,6 @@ class FlamencoWorker: async def _keep_posting_to_manager(self, url: str, json: dict, *, use_auth=True, may_retry_loop: bool): - import requests - post_kwargs = { 'json': json, 'loop': self.loop, @@ -370,9 +370,6 @@ class FlamencoWorker: :param delay: waits this many seconds before fetching a task. """ - import traceback - import requests - self.state = WorkerState.AWAKE self._cleanup_state_for_new_task()