*: Utilize newer 3.10 syntax for type hints

Tests:

- mypy does not show any errors.

- Installing ejabberd app works. Privileged actions run fine.

- Unit tests work.

- No additional testing was done as type annotations don't have any effect at
runtime.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2023-09-23 22:28:02 -07:00 committed by James Valleroy
parent a86a86d605
commit 38ece87c6c
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
29 changed files with 116 additions and 140 deletions

View File

@ -9,6 +9,7 @@ import logging
import os import os
import sys import sys
import traceback import traceback
import types
import typing import typing
import plinth.log import plinth.log
@ -166,10 +167,9 @@ def _assert_valid_type(arg_name, value, annotation):
return return
if not hasattr(annotation, '__origin__'): # 'int | str' or 'typing.Union[int, str]'
raise TypeError('Unsupported annotation type') if (isinstance(annotation, types.UnionType)
or getattr(annotation, '__origin__', None) == typing.Union):
if annotation.__origin__ == typing.Union:
for arg in annotation.__args__: for arg in annotation.__args__:
try: try:
_assert_valid_type(arg_name, value, arg) _assert_valid_type(arg_name, value, arg)
@ -179,7 +179,8 @@ def _assert_valid_type(arg_name, value, annotation):
raise TypeError(f'Expected one of unioned types for {arg_name}') raise TypeError(f'Expected one of unioned types for {arg_name}')
if annotation.__origin__ == list: # 'list[int]' or 'typing.List[int]'
if getattr(annotation, '__origin__', None) == list:
if not isinstance(value, list): if not isinstance(value, list):
raise TypeError(f'Expected type list for {arg_name}') raise TypeError(f'Expected type list for {arg_name}')
@ -189,7 +190,8 @@ def _assert_valid_type(arg_name, value, annotation):
return return
if annotation.__origin__ == dict: # 'list[dict]' or 'typing.List[dict]'
if getattr(annotation, '__origin__', None) == dict:
if not isinstance(value, dict): if not isinstance(value, dict):
raise TypeError(f'Expected type dict for {arg_name}') raise TypeError(f'Expected type dict for {arg_name}')

View File

@ -160,7 +160,6 @@ for transmission daemon. We will do this by creating a file ``privileged.py``.
import json import json
import pathlib import pathlib
from typing import Union
from plinth import action_utils from plinth import action_utils
from plinth.actions import privileged from plinth.actions import privileged
@ -175,7 +174,7 @@ for transmission daemon. We will do this by creating a file ``privileged.py``.
@privileged @privileged
def merge_configuration(configuration: dict[str, Union[str, bool]]) -> None: def merge_configuration(configuration: dict[str, str | bool]) -> None:
"""Merge given JSON configuration with existing configuration.""" """Merge given JSON configuration with existing configuration."""
current_configuration = _transmission_config.read_bytes() current_configuration = _transmission_config.read_bytes()
current_configuration = json.loads(current_configuration) current_configuration = json.loads(current_configuration)

View File

@ -8,7 +8,6 @@ import importlib
import logging import logging
import pathlib import pathlib
import re import re
from typing import Optional
import django import django
@ -142,8 +141,7 @@ def get_module_import_path(module_name: str) -> str:
return import_path return import_path
def _read_module_import_paths_from_file( def _read_module_import_paths_from_file(file_path: pathlib.Path) -> str | None:
file_path: pathlib.Path) -> Optional[str]:
"""Read a module's import path from a file.""" """Read a module's import path from a file."""
with file_path.open() as file_handle: with file_path.open() as file_handle:
for line in file_handle: for line in file_handle:

View File

