From 2dd00a8f0859f255f06623fcf229f66440d9cfd5 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sat, 23 Sep 2023 18:25:19 -0700 Subject: [PATCH] *: Fix all typing hint related errors - Try to mark class variables in component classes. - Leave typing hints generic, such as 'list' and 'dict' where content is usually not filled, too complex, or context is unimportant. - backups: Handle failure for tarfile extraction so that methods are not called on potentially None valued variables. - backups: Prevent potentially passing a keyword argument twice. - dynamicdns: Deal properly with outcome of urlparsing. - ejabberd: Deal with failed regex match - email: Fix a mypy compliant when iterating a filtered list. - tor: Don't reuse variables for different typed values. - tor: Don't reuse variables for different typed values. - operation: Return None explicitly. - operation: Ensure that keyword argument is not repeated. Tests: - Where only typing hints were modified and no syntax error came up, additional testing was not done. - `mypy --ignore-missing-imports .` run successfully. - Generate developer documentation. - Service runs without errors upon start up. - backups: Listing and restoring specific apps from a backup works. - backups: Mounting a remote backup repository works. - NOT TESTED: dynamicdns: Migrating from old style configuration works. - ejabberd: Verify that setting coturn configuration works. - email: Test that showing configuration from postfix works. - tor: Orport value is properly shown. - transmission: Configuration values are properly set. - users: Running unit tests as root works. - operation: Operation status messages are show properly during app install. - ./setup.py install runs Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- doc/dev/conf.py | 2 +- plinth/app.py | 10 ++++++---- plinth/frontpage.py | 3 ++- plinth/menu.py | 4 +++- plinth/migrations/0001_initial.py | 2 +- plinth/modules/apache/urls.py | 2 +- plinth/modules/avahi/manifest.py | 2 +- plinth/modules/backups/manifest.py | 2 +- plinth/modules/backups/privileged.py | 13 +++++++++---- plinth/modules/backups/repository.py | 4 ++-- plinth/modules/cockpit/manifest.py | 2 +- plinth/modules/coturn/components.py | 15 +++++++-------- plinth/modules/diagnostics/manifest.py | 2 +- plinth/modules/dynamicdns/privileged.py | 9 +++++---- plinth/modules/ejabberd/privileged.py | 8 ++++++-- plinth/modules/email/postfix.py | 5 +++-- plinth/modules/email/privileged/postfix.py | 4 ++-- plinth/modules/firewall/__init__.py | 2 +- plinth/modules/firewall/components.py | 3 ++- plinth/modules/firewall/manifest.py | 2 +- plinth/modules/janus/manifest.py | 2 +- plinth/modules/jsxc/manifest.py | 2 +- plinth/modules/letsencrypt/components.py | 3 ++- plinth/modules/matrixsynapse/__init__.py | 3 +-- plinth/modules/names/components.py | 6 ++++-- plinth/modules/names/manifest.py | 2 +- plinth/modules/pagekite/privileged.py | 5 ++--- plinth/modules/performance/manifest.py | 2 +- plinth/modules/power/manifest.py | 2 +- plinth/modules/privoxy/manifest.py | 2 +- plinth/modules/storage/manifest.py | 2 +- plinth/modules/storage/udisks2.py | 10 +++++----- plinth/modules/tor/privileged.py | 7 +++++-- plinth/modules/transmission/privileged.py | 9 +++++---- plinth/modules/users/components.py | 3 ++- plinth/modules/users/tests/test_privileged.py | 12 +++++------- plinth/modules/wireguard/privileged.py | 2 +- plinth/operation.py | 12 +++++++----- plinth/settings.py | 2 +- plinth/tests/test_utils.py | 2 +- plinth/views.py | 7 ++++--- plinth/web_server.py | 3 ++- setup.py | 2 +- 43 files changed, 111 insertions(+), 87 deletions(-) diff --git a/doc/dev/conf.py b/doc/dev/conf.py index 42deaba26..27fd9e7d7 100644 --- a/doc/dev/conf.py +++ b/doc/dev/conf.py @@ -111,7 +111,7 @@ htmlhelp_basename = 'FreedomBoxdoc' # -- Options for LaTeX output ------------------------------------------------ -latex_elements = { +latex_elements: dict = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', diff --git a/plinth/app.py b/plinth/app.py index 5e294ff06..a7762d2bd 100644 --- a/plinth/app.py +++ b/plinth/app.py @@ -7,6 +7,7 @@ import collections import enum import inspect import logging +from typing import ClassVar from plinth import cfg from plinth.signals import post_app_loading @@ -39,14 +40,15 @@ class App: """ - app_id = None + app_id: str | None = None - can_be_disabled = True + can_be_disabled: bool = True - locked = False # Whether user interaction with the app is allowed. + locked: bool = False # Whether user interaction with the app is allowed. # XXX: Lockdown the application UI by implementing a middleware - _all_apps = collections.OrderedDict() + _all_apps: ClassVar[collections.OrderedDict[ + str, 'App']] = collections.OrderedDict() class SetupState(enum.Enum): """Various states of app being setup.""" diff --git a/plinth/frontpage.py b/plinth/frontpage.py index 012ba3c75..08508b6c7 100644 --- a/plinth/frontpage.py +++ b/plinth/frontpage.py @@ -4,6 +4,7 @@ import json import logging import pathlib +from typing import ClassVar from plinth import app, cfg from plinth.modules.users import privileged as users_privileged @@ -14,7 +15,7 @@ logger = logging.getLogger(__name__) class Shortcut(app.FollowerComponent): """An application component for handling shortcuts.""" - _all_shortcuts = {} + _all_shortcuts: ClassVar[dict[str, 'Shortcut']] = {} def __init__(self, component_id, name, short_description=None, icon=None, url=None, description=None, manual_page=None, diff --git a/plinth/menu.py b/plinth/menu.py index 114ff9a64..fb1a561fa 100644 --- a/plinth/menu.py +++ b/plinth/menu.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later +from typing import ClassVar + from django.urls import reverse_lazy from plinth import app @@ -8,7 +10,7 @@ from plinth import app class Menu(app.FollowerComponent): """Component to manage a single menu item.""" - _all_menus = set() + _all_menus: ClassVar[set['Menu']] = set() def __init__(self, component_id, name=None, short_description=None, icon=None, url_name=None, url_args=None, url_kwargs=None, diff --git a/plinth/migrations/0001_initial.py b/plinth/migrations/0001_initial.py index 88ad4c7b1..e5d1d01ca 100644 --- a/plinth/migrations/0001_initial.py +++ b/plinth/migrations/0001_initial.py @@ -17,7 +17,7 @@ class Migration(migrations.Migration): initial = True - dependencies = [] + dependencies: list = [] operations = [ migrations.CreateModel( diff --git a/plinth/modules/apache/urls.py b/plinth/modules/apache/urls.py index e018e02b8..5bf690c8c 100644 --- a/plinth/modules/apache/urls.py +++ b/plinth/modules/apache/urls.py @@ -3,4 +3,4 @@ URLs for the Apache module. """ -urlpatterns = [] +urlpatterns: list = [] diff --git a/plinth/modules/avahi/manifest.py b/plinth/modules/avahi/manifest.py index 1a87951f7..71716c490 100644 --- a/plinth/modules/avahi/manifest.py +++ b/plinth/modules/avahi/manifest.py @@ -7,4 +7,4 @@ Application manifest for avahi. # /etc/avahi/services. Currently, we don't intend to make that customizable. # There is no necessity for backup and restore. This manifest will ensure that # avahi enable/disable setting is preserved. -backup = {} +backup: dict = {} diff --git a/plinth/modules/backups/manifest.py b/plinth/modules/backups/manifest.py index f78adbd55..6d15b059b 100644 --- a/plinth/modules/backups/manifest.py +++ b/plinth/modules/backups/manifest.py @@ -6,4 +6,4 @@ Application manifest for backups. # Currently, backup application does not have any settings. However, settings # such as scheduler settings, backup location, secrets to connect to remove # servers need to be backed up. -backup = {} +backup: dict = {} diff --git a/plinth/modules/backups/privileged.py b/plinth/modules/backups/privileged.py index 429eedc75..d43cc4f6e 100644 --- a/plinth/modules/backups/privileged.py +++ b/plinth/modules/backups/privileged.py @@ -31,7 +31,7 @@ def mount(mountpoint: str, remote_path: str, ssh_keyfile: Optional[str] = None, except AlreadyMountedError: return - kwargs = {} + input_ = None # the shell would expand ~/ to the local home directory remote_path = remote_path.replace('~/', '').replace('~', '') # 'reconnect', 'ServerAliveInternal' and 'ServerAliveCountMax' allow the @@ -55,9 +55,9 @@ def mount(mountpoint: str, remote_path: str, ssh_keyfile: Optional[str] = None, if not password: raise ValueError('mount requires either a password or ssh_keyfile') cmd += ['-o', 'password_stdin'] - kwargs['input'] = password.encode() + input_ = password.encode() - subprocess.run(cmd, check=True, timeout=TIMEOUT, **kwargs) + subprocess.run(cmd, check=True, timeout=TIMEOUT, input=input_) @privileged @@ -266,7 +266,12 @@ def get_exported_archive_apps(path: str) -> list[str]: for name in filenames: if 'var/lib/plinth/backups-manifests/' in name \ and name.endswith('.json'): - manifest_data = tar_handle.extractfile(name).read() + file_handle = tar_handle.extractfile(name) + if not file_handle: + raise RuntimeError( + 'Unable to extract app manifest from backup file.') + + manifest_data = file_handle.read() manifest = json.loads(manifest_data) break diff --git a/plinth/modules/backups/repository.py b/plinth/modules/backups/repository.py index 9fc7101f3..026d8f5f7 100644 --- a/plinth/modules/backups/repository.py +++ b/plinth/modules/backups/repository.py @@ -72,9 +72,9 @@ KNOWN_ERRORS = [ class BaseBorgRepository(abc.ABC): """Base class for all kinds of Borg repositories.""" - flags = {} + flags: dict[str, bool] = {} is_mounted = True - known_credentials = [] + known_credentials: list[str] = [] def __init__(self, path, credentials=None, uuid=None, schedule=None, **kwargs): diff --git a/plinth/modules/cockpit/manifest.py b/plinth/modules/cockpit/manifest.py index f7bcbeec6..427a0167f 100644 --- a/plinth/modules/cockpit/manifest.py +++ b/plinth/modules/cockpit/manifest.py @@ -17,4 +17,4 @@ clients = [{ # triggered on every Plinth domain change (and cockpit application install) and # will set the value of allowed domains correctly. This is the only key the is # customized in cockpit.conf. -backup = {} +backup: dict = {} diff --git a/plinth/modules/coturn/components.py b/plinth/modules/coturn/components.py index 8489a8a63..1e8a83fba 100644 --- a/plinth/modules/coturn/components.py +++ b/plinth/modules/coturn/components.py @@ -2,8 +2,6 @@ """App component for other apps to manage their STUN/TURN server configuration. """ -from __future__ import annotations # Can be removed in Python 3.10 - import base64 import hashlib import hmac @@ -11,6 +9,7 @@ import json import re from dataclasses import dataclass, field from time import time +from typing import ClassVar, Iterable from plinth import app @@ -36,9 +35,9 @@ class TurnConfiguration: that must be used by a STUN/TURN client after advice from the server. """ - domain: str = None + domain: str | None = None uris: list[str] = field(default_factory=list) - shared_secret: str = None + shared_secret: str | None = None def __post_init__(self): """Generate URIs after object initialization if necessary.""" @@ -76,8 +75,8 @@ class UserTurnConfiguration(TurnConfiguration): time. """ - username: str = None - credential: str = None + username: str | None = None + credential: str | None = None def to_json(self) -> str: """Return a JSON representation of the configuration.""" @@ -102,7 +101,7 @@ class TurnConsumer(app.FollowerComponent): """ - _all = {} + _all: ClassVar[dict[str, 'TurnConsumer']] = {} def __init__(self, component_id): """Initialize the component. @@ -115,7 +114,7 @@ class TurnConsumer(app.FollowerComponent): self._all[component_id] = self @classmethod - def list(cls) -> list[TurnConsumer]: # noqa + def list(cls) -> Iterable['TurnConsumer']: """Return a list of all Coturn components.""" return cls._all.values() diff --git a/plinth/modules/diagnostics/manifest.py b/plinth/modules/diagnostics/manifest.py index 5b39e5f2d..a6577788c 100644 --- a/plinth/modules/diagnostics/manifest.py +++ b/plinth/modules/diagnostics/manifest.py @@ -3,4 +3,4 @@ Application manifest for diagnostics. """ -backup = {} +backup: dict = {} diff --git a/plinth/modules/dynamicdns/privileged.py b/plinth/modules/dynamicdns/privileged.py index 79d618a33..f5e8c090f 100644 --- a/plinth/modules/dynamicdns/privileged.py +++ b/plinth/modules/dynamicdns/privileged.py @@ -3,6 +3,7 @@ import pathlib import urllib +from typing import Any from plinth.actions import privileged @@ -30,7 +31,7 @@ def _read_configuration(path, separator='='): @privileged -def export_config() -> dict[str, object]: +def export_config() -> dict[str, bool | dict[str, dict[str, str | None]]]: """Return the old ez-ipupdate configuration in JSON format.""" input_config = {} if _active_config.exists(): @@ -68,12 +69,12 @@ def export_config() -> dict[str, object]: update_url = domain['update_url'] try: server = urllib.parse.urlparse(update_url).netloc - service_types = { + service_types: dict[str, str] = { 'dynupdate.noip.com': 'noip.com', 'dynupdate.no-ip.com': 'noip.com', 'freedns.afraid.org': 'freedns.afraid.org' } - domain['service_type'] = service_types.get(server, 'other') + domain['service_type'] = service_types.get(str(server), 'other') except ValueError: pass @@ -86,7 +87,7 @@ def export_config() -> dict[str, object]: and _active_config.exists()): enabled = True - output_config = {'enabled': enabled, 'domains': {}} + output_config: dict[str, Any] = {'enabled': enabled, 'domains': {}} if domain['domain']: output_config['domains'][domain['domain']] = domain diff --git a/plinth/modules/ejabberd/privileged.py b/plinth/modules/ejabberd/privileged.py index 118ad299e..e4dd971ce 100644 --- a/plinth/modules/ejabberd/privileged.py +++ b/plinth/modules/ejabberd/privileged.py @@ -28,7 +28,7 @@ MOD_IRC_DEPRECATED_VERSION = Version('18.06') yaml = YAML() yaml.allow_duplicate_keys = True -yaml.preserve_quotes = True +yaml.preserve_quotes = True # type: ignore [assignment] TURN_URI_REGEX = r'(stun|turn):(.*):([0-9]{4})\?transport=(tcp|udp)' @@ -286,7 +286,11 @@ def mam(command: str) -> Optional[bool]: def _generate_service(uri: str) -> dict: """Generate ejabberd mod_stun_disco service config from Coturn URI.""" pattern = re.compile(TURN_URI_REGEX) - typ, domain, port, transport = pattern.match(uri).groups() + match = pattern.match(uri) + if not match: + raise ValueError('URL does not match TURN URI') + + typ, domain, port, transport = match.groups() return { "host": domain, "port": int(port), diff --git a/plinth/modules/email/postfix.py b/plinth/modules/email/postfix.py index a23c3153a..1a04731cb 100644 --- a/plinth/modules/email/postfix.py +++ b/plinth/modules/email/postfix.py @@ -23,7 +23,7 @@ class Service: # NOQA, pylint: disable=too-many-instance-attributes wakeup: str maxproc: str command: str - options: str + options: dict[str, str] def __str__(self) -> str: parts = [ @@ -49,7 +49,8 @@ def get_config(keys: list) -> dict: output = _run(args) result = {} - for line in filter(None, output.split('\n')): + lines: list[str] = list(filter(None, output.split('\n'))) + for line in lines: key, sep, value = line.partition('=') if not sep: raise ValueError('Invalid output detected') diff --git a/plinth/modules/email/privileged/postfix.py b/plinth/modules/email/privileged/postfix.py index be479c440..dde893141 100644 --- a/plinth/modules/email/privileged/postfix.py +++ b/plinth/modules/email/privileged/postfix.py @@ -32,7 +32,7 @@ default_config = { ]) } -submission_options = { +submission_options: dict[str, str] = { 'syslog_name': 'postfix/submission', 'smtpd_tls_security_level': 'encrypt', 'smtpd_client_restrictions': 'permit_sasl_authenticated,reject', @@ -43,7 +43,7 @@ submission_service = postconf.Service(service='submission', type_='inet', wakeup='-', maxproc='-', command='smtpd', options=submission_options) -smtps_options = { +smtps_options: dict[str, str] = { 'syslog_name': 'postfix/smtps', 'smtpd_tls_wrappermode': 'yes', 'smtpd_sasl_auth_enable': 'yes', diff --git a/plinth/modules/firewall/__init__.py b/plinth/modules/firewall/__init__.py index a4c2161ee..f390aa369 100644 --- a/plinth/modules/firewall/__init__.py +++ b/plinth/modules/firewall/__init__.py @@ -27,7 +27,7 @@ _description = [ 'security threat from the Internet.'), box_name=cfg.box_name) ] -_port_details = {} +_port_details: dict[str, list[str]] = {} logger = logging.getLogger(__name__) diff --git a/plinth/modules/firewall/components.py b/plinth/modules/firewall/components.py index 2bac48bd6..b3e266d97 100644 --- a/plinth/modules/firewall/components.py +++ b/plinth/modules/firewall/components.py @@ -5,6 +5,7 @@ App component for other apps to use firewall functionality. import logging import re +from typing import ClassVar from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ @@ -18,7 +19,7 @@ logger = logging.getLogger(__name__) class Firewall(app.FollowerComponent): """Component to open/close firewall ports for an app.""" - _all_firewall_components = {} + _all_firewall_components: ClassVar[dict[str, 'Firewall']] = {} def __init__(self, component_id, name=None, ports=None, is_external=False): """Initialize the firewall component.""" diff --git a/plinth/modules/firewall/manifest.py b/plinth/modules/firewall/manifest.py index f8bd7bbfc..e666aa9bd 100644 --- a/plinth/modules/firewall/manifest.py +++ b/plinth/modules/firewall/manifest.py @@ -3,4 +3,4 @@ Application manifest for firewall. """ -backup = {} +backup: dict = {} diff --git a/plinth/modules/janus/manifest.py b/plinth/modules/janus/manifest.py index 53aa2a9cb..467c65002 100644 --- a/plinth/modules/janus/manifest.py +++ b/plinth/modules/janus/manifest.py @@ -11,4 +11,4 @@ clients = [{ }] }] -backup = {} +backup: dict = {} diff --git a/plinth/modules/jsxc/manifest.py b/plinth/modules/jsxc/manifest.py index 9898d5e1d..0eb350b7c 100644 --- a/plinth/modules/jsxc/manifest.py +++ b/plinth/modules/jsxc/manifest.py @@ -11,4 +11,4 @@ clients = [{ }] }] -backup = {} +backup: dict = {} diff --git a/plinth/modules/letsencrypt/components.py b/plinth/modules/letsencrypt/components.py index f951277fa..762d28def 100644 --- a/plinth/modules/letsencrypt/components.py +++ b/plinth/modules/letsencrypt/components.py @@ -4,6 +4,7 @@ import logging import pathlib import threading +from typing import ClassVar from plinth import app from plinth.modules.names.components import DomainName @@ -38,7 +39,7 @@ class LetsEncrypt(app.FollowerComponent): """ - _all = {} + _all: ClassVar[dict[str, 'LetsEncrypt']] = {} def __init__(self, component_id, domains=None, daemons=None, should_copy_certificates=False, private_key_path=None, diff --git a/plinth/modules/matrixsynapse/__init__.py b/plinth/modules/matrixsynapse/__init__.py index de16cc499..4480adecd 100644 --- a/plinth/modules/matrixsynapse/__init__.py +++ b/plinth/modules/matrixsynapse/__init__.py @@ -3,7 +3,6 @@ import logging import os -from typing import List from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ @@ -191,7 +190,7 @@ def get_configured_domain_name(): return config['server_name'] -def get_turn_configuration() -> (List[str], str, bool): +def get_turn_configuration() -> tuple[TurnConfiguration, bool]: """Return TurnConfiguration if setup else empty.""" for file_path, managed in ((privileged.OVERRIDDEN_TURN_CONF_PATH, False), (privileged.TURN_CONF_PATH, True)): diff --git a/plinth/modules/names/components.py b/plinth/modules/names/components.py index b9dc82797..bebf9941d 100644 --- a/plinth/modules/names/components.py +++ b/plinth/modules/names/components.py @@ -3,6 +3,8 @@ App component to introduce a new domain type. """ +from typing import ClassVar + from django.utils.translation import gettext_lazy as _ from plinth import app @@ -39,7 +41,7 @@ class DomainType(app.FollowerComponent): """ - _all = {} + _all: ClassVar[dict[str, 'DomainType']] = {} def __init__(self, component_id, display_name, configuration_url, can_have_certificate=True): @@ -90,7 +92,7 @@ class DomainName(app.FollowerComponent): primary reason for making a domain name available as a component. """ - _all = {} + _all: ClassVar[dict[str, 'DomainName']] = {} def __init__(self, component_id, name, domain_type, services): """Initialize a domain name. diff --git a/plinth/modules/names/manifest.py b/plinth/modules/names/manifest.py index 2d5b9ab3f..a8bdec5ae 100644 --- a/plinth/modules/names/manifest.py +++ b/plinth/modules/names/manifest.py @@ -3,4 +3,4 @@ Application manifest for names. """ -backup = {} +backup: dict = {} diff --git a/plinth/modules/pagekite/privileged.py b/plinth/modules/pagekite/privileged.py index f9b5eec69..c432a145f 100644 --- a/plinth/modules/pagekite/privileged.py +++ b/plinth/modules/pagekite/privileged.py @@ -2,7 +2,6 @@ """Configure PageKite.""" import os -from typing import Union import augeas @@ -118,7 +117,7 @@ def set_config(frontend: str, kite_name: str, kite_secret: str): @privileged -def remove_service(service: dict[str, Union[str, bool]]): +def remove_service(service: dict[str, str]): """Search and remove the service(s) that match all given parameters.""" aug = _augeas_load() service = utils.load_service(service) @@ -171,7 +170,7 @@ def _add_service(aug, service): @privileged -def add_service(service: dict[str, Union[str, bool]]): +def add_service(service: dict[str, str]): """Add one service.""" aug = _augeas_load() service = utils.load_service(service) diff --git a/plinth/modules/performance/manifest.py b/plinth/modules/performance/manifest.py index a770c1c33..ee153ee02 100644 --- a/plinth/modules/performance/manifest.py +++ b/plinth/modules/performance/manifest.py @@ -13,4 +13,4 @@ clients = [{ }] }] -backup = {} +backup: dict = {} diff --git a/plinth/modules/power/manifest.py b/plinth/modules/power/manifest.py index d958ecbac..bdacbc3d0 100644 --- a/plinth/modules/power/manifest.py +++ b/plinth/modules/power/manifest.py @@ -3,4 +3,4 @@ Application manifest for power. """ -backup = {} +backup: dict = {} diff --git a/plinth/modules/privoxy/manifest.py b/plinth/modules/privoxy/manifest.py index 87747ba2b..099231aa8 100644 --- a/plinth/modules/privoxy/manifest.py +++ b/plinth/modules/privoxy/manifest.py @@ -3,4 +3,4 @@ Application manifest for privoxy. """ -backup = {} +backup: dict = {} diff --git a/plinth/modules/storage/manifest.py b/plinth/modules/storage/manifest.py index f27f6313e..dd99aa59c 100644 --- a/plinth/modules/storage/manifest.py +++ b/plinth/modules/storage/manifest.py @@ -3,4 +3,4 @@ Application manifest for storage. """ -backup = {} +backup: dict = {} diff --git a/plinth/modules/storage/udisks2.py b/plinth/modules/storage/udisks2.py index 5784c0326..7779f35e6 100644 --- a/plinth/modules/storage/udisks2.py +++ b/plinth/modules/storage/udisks2.py @@ -14,7 +14,7 @@ gio = import_from_gi('Gio', '2.0') _DBUS_NAME = 'org.freedesktop.UDisks2' -_INTERFACES = { +_INTERFACES: dict[str, str] = { 'Ata': 'org.freedesktop.UDisks2.Drive.Ata', 'Block': 'org.freedesktop.UDisks2.Block', 'Drive': 'org.freedesktop.UDisks2.Drive', @@ -28,14 +28,14 @@ _INTERFACES = { 'UDisks2': 'org.freedesktop.UDisks2', } -_OBJECTS = { +_OBJECTS: dict[str, str] = { 'drives': '/org/freedesktop/UDisks2/drives/', 'jobs': '/org/freedesktop/UDisks2/jobs/', 'Manager': '/org/freedesktop/UDisks2/Manager', 'UDisks2': '/org/freedesktop/UDisks2', } -_ERRORS = { +_ERRORS: dict[str, str] = { 'AlreadyMounted': 'org.freedesktop.UDisks2.Error.AlreadyMounted', 'Failed': 'org.freedesktop.UDisks2.Error.Failed', } @@ -54,8 +54,8 @@ def _get_dbus_proxy(object_, interface): class Proxy: """Base methods for abstraction over UDisks2 DBus proxy objects.""" - interface = None - properties = {} + interface: str | None = None + properties: dict[str, tuple[str, str]] = {} def __init__(self, object_path): """Return an object instance.""" diff --git a/plinth/modules/tor/privileged.py b/plinth/modules/tor/privileged.py index 31906ca7a..4e6b37f10 100644 --- a/plinth/modules/tor/privileged.py +++ b/plinth/modules/tor/privileged.py @@ -254,8 +254,8 @@ def _get_ports() -> dict[str, str]: def _get_orport() -> str: """Return the ORPort by querying running instance.""" - cookie = open(TOR_AUTH_COOKIE, 'rb').read() - cookie = codecs.encode(cookie, 'hex').decode() + cookie_bytes = open(TOR_AUTH_COOKIE, 'rb').read() + cookie = codecs.encode(cookie_bytes, 'hex').decode() commands = '''AUTHENTICATE {cookie} GETINFO net/listeners/or @@ -270,6 +270,9 @@ QUIT line = response.split(b'\r\n')[1].decode() matches = re.match(r'.*=".+:(\d+)"', line) + if not matches: + raise ValueError('Invalid orport value returned by Tor') + return matches.group(1) diff --git a/plinth/modules/transmission/privileged.py b/plinth/modules/transmission/privileged.py index b9fe96483..6c5992bdd 100644 --- a/plinth/modules/transmission/privileged.py +++ b/plinth/modules/transmission/privileged.py @@ -22,12 +22,13 @@ def get_configuration() -> dict[str, str]: @privileged def merge_configuration(configuration: dict[str, Union[str, bool]]): """Merge given JSON configuration with existing configuration.""" - current_configuration = _transmission_config.read_bytes() - current_configuration = json.loads(current_configuration) + current_configuration_bytes = _transmission_config.read_bytes() + current_configuration = json.loads(current_configuration_bytes) new_configuration = current_configuration new_configuration.update(configuration) - new_configuration = json.dumps(new_configuration, indent=4, sort_keys=True) + new_configuration_bytes = json.dumps(new_configuration, indent=4, + sort_keys=True) - _transmission_config.write_text(new_configuration, encoding='utf-8') + _transmission_config.write_text(new_configuration_bytes, encoding='utf-8') action_utils.service_reload('transmission-daemon') diff --git a/plinth/modules/users/components.py b/plinth/modules/users/components.py index 1d79e412b..d0216accb 100644 --- a/plinth/modules/users/components.py +++ b/plinth/modules/users/components.py @@ -4,6 +4,7 @@ App component to manage users and groups. """ import itertools +from typing import ClassVar from plinth import app @@ -12,7 +13,7 @@ class UsersAndGroups(app.FollowerComponent): """Component to manage users and groups of an app.""" # Class variable to hold a list of user groups for apps - _all_components = set() + _all_components: ClassVar[set['UsersAndGroups']] = set() def __init__(self, component_id, reserved_usernames=[], groups={}): """Store reserved_usernames and groups of the app. diff --git a/plinth/modules/users/tests/test_privileged.py b/plinth/modules/users/tests/test_privileged.py index 0852b01f9..356c6c10c 100644 --- a/plinth/modules/users/tests/test_privileged.py +++ b/plinth/modules/users/tests/test_privileged.py @@ -16,11 +16,6 @@ from plinth import action_utils from plinth.modules.users import privileged from plinth.tests import config as test_config -pytestmark = pytest.mark.usefixtures('mock_privileged') -privileged_modules_to_mock = [ - 'plinth.modules.users.privileged', 'plinth.modules.security.privileged' -] - _cleanup_users = None _cleanup_groups = None @@ -39,10 +34,13 @@ def _is_ldap_set_up(): return False -pytestmark = [ - pytest.mark.usefixtures('needs_root', 'load_cfg'), +pytestmark: list[pytest.MarkDecorator] = [ + pytest.mark.usefixtures('needs_root', 'load_cfg', 'mock_privileged'), pytest.mark.skipif(not _is_ldap_set_up(), reason="LDAP is not configured") ] +privileged_modules_to_mock = [ + 'plinth.modules.users.privileged', 'plinth.modules.security.privileged' +] def _random_string(length=8): diff --git a/plinth/modules/wireguard/privileged.py b/plinth/modules/wireguard/privileged.py index 34a61d0f0..50dd0cb26 100644 --- a/plinth/modules/wireguard/privileged.py +++ b/plinth/modules/wireguard/privileged.py @@ -19,7 +19,7 @@ def get_info() -> dict[str, dict]: if not line: continue - fields = [ + fields: list = [ field if field != '(none)' else None for field in line.split() ] interface_name = fields[0] diff --git a/plinth/operation.py b/plinth/operation.py index ddb5f2522..1985c8e08 100644 --- a/plinth/operation.py +++ b/plinth/operation.py @@ -89,10 +89,10 @@ class Operation: return self.return_value @staticmethod - def get_operation(): + def get_operation() -> 'Operation': """Return the operation associated with this thread.""" thread = threading.current_thread() - return thread._operation + return thread._operation # type: ignore [attr-defined] def on_update(self, message: Optional[str] = None, exception: Optional[Exception] = None): @@ -106,7 +106,7 @@ class Operation: self._update_notification() @property - def message(self): + def message(self) -> str | None: """Return a message about status of the operation.""" from django.utils.translation import gettext_noop if self._message: # Progress has been set by the operation itself @@ -124,6 +124,8 @@ class Operation: if self.state == Operation.State.COMPLETED: return gettext_noop('Finished: {name}') + return None + @property def translated_message(self): """Return a message about status of operation after translating. @@ -183,8 +185,8 @@ class OperationsManager: def new(self, *args, **kwargs): """Create a new operation instance and add to global list.""" with self._lock: - operation = Operation(*args, **kwargs, - on_complete=self._on_operation_complete) + kwargs['on_complete'] = self._on_operation_complete + operation = Operation(*args, **kwargs) self._operations.append(operation) logger.info('%s: added', operation) self._schedule_next() diff --git a/plinth/settings.py b/plinth/settings.py index 6c0e905a8..c7320ff7b 100644 --- a/plinth/settings.py +++ b/plinth/settings.py @@ -124,7 +124,7 @@ LOGIN_URL = 'users:login' LOGIN_REDIRECT_URL = 'index' # Overridden before initialization -MESSAGE_TAGS = {} +MESSAGE_TAGS: dict = {} MIDDLEWARE = ( 'django.middleware.security.SecurityMiddleware', diff --git a/plinth/tests/test_utils.py b/plinth/tests/test_utils.py index ff4f0c637..c9e220433 100644 --- a/plinth/tests/test_utils.py +++ b/plinth/tests/test_utils.py @@ -92,7 +92,7 @@ class TestYAMLFileUtil: kv_pair = {'key': 'value'} yaml = ruamel.yaml.YAML() - yaml.preserve_quotes = True + yaml.preserve_quotes = True # type: ignore [assignment] def test_update_empty_yaml_file(self): """ diff --git a/plinth/views.py b/plinth/views.py index 8cb5d4c20..da37eeaea 100644 --- a/plinth/views.py +++ b/plinth/views.py @@ -9,6 +9,7 @@ import urllib.parse from django.contrib import messages from django.core.exceptions import ImproperlyConfigured +from django.forms import Form from django.http import Http404, HttpResponseBadRequest, HttpResponseRedirect from django.shortcuts import redirect from django.template.response import TemplateResponse @@ -164,9 +165,9 @@ class AppView(FormView): instead of the simple appearance provided by default. """ - form_class = None - app_id = None - template_name = 'app.html' + form_class: Form | None = None + app_id: str | None = None + template_name: str = 'app.html' def __init__(self, *args, **kwargs): """Initialize the view.""" diff --git a/plinth/web_server.py b/plinth/web_server.py index 9fcb622b0..65a9586fe 100644 --- a/plinth/web_server.py +++ b/plinth/web_server.py @@ -7,6 +7,7 @@ import logging import os import sys import warnings +from typing import ClassVar import cherrypy @@ -104,7 +105,7 @@ class StaticFiles(app_module.FollowerComponent): """ - _all_instances = {} + _all_instances: ClassVar[dict[str, 'StaticFiles']] = {} def __init__(self, component_id, directory_map=None): """Initialize the component. diff --git a/setup.py b/setup.py index ccf2aa74f..f0a002e78 100755 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ LOCALE_PATHS = ['plinth/locale'] class DjangoCommand(Command): """Setup command to run a Django management command.""" - user_options = [] + user_options: list = [] def initialize_options(self): """Declare the options for this command."""