refactor: improve backend type safety and import organization

- Add TYPE_CHECKING guards for runtime-expensive imports (aiohttp, aiosqlite)
- Reorganize imports to follow PEP 8 conventions
- Convert TypeAlias to modern PEP 695 type syntax (where appropriate)
- Use Sequence/Mapping from collections.abc for type hints (covariant)
- Replace string literals with cast() for improved type inference
- Fix casting of Fail2BanResponse and TypedDict patterns
- Add IpLookupResult TypedDict for precise return type annotation
- Reformat overlong lines for readability (120 char limit)
- Add asyncio_mode and filterwarnings to pytest config
- Update test fixtures with improved type hints

This improves mypy type checking and makes type relationships explicit.
This commit is contained in:
2026-03-20 13:44:14 +01:00
parent bdcdd5d672
commit 1c0bac1353
30 changed files with 431 additions and 644 deletions

View File

@@ -28,7 +28,7 @@ import os
import re
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, cast, TypeAlias
from typing import cast
import structlog
@@ -59,7 +59,6 @@ from app.services.jail_service import JailNotFoundError as JailNotFoundError
from app.utils import conffile_parser
from app.utils.fail2ban_client import (
Fail2BanClient,
Fail2BanCommand,
Fail2BanConnectionError,
Fail2BanResponse,
)
@@ -73,9 +72,7 @@ log: structlog.stdlib.BoundLogger = structlog.get_logger()
_SOCKET_TIMEOUT: float = 10.0
# Allowlist pattern for jail names used in path construction.
_SAFE_JAIL_NAME_RE: re.Pattern[str] = re.compile(
r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
)
_SAFE_JAIL_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
# Sections that are not jail definitions.
_META_SECTIONS: frozenset[str] = frozenset({"INCLUDES", "DEFAULT"})
@@ -167,8 +164,7 @@ class FilterReadonlyError(Exception):
"""
self.name: str = name
super().__init__(
f"Filter {name!r} is a shipped default (.conf only); "
"only user-created .local files can be deleted."
f"Filter {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
)
@@ -423,9 +419,7 @@ def _parse_jails_sync(
# items() merges DEFAULT values automatically.
jails[section] = dict(parser.items(section))
except configparser.Error as exc:
log.warning(
"jail_section_parse_error", section=section, error=str(exc)
)
log.warning("jail_section_parse_error", section=section, error=str(exc))
log.debug("jails_parsed", count=len(jails), config_dir=str(config_dir))
return jails, source_files
@@ -522,11 +516,7 @@ def _build_inactive_jail(
bantime_escalation=bantime_escalation,
source_file=source_file,
enabled=enabled,
has_local_override=(
(config_dir / "jail.d" / f"{name}.local").is_file()
if config_dir is not None
else False
),
has_local_override=((config_dir / "jail.d" / f"{name}.local").is_file() if config_dir is not None else False),
)
@@ -557,7 +547,7 @@ async def _get_active_jail_names(socket_path: str) -> set[str]:
return result
def _ok(response: object) -> object:
code, data = cast(Fail2BanResponse, response)
code, data = cast("Fail2BanResponse", response)
if code != 0:
raise ValueError(f"fail2ban error {code}: {data!r}")
return data
@@ -572,9 +562,7 @@ async def _get_active_jail_names(socket_path: str) -> set[str]:
log.warning("fail2ban_unreachable_during_inactive_list")
return set()
except Exception as exc: # noqa: BLE001
log.warning(
"fail2ban_status_error_during_inactive_list", error=str(exc)
)
log.warning("fail2ban_status_error_during_inactive_list", error=str(exc))
return set()
@@ -662,10 +650,7 @@ def _validate_jail_config_sync(
issues.append(
JailValidationIssue(
field="filter",
message=(
f"Filter file not found: filter.d/{base_filter}.conf"
" (or .local)"
),
message=(f"Filter file not found: filter.d/{base_filter}.conf (or .local)"),
)
)
@@ -681,10 +666,7 @@ def _validate_jail_config_sync(
issues.append(
JailValidationIssue(
field="action",
message=(
f"Action file not found: action.d/{action_name}.conf"
" (or .local)"
),
message=(f"Action file not found: action.d/{action_name}.conf (or .local)"),
)
)
@@ -840,9 +822,7 @@ def _write_local_override_sync(
try:
jail_d.mkdir(parents=True, exist_ok=True)
except OSError as exc:
raise ConfigWriteError(
f"Cannot create jail.d directory: {exc}"
) from exc
raise ConfigWriteError(f"Cannot create jail.d directory: {exc}") from exc
local_path = jail_d / f"{jail_name}.local"
@@ -867,7 +847,7 @@ def _write_local_override_sync(
if overrides.get("port") is not None:
lines.append(f"port = {overrides['port']}")
if overrides.get("logpath"):
paths: list[str] = cast(list[str], overrides["logpath"])
paths: list[str] = cast("list[str]", overrides["logpath"])
if paths:
lines.append(f"logpath = {paths[0]}")
for p in paths[1:]:
@@ -890,9 +870,7 @@ def _write_local_override_sync(
# Clean up temp file if rename failed.
with contextlib.suppress(OSError):
os.unlink(tmp_name) # noqa: F821 — only reachable when tmp_name is set
raise ConfigWriteError(
f"Failed to write {local_path}: {exc}"
) from exc
raise ConfigWriteError(f"Failed to write {local_path}: {exc}") from exc
log.info(
"jail_local_written",
@@ -921,9 +899,7 @@ def _restore_local_file_sync(local_path: Path, original_content: bytes | None) -
try:
local_path.unlink(missing_ok=True)
except OSError as exc:
raise ConfigWriteError(
f"Failed to delete {local_path} during rollback: {exc}"
) from exc
raise ConfigWriteError(f"Failed to delete {local_path} during rollback: {exc}") from exc
return
tmp_name: str | None = None
@@ -941,9 +917,7 @@ def _restore_local_file_sync(local_path: Path, original_content: bytes | None) -
with contextlib.suppress(OSError):
if tmp_name is not None:
os.unlink(tmp_name)
raise ConfigWriteError(
f"Failed to restore {local_path} during rollback: {exc}"
) from exc
raise ConfigWriteError(f"Failed to restore {local_path} during rollback: {exc}") from exc
def _validate_regex_patterns(patterns: list[str]) -> None:
@@ -979,9 +953,7 @@ def _write_filter_local_sync(filter_d: Path, name: str, content: str) -> None:
try:
filter_d.mkdir(parents=True, exist_ok=True)
except OSError as exc:
raise ConfigWriteError(
f"Cannot create filter.d directory: {exc}"
) from exc
raise ConfigWriteError(f"Cannot create filter.d directory: {exc}") from exc
local_path = filter_d / f"{name}.local"
try:
@@ -998,9 +970,7 @@ def _write_filter_local_sync(filter_d: Path, name: str, content: str) -> None:
except OSError as exc:
with contextlib.suppress(OSError):
os.unlink(tmp_name) # noqa: F821
raise ConfigWriteError(
f"Failed to write {local_path}: {exc}"
) from exc
raise ConfigWriteError(f"Failed to write {local_path}: {exc}") from exc
log.info("filter_local_written", filter=name, path=str(local_path))
@@ -1031,9 +1001,7 @@ def _set_jail_local_key_sync(
try:
jail_d.mkdir(parents=True, exist_ok=True)
except OSError as exc:
raise ConfigWriteError(
f"Cannot create jail.d directory: {exc}"
) from exc
raise ConfigWriteError(f"Cannot create jail.d directory: {exc}") from exc
local_path = jail_d / f"{jail_name}.local"
@@ -1072,9 +1040,7 @@ def _set_jail_local_key_sync(
except OSError as exc:
with contextlib.suppress(OSError):
os.unlink(tmp_name) # noqa: F821
raise ConfigWriteError(
f"Failed to write {local_path}: {exc}"
) from exc
raise ConfigWriteError(f"Failed to write {local_path}: {exc}") from exc
log.info(
"jail_local_key_set",
@@ -1112,8 +1078,8 @@ async def list_inactive_jails(
inactive jails.
"""
loop = asyncio.get_event_loop()
parsed_result: tuple[dict[str, dict[str, str]], dict[str, str]] = (
await loop.run_in_executor(None, _parse_jails_sync, Path(config_dir))
parsed_result: tuple[dict[str, dict[str, str]], dict[str, str]] = await loop.run_in_executor(
None, _parse_jails_sync, Path(config_dir)
)
all_jails, source_files = parsed_result
active_names: set[str] = await _get_active_jail_names(socket_path)
@@ -1170,9 +1136,7 @@ async def activate_jail(
_safe_jail_name(name)
loop = asyncio.get_event_loop()
all_jails, _source_files = await loop.run_in_executor(
None, _parse_jails_sync, Path(config_dir)
)
all_jails, _source_files = await loop.run_in_executor(None, _parse_jails_sync, Path(config_dir))
if name not in all_jails:
raise JailNotFoundInConfigError(name)
@@ -1208,10 +1172,7 @@ async def activate_jail(
active=False,
fail2ban_running=True,
validation_warnings=warnings,
message=(
f"Jail {name!r} cannot be activated: "
+ "; ".join(i.message for i in blocking)
),
message=(f"Jail {name!r} cannot be activated: " + "; ".join(i.message for i in blocking)),
)
overrides: dict[str, object] = {
@@ -1254,9 +1215,7 @@ async def activate_jail(
jail=name,
error=str(exc),
)
recovered = await _rollback_activation_async(
config_dir, name, socket_path, original_content
)
recovered = await _rollback_activation_async(config_dir, name, socket_path, original_content)
return JailActivationResponse(
name=name,
active=False,
@@ -1272,9 +1231,7 @@ async def activate_jail(
)
except Exception as exc: # noqa: BLE001
log.warning("reload_after_activate_failed", jail=name, error=str(exc))
recovered = await _rollback_activation_async(
config_dir, name, socket_path, original_content
)
recovered = await _rollback_activation_async(config_dir, name, socket_path, original_content)
return JailActivationResponse(
name=name,
active=False,
@@ -1305,9 +1262,7 @@ async def activate_jail(
jail=name,
message="fail2ban socket unreachable after reload — initiating rollback.",
)
recovered = await _rollback_activation_async(
config_dir, name, socket_path, original_content
)
recovered = await _rollback_activation_async(config_dir, name, socket_path, original_content)
return JailActivationResponse(
name=name,
active=False,
@@ -1330,9 +1285,7 @@ async def activate_jail(
jail=name,
message="Jail did not appear in running jails — initiating rollback.",
)
recovered = await _rollback_activation_async(
config_dir, name, socket_path, original_content
)
recovered = await _rollback_activation_async(config_dir, name, socket_path, original_content)
return JailActivationResponse(
name=name,
active=False,
@@ -1388,14 +1341,10 @@ async def _rollback_activation_async(
# Step 1 — restore original file (or delete it).
try:
await loop.run_in_executor(
None, _restore_local_file_sync, local_path, original_content
)
await loop.run_in_executor(None, _restore_local_file_sync, local_path, original_content)
log.info("jail_activation_rollback_file_restored", jail=name)
except ConfigWriteError as exc:
log.error(
"jail_activation_rollback_restore_failed", jail=name, error=str(exc)
)
log.error("jail_activation_rollback_restore_failed", jail=name, error=str(exc))
return False
# Step 2 — reload fail2ban with the restored config.
@@ -1403,9 +1352,7 @@ async def _rollback_activation_async(
await jail_service.reload_all(socket_path)
log.info("jail_activation_rollback_reload_ok", jail=name)
except Exception as exc: # noqa: BLE001
log.warning(
"jail_activation_rollback_reload_failed", jail=name, error=str(exc)
)
log.warning("jail_activation_rollback_reload_failed", jail=name, error=str(exc))
return False
# Step 3 — wait for fail2ban to come back.
@@ -1450,9 +1397,7 @@ async def deactivate_jail(
_safe_jail_name(name)
loop = asyncio.get_event_loop()
all_jails, _source_files = await loop.run_in_executor(
None, _parse_jails_sync, Path(config_dir)
)
all_jails, _source_files = await loop.run_in_executor(None, _parse_jails_sync, Path(config_dir))
if name not in all_jails:
raise JailNotFoundInConfigError(name)
@@ -1510,9 +1455,7 @@ async def delete_jail_local_override(
_safe_jail_name(name)
loop = asyncio.get_event_loop()
all_jails, _source_files = await loop.run_in_executor(
None, _parse_jails_sync, Path(config_dir)
)
all_jails, _source_files = await loop.run_in_executor(None, _parse_jails_sync, Path(config_dir))
if name not in all_jails:
raise JailNotFoundInConfigError(name)
@@ -1523,13 +1466,9 @@ async def delete_jail_local_override(
local_path = Path(config_dir) / "jail.d" / f"{name}.local"
try:
await loop.run_in_executor(
None, lambda: local_path.unlink(missing_ok=True)
)
await loop.run_in_executor(None, lambda: local_path.unlink(missing_ok=True))
except OSError as exc:
raise ConfigWriteError(
f"Failed to delete {local_path}: {exc}"
) from exc
raise ConfigWriteError(f"Failed to delete {local_path}: {exc}") from exc
log.info("jail_local_override_deleted", jail=name, path=str(local_path))
@@ -1610,9 +1549,7 @@ async def rollback_jail(
log.info("jail_rollback_start_attempted", jail=name, start_ok=started)
# Wait for the socket to come back.
fail2ban_running = await wait_for_fail2ban(
socket_path, max_wait_seconds=10.0, poll_interval=2.0
)
fail2ban_running = await wait_for_fail2ban(socket_path, max_wait_seconds=10.0, poll_interval=2.0)
active_jails = 0
if fail2ban_running:
@@ -1626,10 +1563,7 @@ async def rollback_jail(
disabled=True,
fail2ban_running=True,
active_jails=active_jails,
message=(
f"Jail {name!r} disabled and fail2ban restarted successfully "
f"with {active_jails} active jail(s)."
),
message=(f"Jail {name!r} disabled and fail2ban restarted successfully with {active_jails} active jail(s)."),
)
log.warning("jail_rollback_fail2ban_still_down", jail=name)
@@ -1650,9 +1584,7 @@ async def rollback_jail(
# ---------------------------------------------------------------------------
# Allowlist pattern for filter names used in path construction.
_SAFE_FILTER_NAME_RE: re.Pattern[str] = re.compile(
r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
)
_SAFE_FILTER_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
class FilterNotFoundError(Exception):
@@ -1764,9 +1696,7 @@ def _parse_filters_sync(
try:
content = conf_path.read_text(encoding="utf-8")
except OSError as exc:
log.warning(
"filter_read_error", name=name, path=str(conf_path), error=str(exc)
)
log.warning("filter_read_error", name=name, path=str(conf_path), error=str(exc))
continue
if has_local:
@@ -1842,9 +1772,7 @@ async def list_filters(
loop = asyncio.get_event_loop()
# Run the synchronous scan in a thread-pool executor.
raw_filters: list[tuple[str, str, str, bool, str]] = await loop.run_in_executor(
None, _parse_filters_sync, filter_d
)
raw_filters: list[tuple[str, str, str, bool, str]] = await loop.run_in_executor(None, _parse_filters_sync, filter_d)
# Fetch active jail names and their configs concurrently.
all_jails_result, active_names = await asyncio.gather(
@@ -1857,9 +1785,7 @@ async def list_filters(
filters: list[FilterConfig] = []
for name, filename, content, has_local, source_path in raw_filters:
cfg = conffile_parser.parse_filter_file(
content, name=name, filename=filename
)
cfg = conffile_parser.parse_filter_file(content, name=name, filename=filename)
used_by = sorted(filter_to_jails.get(name, []))
filters.append(
FilterConfig(
@@ -1947,9 +1873,7 @@ async def get_filter(
content, has_local, source_path = await loop.run_in_executor(None, _read)
cfg = conffile_parser.parse_filter_file(
content, name=base_name, filename=f"{base_name}.conf"
)
cfg = conffile_parser.parse_filter_file(content, name=base_name, filename=f"{base_name}.conf")
all_jails_result, active_names = await asyncio.gather(
loop.run_in_executor(None, _parse_jails_sync, Path(config_dir)),
@@ -2182,9 +2106,7 @@ async def delete_filter(
try:
local_path.unlink()
except OSError as exc:
raise ConfigWriteError(
f"Failed to delete {local_path}: {exc}"
) from exc
raise ConfigWriteError(f"Failed to delete {local_path}: {exc}") from exc
log.info("filter_local_deleted", filter=base_name, path=str(local_path))
@@ -2226,9 +2148,7 @@ async def assign_filter_to_jail(
loop = asyncio.get_event_loop()
# Verify the jail exists in config.
all_jails, _src = await loop.run_in_executor(
None, _parse_jails_sync, Path(config_dir)
)
all_jails, _src = await loop.run_in_executor(None, _parse_jails_sync, Path(config_dir))
if jail_name not in all_jails:
raise JailNotFoundInConfigError(jail_name)
@@ -2276,9 +2196,7 @@ async def assign_filter_to_jail(
# ---------------------------------------------------------------------------
# Allowlist pattern for action names used in path construction.
_SAFE_ACTION_NAME_RE: re.Pattern[str] = re.compile(
r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
)
_SAFE_ACTION_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
class ActionNotFoundError(Exception):
@@ -2318,8 +2236,7 @@ class ActionReadonlyError(Exception):
"""
self.name: str = name
super().__init__(
f"Action {name!r} is a shipped default (.conf only); "
"only user-created .local files can be deleted."
f"Action {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
)
@@ -2428,9 +2345,7 @@ def _parse_actions_sync(
try:
content = conf_path.read_text(encoding="utf-8")
except OSError as exc:
log.warning(
"action_read_error", name=name, path=str(conf_path), error=str(exc)
)
log.warning("action_read_error", name=name, path=str(conf_path), error=str(exc))
continue
if has_local:
@@ -2495,9 +2410,7 @@ def _append_jail_action_sync(
try:
jail_d.mkdir(parents=True, exist_ok=True)
except OSError as exc:
raise ConfigWriteError(
f"Cannot create jail.d directory: {exc}"
) from exc
raise ConfigWriteError(f"Cannot create jail.d directory: {exc}") from exc
local_path = jail_d / f"{jail_name}.local"
@@ -2517,9 +2430,7 @@ def _append_jail_action_sync(
existing_raw = parser.get(jail_name, "action") if parser.has_option(jail_name, "action") else ""
existing_lines = [
line.strip()
for line in existing_raw.splitlines()
if line.strip() and not line.strip().startswith("#")
line.strip() for line in existing_raw.splitlines() if line.strip() and not line.strip().startswith("#")
]
# Extract base names from existing entries for duplicate checking.
@@ -2533,9 +2444,7 @@ def _append_jail_action_sync(
if existing_lines:
# configparser multi-line: continuation lines start with whitespace.
new_value = existing_lines[0] + "".join(
f"\n {line}" for line in existing_lines[1:]
)
new_value = existing_lines[0] + "".join(f"\n {line}" for line in existing_lines[1:])
parser.set(jail_name, "action", new_value)
else:
parser.set(jail_name, "action", action_entry)
@@ -2559,9 +2468,7 @@ def _append_jail_action_sync(
except OSError as exc:
with contextlib.suppress(OSError):
os.unlink(tmp_name) # noqa: F821
raise ConfigWriteError(
f"Failed to write {local_path}: {exc}"
) from exc
raise ConfigWriteError(f"Failed to write {local_path}: {exc}") from exc
log.info(
"jail_action_appended",
@@ -2612,9 +2519,7 @@ def _remove_jail_action_sync(
existing_raw = parser.get(jail_name, "action")
existing_lines = [
line.strip()
for line in existing_raw.splitlines()
if line.strip() and not line.strip().startswith("#")
line.strip() for line in existing_raw.splitlines() if line.strip() and not line.strip().startswith("#")
]
def _base(entry: str) -> str:
@@ -2628,9 +2533,7 @@ def _remove_jail_action_sync(
return
if filtered:
new_value = filtered[0] + "".join(
f"\n {line}" for line in filtered[1:]
)
new_value = filtered[0] + "".join(f"\n {line}" for line in filtered[1:])
parser.set(jail_name, "action", new_value)
else:
parser.remove_option(jail_name, "action")
@@ -2654,9 +2557,7 @@ def _remove_jail_action_sync(
except OSError as exc:
with contextlib.suppress(OSError):
os.unlink(tmp_name) # noqa: F821
raise ConfigWriteError(
f"Failed to write {local_path}: {exc}"
) from exc
raise ConfigWriteError(f"Failed to write {local_path}: {exc}") from exc
log.info(
"jail_action_removed",
@@ -2683,9 +2584,7 @@ def _write_action_local_sync(action_d: Path, name: str, content: str) -> None:
try:
action_d.mkdir(parents=True, exist_ok=True)
except OSError as exc:
raise ConfigWriteError(
f"Cannot create action.d directory: {exc}"
) from exc
raise ConfigWriteError(f"Cannot create action.d directory: {exc}") from exc
local_path = action_d / f"{name}.local"
try:
@@ -2702,9 +2601,7 @@ def _write_action_local_sync(action_d: Path, name: str, content: str) -> None:
except OSError as exc:
with contextlib.suppress(OSError):
os.unlink(tmp_name) # noqa: F821
raise ConfigWriteError(
f"Failed to write {local_path}: {exc}"
) from exc
raise ConfigWriteError(f"Failed to write {local_path}: {exc}") from exc
log.info("action_local_written", action=name, path=str(local_path))
@@ -2740,9 +2637,7 @@ async def list_actions(
action_d = Path(config_dir) / "action.d"
loop = asyncio.get_event_loop()
raw_actions: list[tuple[str, str, str, bool, str]] = await loop.run_in_executor(
None, _parse_actions_sync, action_d
)
raw_actions: list[tuple[str, str, str, bool, str]] = await loop.run_in_executor(None, _parse_actions_sync, action_d)
all_jails_result, active_names = await asyncio.gather(
loop.run_in_executor(None, _parse_jails_sync, Path(config_dir)),
@@ -2754,9 +2649,7 @@ async def list_actions(
actions: list[ActionConfig] = []
for name, filename, content, has_local, source_path in raw_actions:
cfg = conffile_parser.parse_action_file(
content, name=name, filename=filename
)
cfg = conffile_parser.parse_action_file(content, name=name, filename=filename)
used_by = sorted(action_to_jails.get(name, []))
actions.append(
ActionConfig(
@@ -2843,9 +2736,7 @@ async def get_action(
content, has_local, source_path = await loop.run_in_executor(None, _read)
cfg = conffile_parser.parse_action_file(
content, name=base_name, filename=f"{base_name}.conf"
)
cfg = conffile_parser.parse_action_file(content, name=base_name, filename=f"{base_name}.conf")
all_jails_result, active_names = await asyncio.gather(
loop.run_in_executor(None, _parse_jails_sync, Path(config_dir)),
@@ -3061,9 +2952,7 @@ async def delete_action(
try:
local_path.unlink()
except OSError as exc:
raise ConfigWriteError(
f"Failed to delete {local_path}: {exc}"
) from exc
raise ConfigWriteError(f"Failed to delete {local_path}: {exc}") from exc
log.info("action_local_deleted", action=base_name, path=str(local_path))
@@ -3105,9 +2994,7 @@ async def assign_action_to_jail(
loop = asyncio.get_event_loop()
all_jails, _src = await loop.run_in_executor(
None, _parse_jails_sync, Path(config_dir)
)
all_jails, _src = await loop.run_in_executor(None, _parse_jails_sync, Path(config_dir))
if jail_name not in all_jails:
raise JailNotFoundInConfigError(jail_name)
@@ -3187,9 +3074,7 @@ async def remove_action_from_jail(
loop = asyncio.get_event_loop()
all_jails, _src = await loop.run_in_executor(
None, _parse_jails_sync, Path(config_dir)
)
all_jails, _src = await loop.run_in_executor(None, _parse_jails_sync, Path(config_dir))
if jail_name not in all_jails:
raise JailNotFoundInConfigError(jail_name)
@@ -3218,4 +3103,3 @@ async def remove_action_from_jail(
action=action_name,
reload=do_reload,
)