@ -7,7 +7,6 @@ import pathlib
import re import re
import subprocess import subprocess
import tarfile import tarfile
from typing import Optional, Union
from plinth.actions import privileged from plinth.actions import privileged
from plinth.utils import Version from plinth.utils import Version
@ -22,8 +21,8 @@ class AlreadyMountedError(Exception):
@privileged @privileged
def mount(mountpoint: str, remote_path: str, ssh_keyfile: Optional[str] = None, def mount(mountpoint: str, remote_path: str, ssh_keyfile: str | None = None,
password: Optional[str] = None, password: str | None = None,
user_known_hosts_file: str = '/dev/null'): user_known_hosts_file: str = '/dev/null'):
"""Mount a remote ssh path via sshfs.""" """Mount a remote ssh path via sshfs."""
try: try:
@ -110,7 +109,7 @@ def setup(path: str):
def _init_repository(path: str, encryption: str, def _init_repository(path: str, encryption: str,
encryption_passphrase: Optional[str] = None): encryption_passphrase: str | None = None):
"""Initialize a local or remote borg repository.""" """Initialize a local or remote borg repository."""
if encryption != 'none': if encryption != 'none':
if not encryption_passphrase: if not encryption_passphrase:
@ -121,14 +120,13 @@ def _init_repository(path: str, encryption: str,
@privileged @privileged
def init(path: str, encryption: str, def init(path: str, encryption: str, encryption_passphrase: str | None = None):
encryption_passphrase: Optional[str] = None):
"""Initialize the borg repository.""" """Initialize the borg repository."""
_init_repository(path, encryption, encryption_passphrase) _init_repository(path, encryption, encryption_passphrase)
@privileged @privileged
def info(path: str, encryption_passphrase: Optional[str] = None) -> dict: def info(path: str, encryption_passphrase: str | None = None) -> dict:
"""Show repository information.""" """Show repository information."""
process = _run(['borg', 'info', '--json', path], encryption_passphrase, process = _run(['borg', 'info', '--json', path], encryption_passphrase,
stdout=subprocess.PIPE) stdout=subprocess.PIPE)
@ -136,7 +134,7 @@ def info(path: str, encryption_passphrase: Optional[str] = None) -> dict:
@privileged @privileged
def list_repo(path: str, encryption_passphrase: Optional[str] = None) -> dict: def list_repo(path: str, encryption_passphrase: str | None = None) -> dict:
"""List repository contents.""" """List repository contents."""
process = _run(['borg', 'list', '--json', '--format="{comment}"', path], process = _run(['borg', 'list', '--json', '--format="{comment}"', path],
encryption_passphrase, stdout=subprocess.PIPE) encryption_passphrase, stdout=subprocess.PIPE)
@ -150,8 +148,8 @@ def _get_borg_version():
@privileged @privileged
def create_archive(path: str, paths: list[str], comment: Optional[str] = None, def create_archive(path: str, paths: list[str], comment: str | None = None,
encryption_passphrase: Optional[str] = None): encryption_passphrase: str | None = None):
"""Create archive.""" """Create archive."""
existing_paths = filter(os.path.exists, paths) existing_paths = filter(os.path.exists, paths)
command = ['borg', 'create', '--json'] command = ['borg', 'create', '--json']
@ -169,7 +167,7 @@ def create_archive(path: str, paths: list[str], comment: Optional[str] = None,
@privileged @privileged
def delete_archive(path: str, encryption_passphrase: Optional[str] = None): def delete_archive(path: str, encryption_passphrase: str | None = None):
"""Delete archive.""" """Delete archive."""
_run(['borg', 'delete', path], encryption_passphrase) _run(['borg', 'delete', path], encryption_passphrase)
@ -199,7 +197,7 @@ def _extract(archive_path, destination, encryption_passphrase, locations=None):
@privileged @privileged
def export_tar(path: str, encryption_passphrase: Optional[str] = None): def export_tar(path: str, encryption_passphrase: str | None = None):
"""Export archive contents as tar stream on stdout.""" """Export archive contents as tar stream on stdout."""
_run(['borg', 'export-tar', path, '-', '--tar-filter=gzip'], _run(['borg', 'export-tar', path, '-', '--tar-filter=gzip'],
encryption_passphrase) encryption_passphrase)
@ -214,7 +212,7 @@ def _read_archive_file(archive, filepath, encryption_passphrase):
@privileged @privileged
def get_archive_apps(path: str, def get_archive_apps(path: str,
encryption_passphrase: Optional[str] = None) -> list[str]: encryption_passphrase: str | None = None) -> list[str]:
"""Get list of apps included in archive.""" """Get list of apps included in archive."""
manifest_folder = os.path.relpath(MANIFESTS_FOLDER, '/') manifest_folder = os.path.relpath(MANIFESTS_FOLDER, '/')
borg_call = [ borg_call = [
@ -286,7 +284,7 @@ def get_exported_archive_apps(path: str) -> list[str]:
@privileged @privileged
def restore_archive(archive_path: str, destination: str, def restore_archive(archive_path: str, destination: str,
directories: list[str], files: list[str], directories: list[str], files: list[str],
encryption_passphrase: Optional[str] = None): encryption_passphrase: str | None = None):
"""Restore files from an archive.""" """Restore files from an archive."""
locations_all = directories + files locations_all = directories + files
locations_all = [ locations_all = [
@ -319,8 +317,7 @@ def _assert_app_id(app_id):
@privileged @privileged
def dump_settings(app_id: str, settings: dict[str, Union[int, float, bool, def dump_settings(app_id: str, settings: dict[str, int | float | bool | str]):
str]]):
"""Dump an app's settings to a JSON file.""" """Dump an app's settings to a JSON file."""
_assert_app_id(app_id) _assert_app_id(app_id)
BACKUPS_DATA_PATH.mkdir(exist_ok=True) BACKUPS_DATA_PATH.mkdir(exist_ok=True)
@ -329,7 +326,7 @@ def dump_settings(app_id: str, settings: dict[str, Union[int, float, bool,
@privileged @privileged
def load_settings(app_id: str) -> dict[str, Union[int, float, bool, str]]: def load_settings(app_id: str) -> dict[str, int | float | bool | str]:
"""Load an app's settings from a JSON file.""" """Load an app's settings from a JSON file."""
_assert_app_id(app_id) _assert_app_id(app_id)
settings_path = BACKUPS_DATA_PATH / f'{app_id}-settings.json' settings_path = BACKUPS_DATA_PATH / f'{app_id}-settings.json'
@ -339,7 +336,7 @@ def load_settings(app_id: str) -> dict[str, Union[int, float, bool, str]]:
return {} return {}
def _get_env(encryption_passphrase: Optional[str] = None): def _get_env(encryption_passphrase: str | None = None):
"""Create encryption and ssh kwargs out of given arguments.""" """Create encryption and ssh kwargs out of given arguments."""
env = dict(os.environ, BORG_RELOCATED_REPO_ACCESS_IS_OK='yes', env = dict(os.environ, BORG_RELOCATED_REPO_ACCESS_IS_OK='yes',
LANG='C.UTF-8') LANG='C.UTF-8')

View File

@ -11,7 +11,6 @@ import secrets
import shutil import shutil
import string import string
import subprocess import subprocess
from typing import Optional
import augeas import augeas
@ -124,7 +123,7 @@ def get_configuration() -> dict[str, object]:
@privileged @privileged
def add_password(permissions: list[str], comment: Optional[str] = None): def add_password(permissions: list[str], comment: str | None = None):
"""Generate a password with given permissions.""" """Generate a password with given permissions."""
conf = conf_file_read() conf = conf_file_read()
permissions = _format_permissions(permissions) permissions = _format_permissions(permissions)

View File

@ -4,7 +4,6 @@
import os import os
import pathlib import pathlib
import subprocess import subprocess
from typing import Optional
import augeas import augeas
@ -29,7 +28,7 @@ def set_hostname(hostname: str):
@privileged @privileged
def set_domainname(domainname: Optional[str] = None): def set_domainname(domainname: str | None = None):
"""Set system domainname in /etc/hosts.""" """Set system domainname in /etc/hosts."""
hostname = subprocess.check_output(['hostname']).decode().strip() hostname = subprocess.check_output(['hostname']).decode().strip()
hosts_path = pathlib.Path('/etc/hosts') hosts_path = pathlib.Path('/etc/hosts')

View File

@ -3,7 +3,6 @@
import json import json
import logging import logging
from typing import Tuple
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -226,7 +225,7 @@ def update_turn_configuration(config: TurnConfiguration, managed=True,
privileged.configure_turn(json.loads(config.to_json()), managed) privileged.configure_turn(json.loads(config.to_json()), managed)
def get_turn_configuration() -> Tuple[TurnConfiguration, bool]: def get_turn_configuration() -> tuple[TurnConfiguration, bool]:
"""Get the latest STUN/TURN configuration.""" """Get the latest STUN/TURN configuration."""
tc, managed = privileged.get_turn_config() tc, managed = privileged.get_turn_config()
return TurnConfiguration(tc['domain'], tc['uris'], return TurnConfiguration(tc['domain'], tc['uris'],

View File

@ -8,7 +8,7 @@ import shutil
import socket import socket
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Tuple from typing import Any
from ruamel.yaml import YAML, scalarstring from ruamel.yaml import YAML, scalarstring
@ -240,7 +240,7 @@ def set_domains(domains: list[str]):
@privileged @privileged
def mam(command: str) -> Optional[bool]: def mam(command: str) -> bool | None:
"""Enable, disable, or get status of Message Archive Management (MAM).""" """Enable, disable, or get status of Message Archive Management (MAM)."""
if command not in ('enable', 'disable', 'status'): if command not in ('enable', 'disable', 'status'):
raise ValueError('Invalid command') raise ValueError('Invalid command')
@ -308,7 +308,7 @@ def _generate_uris(services: list[dict]) -> list[str]:
@privileged @privileged
def get_turn_config() -> Tuple[dict[str, Any], bool]: def get_turn_config() -> tuple[dict[str, Any], bool]:
"""Get the latest STUN/TURN configuration in JSON format.""" """Get the latest STUN/TURN configuration in JSON format."""
with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle: with open(EJABBERD_CONFIG, 'r', encoding='utf-8') as file_handle:
conf = yaml.load(file_handle) conf = yaml.load(file_handle)

View File

@ -10,7 +10,6 @@ See: https://rspamd.com/doc/modules/dkim_signing.html
""" """
from dataclasses import dataclass from dataclasses import dataclass
from typing import Union
@dataclass @dataclass
@ -19,12 +18,12 @@ class Entry: # pylint: disable=too-many-instance-attributes
type_: str type_: str
value: str value: str
domain: Union[str, None] = None domain: str | None = None
class_: str = 'IN' class_: str = 'IN'
ttl: int = 60 ttl: int = 60
priority: int = 10 priority: int = 10
weight: Union[int, None] = None weight: int | None = None
port: Union[int, None] = None port: int | None = None
def get_split_value(self): def get_split_value(self):
"""If the record is TXT and value > 255, split it.""" """If the record is TXT and value > 255, split it."""

View File

@ -9,7 +9,7 @@ import re
import shutil import shutil
import subprocess import subprocess
import time import time
from typing import Any, Optional from typing import Any
from plinth import action_utils from plinth import action_utils
from plinth.actions import privileged from plinth.actions import privileged
@ -347,7 +347,7 @@ def repo_info(name: str) -> dict[str, str]:
@privileged @privileged
def create_repo(url: Optional[str] = None, name: Optional[str] = None, def create_repo(url: str | None = None, name: str | None = None,
description: str = '', owner: str = '', description: str = '', owner: str = '',
keep_ownership: bool = False, is_private: bool = False, keep_ownership: bool = False, is_private: bool = False,
skip_prepare: bool = False, prepare_only: bool = False): skip_prepare: bool = False, prepare_only: bool = False):

View File

@ -1,8 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Configure I2P.""" """Configure I2P."""
from typing import Optional
from plinth.actions import privileged from plinth.actions import privileged
from plinth.modules.i2p.helpers import RouterEditor, TunnelEditor from plinth.modules.i2p.helpers import RouterEditor, TunnelEditor
@ -19,8 +17,8 @@ def set_tunnel_property(name: str, property_: str, value: str):
@privileged @privileged
def add_favorite(name: str, url: str, description: Optional[str], def add_favorite(name: str, url: str, description: str | None,
icon: Optional[str]): icon: str | None):
"""Add a favorite to router.config.""" """Add a favorite to router.config."""
editor = RouterEditor() editor = RouterEditor()
editor.read_conf().add_favorite(name, url, description, icon).write_conf() editor.read_conf().add_favorite(name, url, description, icon).write_conf()

View File

@ -6,7 +6,6 @@ import pathlib
import re import re
import shutil import shutil
import subprocess import subprocess
from typing import Tuple
from plinth.actions import privileged from plinth.actions import privileged
@ -43,7 +42,7 @@ def _get_title(site):
@privileged @privileged
def get_sites() -> list[Tuple[str, str]]: def get_sites() -> list[tuple[str, str]]:
"""Get wikis and blogs.""" """Get wikis and blogs."""
sites = [] sites = []
if os.path.exists(SITE_PATH): if os.path.exists(SITE_PATH):

View File

@ -7,7 +7,6 @@ import json
import os import os
import pathlib import pathlib
import shutil import shutil
from typing import Dict, List, Optional, Union
import requests import requests
import yaml import yaml
@ -102,7 +101,7 @@ def get_config():
@privileged @privileged
def set_config(public_registration: bool, def set_config(public_registration: bool,
registration_verification: Optional[str] = None): registration_verification: str | None = None):
"""Enable/disable public user registration.""" """Enable/disable public user registration."""
if registration_verification == 'token': if registration_verification == 'token':
_create_registration_token() _create_registration_token()
@ -243,7 +242,7 @@ def _get_access_token() -> str:
@privileged @privileged
def list_registration_tokens() -> List[Dict[str, Optional[Union[str, int]]]]: def list_registration_tokens() -> list[dict[str, str | int | None]]:
"""Return the current list of registration tokens.""" """Return the current list of registration tokens."""
if not action_utils.service_is_running('matrix-synapse'): if not action_utils.service_is_running('matrix-synapse'):
return [] return []
@ -262,7 +261,7 @@ def _get_headers(access_token: str):
def _list_registration_tokens( def _list_registration_tokens(
access_token: str) -> List[Dict[str, Optional[Union[str, int]]]]: access_token: str) -> list[dict[str, str | int | None]]:
"""Use Admin API to fetch the list of registration tokens. """Use Admin API to fetch the list of registration tokens.
For details on registration tokens API, see: For details on registration tokens API, see:

View File

@ -1,8 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
"""Configure Minetest server.""" """Configure Minetest server."""
from typing import Optional
import augeas import augeas
from plinth import action_utils from plinth import action_utils
@ -13,10 +11,9 @@ AUG_PATH = '/files' + CONFIG_FILE + '/.anon'
@privileged @privileged
def configure(max_players: Optional[int] = None, def configure(max_players: int | None = None, enable_pvp: bool | None = None,
enable_pvp: Optional[bool] = None, creative_mode: bool | None = None,
creative_mode: Optional[bool] = None, enable_damage: bool | None = None):
enable_damage: Optional[bool] = None):
"""Update configuration file and restart daemon if necessary.""" """Update configuration file and restart daemon if necessary."""
aug = load_augeas() aug = load_augeas()
if max_players is not None: if max_players is not None:

View File

@ -5,7 +5,6 @@ Configure Mumble server.
import pathlib import pathlib
import subprocess import subprocess
from typing import Optional
import augeas import augeas
@ -33,7 +32,7 @@ def set_super_user_password(password: str):
@privileged @privileged
def get_domain() -> Optional[str]: def get_domain() -> str | None:
"""Return domain name set in mumble or empty string.""" """Return domain name set in mumble or empty string."""
domain_file = pathlib.Path('/var/lib/mumble-server/domain-freedombox') domain_file = pathlib.Path('/var/lib/mumble-server/domain-freedombox')
try: try:
@ -43,7 +42,7 @@ def get_domain() -> Optional[str]:
@privileged @privileged
def set_domain(domain_name: Optional[str]): def set_domain(domain_name: str | None):
"""Write a file containing domain name.""" """Write a file containing domain name."""
if domain_name: if domain_name:
domain_file = pathlib.Path('/var/lib/mumble-server/domain-freedombox') domain_file = pathlib.Path('/var/lib/mumble-server/domain-freedombox')
@ -69,7 +68,7 @@ def change_root_channel_name(root_channel_name: str):
@privileged @privileged
def get_root_channel_name() -> Optional[str]: def get_root_channel_name() -> str | None:
"""Return the currently configured Root channel name.""" """Return the currently configured Root channel name."""
aug = _load_augeas() aug = _load_augeas()
name = aug.get('.anon/registerName') name = aug.get('.anon/registerName')

View File

@ -2,7 +2,6 @@
"""Configure Privacy App.""" """Configure Privacy App."""
import pathlib import pathlib
from typing import Optional
import augeas import augeas
@ -30,7 +29,7 @@ def setup():
@privileged @privileged
def set_configuration(enable_popcon: Optional[bool] = None): def set_configuration(enable_popcon: bool | None = None):
"""Update popcon configuration.""" """Update popcon configuration."""
aug = _load_augeas() aug = _load_augeas()
if enable_popcon: if enable_popcon:

View File

@ -7,7 +7,6 @@ import pathlib
import random import random
import string import string
from shutil import move from shutil import move
from typing import Union
from plinth import action_utils from plinth import action_utils
from plinth.actions import privileged from plinth.actions import privileged
@ -64,7 +63,7 @@ def setup():
@privileged @privileged
def get_config() -> dict[str, Union[int, str]]: def get_config() -> dict[str, int | str]:
"""Read and print Shadowsocks configuration.""" """Read and print Shadowsocks configuration."""
config = open(SHADOWSOCKS_CONFIG_SYMLINK, 'r', encoding='utf-8').read() config = open(SHADOWSOCKS_CONFIG_SYMLINK, 'r', encoding='utf-8').read()
return json.loads(config) return json.loads(config)
@ -86,7 +85,7 @@ def _merge_config(config):
@privileged @privileged
def merge_config(config: dict[str, Union[int, str]]): def merge_config(config: dict[str, int | str]):
"""Configure Shadowsocks Client.""" """Configure Shadowsocks Client."""
_merge_config(config) _merge_config(config)

View File

@ -6,7 +6,6 @@ import os
import pathlib import pathlib
import random import random
import string import string
from typing import Union
from plinth import action_utils from plinth import action_utils
from plinth.actions import privileged from plinth.actions import privileged
@ -47,7 +46,7 @@ def setup():
@privileged @privileged
def get_config() -> dict[str, Union[int, str]]: def get_config() -> dict[str, int | str]:
"""Read and print Shadowsocks Server configuration.""" """Read and print Shadowsocks Server configuration."""
config = open(SHADOWSOCKS_CONFIG_SYMLINK, 'r', encoding='utf-8').read() config = open(SHADOWSOCKS_CONFIG_SYMLINK, 'r', encoding='utf-8').read()
return json.loads(config) return json.loads(config)

View File

@ -10,7 +10,7 @@ import shutil
import socket import socket
import subprocess import subprocess
import time import time
from typing import Any, Optional, Union from typing import Any
import augeas import augeas
@ -143,11 +143,10 @@ def _remove_proxy():
@privileged @privileged
def configure(use_upstream_bridges: Optional[bool] = None, def configure(use_upstream_bridges: bool | None = None,
upstream_bridges: Optional[str] = None, upstream_bridges: str | None = None, relay: bool | None = None,
relay: Optional[bool] = None, bridge_relay: bool | None = None,
bridge_relay: Optional[bool] = None, hidden_service: bool | None = None):
hidden_service: Optional[bool] = None):
"""Configure Tor.""" """Configure Tor."""
aug = augeas_load() aug = augeas_load()
@ -193,7 +192,7 @@ def restart():
@privileged @privileged
def get_status() -> dict[str, Union[bool, str, dict[str, Any]]]: def get_status() -> dict[str, bool | str | dict[str, Any]]:
"""Return dict with Tor status.""" """Return dict with Tor status."""
aug = augeas_load() aug = augeas_load()
return { return {
@ -323,8 +322,7 @@ def _disable():
action_utils.service_disable(SERVICE_NAME) action_utils.service_disable(SERVICE_NAME)
def _use_upstream_bridges(use_upstream_bridges: Optional[bool] = None, def _use_upstream_bridges(use_upstream_bridges: bool | None = None, aug=None):
aug=None):
"""Enable use of upstream bridges.""" """Enable use of upstream bridges."""
if use_upstream_bridges is None: if use_upstream_bridges is None:
return return
@ -363,8 +361,7 @@ def _set_upstream_bridges(upstream_bridges=None, aug=None):
aug.save() aug.save()
def _enable_relay(relay: Optional[bool], bridge: Optional[bool], def _enable_relay(relay: bool | None, bridge: bool | None, aug: augeas.Augeas):
aug: augeas.Augeas):
"""Enable Tor bridge relay.""" """Enable Tor bridge relay."""
if relay is None and bridge is None: if relay is None and bridge is None:
return return

View File

@ -5,7 +5,7 @@ import logging
import os import os
import shutil import shutil
import subprocess import subprocess
from typing import Any, Optional, Union from typing import Any
import augeas import augeas
@ -66,9 +66,9 @@ def _first_time_setup():
@privileged @privileged
def configure(use_upstream_bridges: Optional[bool] = None, def configure(use_upstream_bridges: bool | None = None,
upstream_bridges: Optional[str] = None, upstream_bridges: str | None = None,
apt_transport_tor: Optional[bool] = None): apt_transport_tor: bool | None = None):
"""Configure Tor.""" """Configure Tor."""
aug = augeas_load() aug = augeas_load()
@ -92,7 +92,7 @@ def restart():
@privileged @privileged
def get_status() -> dict[str, Union[bool, str, dict[str, Any]]]: def get_status() -> dict[str, bool | str | dict[str, Any]]:
"""Return dict with Tor Proxy status.""" """Return dict with Tor Proxy status."""
aug = augeas_load() aug = augeas_load()
return { return {
@ -125,8 +125,7 @@ def _disable():
action_utils.service_disable(SERVICE_NAME) action_utils.service_disable(SERVICE_NAME)
def _use_upstream_bridges(use_upstream_bridges: Optional[bool] = None, def _use_upstream_bridges(use_upstream_bridges: bool | None = None, aug=None):
aug=None):
"""Enable use of upstream bridges.""" """Enable use of upstream bridges."""
if use_upstream_bridges is None: if use_upstream_bridges is None:
return return

View File

@ -5,7 +5,6 @@ Configuration helper for Transmission daemon.
import json import json
import pathlib import pathlib
from typing import Union
from plinth import action_utils from plinth import action_utils
from plinth.actions import privileged from plinth.actions import privileged
@ -20,7 +19,7 @@ def get_configuration() -> dict[str, str]:
@privileged @privileged
def merge_configuration(configuration: dict[str, Union[str, bool]]): def merge_configuration(configuration: dict[str, str | bool]):
"""Merge given JSON configuration with existing configuration.""" """Merge given JSON configuration with existing configuration."""
current_configuration_bytes = _transmission_config.read_bytes() current_configuration_bytes = _transmission_config.read_bytes()
current_configuration = json.loads(current_configuration_bytes) current_configuration = json.loads(current_configuration_bytes)

View File

@ -3,7 +3,6 @@
import os import os
import subprocess import subprocess
from typing import Optional
import augeas import augeas
@ -19,15 +18,16 @@ DB_BACKUP_FILE = '/var/lib/plinth/backups-data/ttrss-database.sql'
@privileged @privileged
def pre_setup(): def pre_setup():
"""Preseed debconf values before packages are installed.""" """Preseed debconf values before packages are installed."""
action_utils.debconf_set_selections( action_utils.debconf_set_selections([
['tt-rss tt-rss/database-type string pgsql', 'tt-rss tt-rss/database-type string pgsql',
'tt-rss tt-rss/purge boolean true', 'tt-rss tt-rss/purge boolean true',
'tt-rss tt-rss/dbconfig-remove boolean true', 'tt-rss tt-rss/dbconfig-remove boolean true',
'tt-rss tt-rss/dbconfig-reinstall boolean true']) 'tt-rss tt-rss/dbconfig-reinstall boolean true'
])
@privileged @privileged
def get_domain() -> Optional[str]: def get_domain() -> str | None:
"""Get the domain set for Tiny Tiny RSS.""" """Get the domain set for Tiny Tiny RSS."""
aug = load_augeas() aug = load_augeas()
@ -41,7 +41,7 @@ def get_domain() -> Optional[str]:
@privileged @privileged
def set_domain(domain_name: Optional[str]): def set_domain(domain_name: str | None):
"""Set the domain to be used by Tiny Tiny RSS.""" """Set the domain to be used by Tiny Tiny RSS."""
if not domain_name: if not domain_name:
return return

View File

@ -7,7 +7,6 @@ import pathlib
import re import re
import subprocess import subprocess
import time import time
from typing import List, Tuple, Union
from plinth.action_utils import (apt_hold, apt_hold_flag, apt_hold_freedombox, from plinth.action_utils import (apt_hold, apt_hold_flag, apt_hold_freedombox,
apt_unhold_freedombox, debconf_set_selections, apt_unhold_freedombox, debconf_set_selections,
@ -81,7 +80,7 @@ Pin: release a=bullseye-backports
Pin-Priority: 500 Pin-Priority: 500
''' '''
DIST_UPGRADE_OBSOLETE_PACKAGES: List[str] = [] DIST_UPGRADE_OBSOLETE_PACKAGES: list[str] = []
DIST_UPGRADE_PACKAGES_WITH_PROMPTS = [ DIST_UPGRADE_PACKAGES_WITH_PROMPTS = [
'bind9', 'firewalld', 'janus', 'minetest-server', 'minidlna', 'bind9', 'firewalld', 'janus', 'minetest-server', 'minidlna',
@ -90,7 +89,7 @@ DIST_UPGRADE_PACKAGES_WITH_PROMPTS = [
DIST_UPGRADE_PRE_INSTALL_PACKAGES = ['base-files'] DIST_UPGRADE_PRE_INSTALL_PACKAGES = ['base-files']
DIST_UPGRADE_PRE_DEBCONF_SELECTIONS: List[str] = [ DIST_UPGRADE_PRE_DEBCONF_SELECTIONS: list[str] = [
# Tell grub-pc to continue without installing grub again. # Tell grub-pc to continue without installing grub again.
'grub-pc grub-pc/install_devices_empty boolean true' 'grub-pc grub-pc/install_devices_empty boolean true'
] ]
@ -317,7 +316,7 @@ def _is_sufficient_free_space() -> bool:
return free_space >= DIST_UPGRADE_REQUIRED_FREE_SPACE return free_space >= DIST_UPGRADE_REQUIRED_FREE_SPACE
def _check_dist_upgrade(test_upgrade=False) -> Tuple[bool, str]: def _check_dist_upgrade(test_upgrade=False) -> tuple[bool, str]:
"""Check if a distribution upgrade be performed. """Check if a distribution upgrade be performed.
Check for new stable release, if updates are enabled, and if there is Check for new stable release, if updates are enabled, and if there is
@ -589,7 +588,7 @@ def _start_dist_upgrade_service():
@privileged @privileged
def start_dist_upgrade(test: bool = False) -> dict[str, Union[str, bool]]: def start_dist_upgrade(test: bool = False) -> dict[str, str | bool]:
"""Start dist upgrade process. """Start dist upgrade process.
Check if a new stable release is available, and start dist-upgrade process Check if a new stable release is available, and start dist-upgrade process

View File

@ -6,7 +6,6 @@ import os
import re import re
import shutil import shutil
import subprocess import subprocess
from typing import Optional
import augeas import augeas
@ -220,8 +219,8 @@ def _disconnect_samba_user(username):
@privileged @privileged
def create_user(username: str, password: str, auth_user: Optional[str] = None, def create_user(username: str, password: str, auth_user: str | None = None,
auth_password: Optional[str] = None): auth_password: str | None = None):
"""Create an LDAP user, set password and flush cache.""" """Create an LDAP user, set password and flush cache."""
_validate_user(auth_user, auth_password) _validate_user(auth_user, auth_password)
@ -232,7 +231,7 @@ def create_user(username: str, password: str, auth_user: Optional[str] = None,
@privileged @privileged
def remove_user(username: str, password: Optional[str] = None): def remove_user(username: str, password: str | None = None):
"""Remove an LDAP user.""" """Remove an LDAP user."""
groups = _get_user_groups(username) groups = _get_user_groups(username)
@ -424,8 +423,8 @@ def _add_user_to_group(username, groupname):
@privileged @privileged
def add_user_to_group(username: str, groupname: str, def add_user_to_group(username: str, groupname: str,
auth_user: Optional[str] = None, auth_user: str | None = None,
auth_password: Optional[str] = None): auth_password: str | None = None):
"""Add an LDAP user to an LDAP group.""" """Add an LDAP user to an LDAP group."""
if groupname == 'admin': if groupname == 'admin':
_validate_user(auth_user, auth_password) _validate_user(auth_user, auth_password)

View File

@ -5,7 +5,6 @@ import configparser
import os import os
import re import re
import subprocess import subprocess
from typing import Optional
from plinth import action_utils from plinth import action_utils
from plinth.actions import privileged from plinth.actions import privileged
@ -65,8 +64,8 @@ def _get_db_name():
@privileged @privileged
def set_configuration(enable_osm: Optional[bool] = None, def set_configuration(enable_osm: bool | None = None,
admin_user: Optional[str] = None): admin_user: str | None = None):
"""Setup Zoph Apache configuration.""" """Setup Zoph Apache configuration."""
_zoph_configure('interface.user.remote', 'true') _zoph_configure('interface.user.remote', 'true')
@ -90,7 +89,7 @@ def set_configuration(enable_osm: Optional[bool] = None,
@privileged @privileged
def is_configured() -> Optional[bool]: def is_configured() -> bool | None:
"""Return whether zoph app is configured.""" """Return whether zoph app is configured."""
try: try:
process = subprocess.run( process = subprocess.run(

View File

@ -4,7 +4,7 @@
import enum import enum
import logging import logging
import threading import threading
from typing import Callable, Optional from typing import Callable
from . import app as app_module from . import app as app_module
@ -22,10 +22,10 @@ class Operation:
COMPLETED: str = 'completed' COMPLETED: str = 'completed'
def __init__(self, app_id: str, name: str, target: Callable, def __init__(self, app_id: str, name: str, target: Callable,
args: Optional[list] = None, kwargs: Optional[dict] = None, args: list | None = None, kwargs: dict | None = None,
show_message: bool = True, show_notification: bool = False, show_message: bool = True, show_notification: bool = False,
thread_data: Optional[dict] = None, thread_data: dict | None = None,
on_complete: Callable = None): on_complete: Callable | None = None):
"""Initialize to no operation.""" """Initialize to no operation."""
self.app_id = app_id self.app_id = app_id
self.name = name self.name = name
@ -39,8 +39,8 @@ class Operation:
self.state = Operation.State.WAITING self.state = Operation.State.WAITING
self.return_value = None self.return_value = None
self._message: Optional[str] = None self._message: str | None = None
self.exception: Optional[Exception] = None self.exception: Exception | None = None
# Operation specific data # Operation specific data
self.thread_data: dict = thread_data or {} self.thread_data: dict = thread_data or {}
@ -94,8 +94,8 @@ class Operation:
thread = threading.current_thread() thread = threading.current_thread()
return thread._operation # type: ignore [attr-defined] return thread._operation # type: ignore [attr-defined]
def on_update(self, message: Optional[str] = None, def on_update(self, message: str | None = None,
exception: Optional[Exception] = None): exception: Exception | None = None):
"""Call from within the thread to update the progress of operation.""" """Call from within the thread to update the progress of operation."""
if message: if message:
self._message = message self._message = message
@ -172,7 +172,7 @@ class OperationsManager:
def __init__(self): def __init__(self):
"""Initialize the object.""" """Initialize the object."""
self._operations: list[Operation] = [] self._operations: list[Operation] = []
self._current_operation: Optional[Operation] = None self._current_operation: Operation | None = None
# Assume that operations manager will be called from various threads # Assume that operations manager will be called from various threads
# including the callback called from the threads it creates. Ensure # including the callback called from the threads it creates. Ensure

View File

@ -5,7 +5,6 @@ import enum
import logging import logging
import pathlib import pathlib
import time import time
from typing import Optional, Union
import apt.cache import apt.cache
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -42,11 +41,11 @@ class Package(PackageExpression):
self, self,
name, name,
optional: bool = False, optional: bool = False,
version: Optional[str] = None, # ">=1.0,<2.0" version: str | None = None, # ">=1.0,<2.0"
distribution: Optional[str] = None, # Debian, Ubuntu distribution: str | None = None, # Debian, Ubuntu
suite: Optional[str] = None, # stable, testing suite: str | None = None, # stable, testing
codename: Optional[str] = None, # bullseye-backports codename: str | None = None, # bullseye-backports
architecture: Optional[str] = None): # arm64 architecture: str | None = None): # arm64
self.name = name self.name = name
self.optional = optional self.optional = optional
self.version = version self.version = version
@ -108,10 +107,10 @@ class Packages(app_module.FollowerComponent):
REMOVE = 'remove' # Remove the packages before installing the app REMOVE = 'remove' # Remove the packages before installing the app
def __init__(self, component_id: str, def __init__(self, component_id: str,
packages: list[Union[str, PackageExpression]], packages: list[str | PackageExpression],
skip_recommends: bool = False, skip_recommends: bool = False,
conflicts: Optional[list[str]] = None, conflicts: list[str] | None = None,
conflicts_action: Optional[ConflictsAction] = None): conflicts_action: ConflictsAction | None = None):
"""Initialize a new packages component. """Initialize a new packages component.
'component_id' should be a unique ID across all components of an app 'component_id' should be a unique ID across all components of an app
@ -230,14 +229,14 @@ class Packages(app_module.FollowerComponent):
return results return results
def find_conflicts(self) -> Optional[list[str]]: def find_conflicts(self) -> list[str] | None:
"""Return list of conflicting packages installed on the system.""" """Return list of conflicting packages installed on the system."""
if not self.conflicts: if not self.conflicts:
return None return None
return packages_installed(self.conflicts) return packages_installed(self.conflicts)
def has_unavailable_packages(self) -> Optional[bool]: def has_unavailable_packages(self) -> bool | None:
"""Return whether any of the packages are not available. """Return whether any of the packages are not available.
Returns True if one or more of the packages is not available in the Returns True if one or more of the packages is not available in the
@ -465,7 +464,7 @@ def filter_conffile_prompt_packages(packages):
return privileged.filter_conffile_packages(list(packages)) return privileged.filter_conffile_packages(list(packages))
def packages_installed(candidates: Union[list, tuple]) -> list: def packages_installed(candidates: list | tuple) -> list:
"""Check which candidates are installed on the system. """Check which candidates are installed on the system.
:param candidates: A list of package names. :param candidates: A list of package names.

View File

@ -5,7 +5,7 @@ import logging
import os import os
import subprocess import subprocess
from collections import defaultdict from collections import defaultdict
from typing import Any, Optional from typing import Any
import apt.cache import apt.cache
import apt_inst import apt_inst
@ -31,7 +31,7 @@ def update():
@privileged @privileged
def install(app_id: str, packages: list[str], skip_recommends: bool = False, def install(app_id: str, packages: list[str], skip_recommends: bool = False,
force_configuration: Optional[str] = None, reinstall: bool = False, force_configuration: str | None = None, reinstall: bool = False,
force_missing_configuration: bool = False): force_missing_configuration: bool = False):
"""Install packages using apt-get.""" """Install packages using apt-get."""
if force_configuration not in ('old', 'new', None): if force_configuration not in ('old', 'new', None):

View File

@ -147,9 +147,9 @@ def test_assert_valid_type(actions_module):
# Invalid values for int, str, float and Optional # Invalid values for int, str, float and Optional
values = [[1, bool], ['foo', int], [1, str], [1, float], values = [[1, bool], ['foo', int], [1, str], [1, float],
[1, typing.Optional[str]], [1.1, typing.Union[int, str]], [1, typing.Optional[str]], [1, str | None],
[1, list], [1, dict], [[1], list[str]], [1.1, typing.Union[int, str]], [1.1, int | str], [1, list],
[{ [1, dict], [[1], list[str]], [{
'a': 'b' 'a': 'b'
}, dict[int, str]], [{ }, dict[int, str]], [{
1: 2 1: 2
@ -165,8 +165,12 @@ def test_assert_valid_type(actions_module):
assert_valid('arg', 1.1, float) assert_valid('arg', 1.1, float)
assert_valid('arg', None, typing.Optional[int]) assert_valid('arg', None, typing.Optional[int])
assert_valid('arg', 1, typing.Optional[int]) assert_valid('arg', 1, typing.Optional[int])
assert_valid('arg', None, int | None)
assert_valid('arg', 1, int | None)
assert_valid('arg', 1, typing.Union[int, str]) assert_valid('arg', 1, typing.Union[int, str])
assert_valid('arg', '1', typing.Union[int, str]) assert_valid('arg', '1', typing.Union[int, str])
assert_valid('arg', 1, int | str)
assert_valid('arg', '1', int | str)
assert_valid('arg', [], list[int]) assert_valid('arg', [], list[int])
assert_valid('arg', ['foo'], list[str]) assert_valid('arg', ['foo'], list[str])
assert_valid('arg', {}, dict[int, str]) assert_valid('arg', {}, dict[int, str])