]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Bump minimum python version to 3.10
authorDaan De Meyer <daan@amutable.com>
Wed, 11 Feb 2026 19:39:33 +0000 (20:39 +0100)
committerDaan De Meyer <daan@amutable.com>
Wed, 11 Feb 2026 22:26:07 +0000 (23:26 +0100)
- CentOS Stream 9 has python 3.9 by default, but python 3.12 is packaged
- Ubuntu Jammy has python 3.10.

So we'll require CentOS Stream 9 users to install the python3.12
package to keep using mkosi, which shouldn't be a problem.

Bumping version allows us to switch to the Union operator among other
improvements. This commit gets rid of Union and Optional, we'll adopt
other 3.10 features later.

30 files changed:
bin/mkosi
docs/CODING_STYLE.md
kernel-install/50-mkosi.install
mkosi/__init__.py
mkosi/__main__.py
mkosi/archive.py
mkosi/bootloader.py
mkosi/completion.py
mkosi/config.py
mkosi/context.py
mkosi/curl.py
mkosi/distribution/__init__.py
mkosi/distribution/opensuse.py
mkosi/distribution/rhel.py
mkosi/initrd.py
mkosi/installer/apt.py
mkosi/installer/dnf.py
mkosi/installer/rpm.py
mkosi/log.py
mkosi/manifest.py
mkosi/mounts.py
mkosi/pager.py
mkosi/partition.py
mkosi/qemu.py
mkosi/resources/man/mkosi.1.md
mkosi/run.py
mkosi/util.py
pyproject.toml
tests/__init__.py
tests/test_json.py

index 9abee8f133ff3c40f51faf6827662299a09b77b1..930b171f37d0762d13d6250ec9d42ede2af77098 100755 (executable)
--- a/bin/mkosi
+++ b/bin/mkosi
@@ -8,7 +8,7 @@ command="$(basename "${BASH_SOURCE[0]//-/.}")"
 if [ -z "$MKOSI_INTERPRETER" ]; then
     # Note the check seems to be inverted here because the if branch is
     # executed when the exit status is 0 which is equal to False in Python.
-    if python3 -c 'import sys; sys.exit(sys.version_info < (3, 9))'; then
+    if python3 -c 'import sys; sys.exit(sys.version_info < (3, 10))'; then
         MKOSI_INTERPRETER=python3
     else
         # python3 is not found or too old, search $PATH for the newest interpreter.
@@ -21,7 +21,7 @@ if [ -z "$MKOSI_INTERPRETER" ]; then
             done | sort --unique --version-sort --reverse | head --lines=1
         )"
 
-        if [ -n "$candidate" ] && "$candidate" -c 'import sys; sys.exit(sys.version_info < (3, 9))'; then
+        if [ -n "$candidate" ] && "$candidate" -c 'import sys; sys.exit(sys.version_info < (3, 10))'; then
             MKOSI_INTERPRETER="$candidate"
         fi
     fi
index ea7b7f9178e4381283673a602e8791e8961e1764..2920437cdc01c669c0839a9a9cefc7fd906b4141 100644 (file)
@@ -9,7 +9,7 @@ SPDX-License-Identifier: LGPL-2.1-or-later
 
 ## Python Version
 
-- The lowest supported Python version is CPython 3.9.
+- The lowest supported Python version is CPython 3.10.
 
 ## Formatting
 
index bff9e4c769c6ab228a2b1f6b4f7ed7c0d09a2bf1..d51451a2eb34e51d1921d1025e59561a7b81032b 100755 (executable)
@@ -6,7 +6,6 @@ import os
 import sys
 import tempfile
 from pathlib import Path
-from typing import Optional
 
 from mkosi import identify_cpu
 from mkosi.archive import make_cpio
@@ -22,7 +21,7 @@ def we_are_wanted(context: KernelInstallContext) -> bool:
     return context.uki_generator == "mkosi" or context.initrd_generator in ("mkosi", "mkosi-initrd")
 
 
-def build_microcode_initrd(output: Path) -> Optional[Path]:
+def build_microcode_initrd(output: Path) -> Path | None:
     vendor, ucode = identify_cpu(Path("/"))
 
     if vendor is None:
index 2f54bfaccc85743a7af0878ca263b819629c7dd8..ca24e703bb35aa26ae24f037e9331465aff89cdf 100644 (file)
@@ -25,7 +25,7 @@ import zipapp
 from collections.abc import Iterator, Mapping, Sequence
 from contextlib import AbstractContextManager
 from pathlib import Path
-from typing import Any, Optional, Union, cast
+from typing import Any, cast
 
 from mkosi.archive import can_extract_tar, extract_tar, make_cpio, make_tar
 from mkosi.bootloader import (
@@ -1186,7 +1186,7 @@ def install_tree(
     src: Path,
     dst: Path,
     *,
-    target: Optional[Path] = None,
+    target: Path | None = None,
     preserve: bool = True,
 ) -> None:
     src = src.resolve()
@@ -1366,7 +1366,7 @@ def gzip_binary(context: Context) -> str:
     return "pigz" if context.config.find_binary("pigz") else "gzip"
 
 
-def kernel_get_ver_from_modules(context: Context) -> Optional[str]:
+def kernel_get_ver_from_modules(context: Context) -> str | None:
     # Try to get version from the first dir under usr/lib/modules but fail if multiple versions are found
     versions = [
         p.name for p in (context.root / "usr/lib/modules").glob("*") if KERNEL_VERSION_PATTERN.match(p.name)
@@ -1403,7 +1403,7 @@ def fixup_vmlinuz_location(context: Context) -> None:
             filename = d.name.removeprefix(f"{type}-")
             match = KERNEL_VERSION_PATTERN.search(filename)
 
-            kver: Optional[str]
+            kver: str | None
             if match:
                 kver = match.group(0)
             else:
@@ -1439,7 +1439,7 @@ def want_initrd(context: Context) -> bool:
     return True
 
 
-def identify_cpu(root: Path) -> tuple[Optional[Path], Optional[Path]]:
+def identify_cpu(root: Path) -> tuple[Path | None, Path | None]:
     for entry in Path("/proc/cpuinfo").read_text().split("\n\n"):
         vendor_id = family = model = stepping = None
         for line in entry.splitlines():
@@ -1885,7 +1885,7 @@ def systemd_stub_binary(context: Context) -> Path:
     return stub
 
 
-def systemd_stub_version(context: Context, stub: Path) -> Optional[GenericVersion]:
+def systemd_stub_version(context: Context, stub: Path) -> GenericVersion | None:
     try:
         sdmagic = extract_pe_section(context, stub, ".sdmagic", context.workspace / "sdmagic")
     except KeyError:
@@ -1950,9 +1950,7 @@ def find_entry_token(context: Context) -> str:
     return cast(str, output["EntryToken"])
 
 
-def finalize_cmdline(
-    context: Context, partitions: Sequence[Partition], roothash: Optional[str]
-) -> list[str]:
+def finalize_cmdline(context: Context, partitions: Sequence[Partition], roothash: str | None) -> list[str]:
     if (context.root / "etc/kernel/cmdline").exists():
         cmdline = [(context.root / "etc/kernel/cmdline").read_text().strip()]
     elif (context.root / "usr/lib/kernel/cmdline").exists():
@@ -2384,7 +2382,7 @@ def maybe_compress(
     context: Context,
     compression: Compression,
     src: Path,
-    dst: Optional[Path] = None,
+    dst: Path | None = None,
 ) -> None:
     if not compression or src.is_dir():
         if dst:
@@ -2418,7 +2416,7 @@ def copy_nspawn_settings(context: Context) -> None:
         copyfile2(context.config.nspawn_settings, context.staging / context.config.output_nspawn_settings)
 
 
-def get_uki_path(context: Context) -> Optional[Path]:
+def get_uki_path(context: Context) -> Path | None:
     if not want_efi(context.config) or context.config.unified_kernel_images == UnifiedKernelImage.none:
         return None
 
@@ -2607,7 +2605,7 @@ def calculate_signature_sop(context: Context) -> None:
         )  # fmt: skip
 
 
-def dir_size(path: Union[Path, os.DirEntry[str]]) -> int:
+def dir_size(path: Path | os.DirEntry[str]) -> int:
     dir_sum = 0
     for entry in os.scandir(path):
         if entry.is_symlink():
@@ -2622,7 +2620,7 @@ def dir_size(path: Union[Path, os.DirEntry[str]]) -> int:
     return dir_sum
 
 
-def save_manifest(context: Context, manifest: Optional[Manifest]) -> None:
+def save_manifest(context: Context, manifest: Manifest | None) -> None:
     if not manifest:
         return
 
@@ -2801,7 +2799,7 @@ def check_inputs(config: Config) -> None:
         )
 
 
-def check_tool(config: Config, *tools: PathString, reason: str, hint: Optional[str] = None) -> Path:
+def check_tool(config: Config, *tools: PathString, reason: str, hint: str | None = None) -> Path:
     tool = config.find_binary(*tools)
     if not tool:
         die(f"Could not find '{tools[0]}' which is required to {reason}.", hint=hint)
@@ -2814,7 +2812,7 @@ def check_systemd_tool(
     *tools: PathString,
     version: str,
     reason: str,
-    hint: Optional[str] = None,
+    hint: str | None = None,
 ) -> None:
     tool = check_tool(config, *tools, reason=reason, hint=hint)
 
@@ -2830,7 +2828,7 @@ def check_ukify(
     config: Config,
     version: str,
     reason: str,
-    hint: Optional[str] = None,
+    hint: str | None = None,
 ) -> None:
     ukify = check_tool(config, "ukify", "/usr/lib/systemd/ukify", reason=reason, hint=hint)
 
@@ -3385,7 +3383,7 @@ def reuse_cache(context: Context) -> bool:
 
 def save_esp_components(
     context: Context,
-) -> tuple[Optional[Path], Optional[str], Optional[Path], list[Path]]:
+) -> tuple[Path | None, str | None, Path | None, list[Path]]:
     if context.config.output_format == OutputFormat.addon:
         stub = systemd_addon_stub_binary(context)
         if not stub.exists():
@@ -3481,7 +3479,7 @@ def make_image(
         cmdline += ["--definitions", workdir(d)]
         opts += ["--ro-bind", d, workdir(d)]
 
-    def can_orphan_file(distribution: Union[Distribution, str, None], release: Optional[str]) -> bool:
+    def can_orphan_file(distribution: Distribution | str | None, release: str | None) -> bool:
         if not isinstance(distribution, Distribution):
             return True
 
@@ -3755,9 +3753,9 @@ def make_oci(context: Context, root_layer: Path, dst: Path) -> None:
 
 def make_esp(
     context: Context,
-    stub: Optional[Path],
-    kver: Optional[str],
-    kimg: Optional[Path],
+    stub: Path | None,
+    kver: str | None,
+    kimg: Path | None,
     microcode: list[Path],
 ) -> list[Partition]:
     if not context.config.architecture.to_efi():
@@ -3944,7 +3942,7 @@ def clamp_mtime(path: Path, mtime: int) -> None:
         os.utime(path, ns=updated, follow_symlinks=False)
 
 
-def normalize_mtime(root: Path, mtime: Optional[int], directory: Path = Path("")) -> None:
+def normalize_mtime(root: Path, mtime: int | None, directory: Path = Path("")) -> None:
     if mtime is None:
         return
 
@@ -4489,7 +4487,7 @@ def run_coredumpctl(args: Args, config: Config) -> None:
     run_systemd_tool("coredumpctl", args, config)
 
 
-def start_storage_target_mode(config: Config) -> AbstractContextManager[Optional[Popen]]:
+def start_storage_target_mode(config: Config) -> AbstractContextManager[Popen | None]:
     if config.storage_target_mode == ConfigFeature.disabled:
         return contextlib.nullcontext()
 
@@ -4926,7 +4924,7 @@ def run_build(
     resources: Path,
     keyring_dir: Path,
     metadata_dir: Path,
-    package_dir: Optional[Path] = None,
+    package_dir: Path | None = None,
 ) -> None:
     if not have_effective_cap(CAP_SYS_ADMIN):
         acquire_privileges()
@@ -4994,7 +4992,7 @@ def ensure_tools_tree_has_etc_resolv_conf(config: Config) -> None:
         )
 
 
-def run_verb(args: Args, tools: Optional[Config], images: Sequence[Config], *, resources: Path) -> None:
+def run_verb(args: Args, tools: Config | None, images: Sequence[Config], *, resources: Path) -> None:
     images = list(images)
 
     if args.verb == Verb.init:
index 57c082b69ec67ac683babf069e8db22d473c98b9..fca0ce7d55ce91b17ec53efc69ffc988322f6e5c 100644 (file)
@@ -5,7 +5,6 @@ import faulthandler
 import signal
 import sys
 from types import FrameType
-from typing import Optional
 
 import mkosi.resources
 from mkosi import run_verb
@@ -17,7 +16,7 @@ from mkosi.util import resource_path
 INTERRUPTED = False
 
 
-def onsignal(signal: int, frame: Optional[FrameType]) -> None:
+def onsignal(signal: int, frame: FrameType | None) -> None:
     global INTERRUPTED
     if INTERRUPTED:
         return
index f3fbea6108f022d64ffc189e83c9ad41d1ba99f4..89a38bcf5b105fc5f7218fa12c1220436968a3de 100644 (file)
@@ -3,7 +3,6 @@
 import os
 from collections.abc import Iterable, Sequence
 from pathlib import Path
-from typing import Optional
 
 from mkosi.log import complete_step, log_step
 from mkosi.run import SandboxProtocol, finalize_passwd_symlinks, nosandbox, run, workdir
@@ -109,7 +108,7 @@ def make_cpio(
     src: Path,
     dst: Path,
     *,
-    files: Optional[Iterable[Path]] = None,
+    files: Iterable[Path] | None = None,
     sandbox: SandboxProtocol = nosandbox,
 ) -> None:
     if not files:
index 6e22f5f00bbee09d2578b49df03e1d7d7893d195..e8fafbf8ac561afdd28557f11d28ee3796afa3ab 100644 (file)
@@ -10,7 +10,6 @@ import tempfile
 import textwrap
 from collections.abc import Iterator, Mapping, Sequence
 from pathlib import Path
-from typing import Optional
 
 from mkosi.config import (
     BiosBootloader,
@@ -168,7 +167,7 @@ def want_grub_bios(context: Context, partitions: Sequence[Partition] = ()) -> bo
     return (have and bios and esp and root and installed) if partitions else have
 
 
-def find_grub_directory(context: Context, *, target: str) -> Optional[Path]:
+def find_grub_directory(context: Context, *, target: str) -> Path | None:
     for d in ("usr/lib/grub", "usr/share/grub2"):
         if (p := context.root / d / target).exists() and any(p.iterdir()):
             return p
@@ -176,7 +175,7 @@ def find_grub_directory(context: Context, *, target: str) -> Optional[Path]:
     return None
 
 
-def find_grub_binary(config: Config, binary: str) -> Optional[Path]:
+def find_grub_binary(config: Config, binary: str) -> Path | None:
     assert "grub" not in binary
 
     # Debian has a bespoke setup where if only grub-pc-bin is installed, grub-bios-setup is installed in
@@ -185,7 +184,7 @@ def find_grub_binary(config: Config, binary: str) -> Optional[Path]:
     return config.find_binary(f"grub-{binary}", f"grub2-{binary}", f"/usr/lib/grub/i386-pc/grub-{binary}")
 
 
-def prepare_grub_config(context: Context) -> Optional[Path]:
+def prepare_grub_config(context: Context) -> Path | None:
     config = context.root / "efi" / context.config.distribution.installer.grub_prefix() / "grub.cfg"
     with umask(~0o700):
         config.parent.mkdir(exist_ok=True)
@@ -223,8 +222,8 @@ def grub_mkimage(
     *,
     target: str,
     modules: Sequence[str] = (),
-    output: Optional[Path] = None,
-    sbat: Optional[Path] = None,
+    output: Path | None = None,
+    sbat: Path | None = None,
 ) -> None:
     mkimage = find_grub_binary(context.config, "mkimage")
     assert mkimage
@@ -293,7 +292,7 @@ def grub_mkimage(
         )  # fmt: skip
 
 
-def find_signed_grub_image(context: Context) -> Optional[Path]:
+def find_signed_grub_image(context: Context) -> Path | None:
     arch = context.config.architecture.to_efi()
 
     patterns = [
@@ -476,9 +475,9 @@ def run_systemd_sign_tool(
     *,
     cmdline: Sequence[PathString],
     options: Sequence[PathString],
-    certificate: Optional[Path],
+    certificate: Path | None,
     certificate_source: CertificateSource,
-    key: Optional[Path],
+    key: Path | None,
     key_source: KeySource,
     env: Mapping[str, str] = {},
     stdout: _FILE = None,
index 398edb2ca2a0e25bc745de38a25bc33f41474905..a72421cd53c4f2388edc5c6aa3568d0e5c5794c3 100644 (file)
@@ -8,7 +8,6 @@ import shlex
 from collections.abc import Iterable, Mapping
 from pathlib import Path
 from textwrap import indent
-from typing import Optional, Union
 
 from mkosi import config
 from mkosi.log import die
@@ -27,7 +26,7 @@ class CompGen(StrEnum):
                 return CompGen.dirs
             else:
                 return CompGen.files
-        # TODO: the type of action.type is Union[Callable[[str], Any], FileType]
+        # TODO: the type of action.type is Callable[[str], Any] | FileType
         # the type of Path is type, but Path also works in this position,
         # because the constructor is a callable from str -> Path
         elif action.type is not None and (isinstance(action.type, type) and issubclass(action.type, Path)):
@@ -60,9 +59,9 @@ class CompGen(StrEnum):
 
 @dataclasses.dataclass(frozen=True)
 class CompletionItem:
-    short: Optional[str]
-    long: Optional[str]
-    help: Optional[str]
+    short: str | None
+    long: str | None
+    help: str | None
     choices: list[str]
     compgen: CompGen
 
@@ -104,7 +103,7 @@ def finalize_completion_bash(options: list[CompletionItem], resources: Path) ->
     def to_bash_array(name: str, entries: Iterable[str]) -> str:
         return f"{name.replace('-', '_')}=(" + " ".join(shlex.quote(str(e)) for e in entries) + ")"
 
-    def to_bash_hasharray(name: str, entries: Mapping[str, Union[str, int]]) -> str:
+    def to_bash_hasharray(name: str, entries: Mapping[str, str | int]) -> str:
         return (
             f"{name.replace('-', '_')}=("
             + " ".join(f"[{shlex.quote(str(k))}]={shlex.quote(str(v))}" for k, v in entries.items())
index fd9e9842e22fd86f15f366835031373ef345a7e4..f8cef3c871f7ffb9aad3a250725ddf0f0b0e820a 100644 (file)
@@ -29,7 +29,7 @@ import uuid
 from collections.abc import Collection, Iterable, Iterator, Sequence
 from contextlib import AbstractContextManager
 from pathlib import Path
-from typing import Any, Callable, ClassVar, Generic, Optional, Protocol, TypeVar, Union, cast
+from typing import Any, Callable, ClassVar, Generic, Protocol, TypeVar, cast
 
 from mkosi.distribution import Distribution, detect_distribution
 from mkosi.log import ARG_DEBUG, ARG_DEBUG_SANDBOX, ARG_DEBUG_SHELL, complete_step, die
@@ -60,7 +60,7 @@ T = TypeVar("T")
 D = TypeVar("D", bound=DataclassInstance)
 SE = TypeVar("SE", bound=StrEnum)
 
-ConfigParseCallback = Callable[[Optional[str], Optional[T]], Optional[T]]
+ConfigParseCallback = Callable[[str | None, T | None], T | None]
 ConfigMatchCallback = Callable[[str, T], bool]
 ConfigDefaultCallback = Callable[[dict[str, Any]], T]
 
@@ -161,7 +161,7 @@ class ConfigFeature(StrEnum):
 @dataclasses.dataclass(frozen=True)
 class ConfigTree:
     source: Path
-    target: Optional[Path]
+    target: Path | None
 
     def with_prefix(self, prefix: PathString = "/") -> tuple[Path, Path]:
         return (
@@ -181,8 +181,8 @@ class DriveFlag(StrEnum):
 class Drive:
     id: str
     size: int
-    directory: Optional[Path]
-    options: Optional[str]
+    directory: Path | None
+    options: str | None
     file_id: str
     flags: list[DriveFlag]
 
@@ -496,7 +496,7 @@ class Architecture(StrEnum):
 
         return a
 
-    def to_efi(self) -> Optional[str]:
+    def to_efi(self) -> str | None:
         return {
             Architecture.x86:         "ia32",
             Architecture.x86_64:      "x64",
@@ -507,7 +507,7 @@ class Architecture(StrEnum):
             Architecture.loongarch64: "loongarch64",
         }.get(self)  # fmt: skip
 
-    def to_grub(self) -> Optional[str]:
+    def to_grub(self) -> str | None:
         return {
             Architecture.x86_64: "x86_64",
             Architecture.x86:    "i386",
@@ -685,7 +685,7 @@ def expand_delayed_specifiers(specifiers: dict[str, str], text: str) -> str:
     return re.sub(r"&(?P<specifier>[&a-zA-Z])", replacer, text)
 
 
-def try_parse_boolean(s: str) -> Optional[bool]:
+def try_parse_boolean(s: str) -> bool | None:
     "Parse 1/true/yes/y/t/on as true and 0/false/no/n/f/off/None as false"
 
     s_l = s.lower()
@@ -798,14 +798,14 @@ def parse_paths_from_directory(
     return sorted(parse_path(os.fspath(p), resolve=resolve, secret=secret) for p in path.glob(glob))
 
 
-def config_parse_key(value: Optional[str], old: Optional[str]) -> Optional[Path]:
+def config_parse_key(value: str | None, old: str | None) -> Path | None:
     if not value:
         return None
 
     return parse_path(value, secret=True) if Path(value).exists() else Path(value)
 
 
-def config_parse_certificate(value: Optional[str], old: Optional[str]) -> Optional[Path]:
+def config_parse_certificate(value: str | None, old: str | None) -> Path | None:
     if not value:
         return None
 
@@ -854,7 +854,7 @@ def config_make_list_matcher(parse: Callable[[str], T]) -> ConfigMatchCallback[l
     return config_match_list
 
 
-def config_parse_string(value: Optional[str], old: Optional[str]) -> Optional[str]:
+def config_parse_string(value: str | None, old: str | None) -> str | None:
     return value or None
 
 
@@ -876,7 +876,7 @@ def config_match_key_value(match: str, value: dict[str, str]) -> bool:
     return value.get(k, None) == v
 
 
-def config_parse_boolean(value: Optional[str], old: Optional[bool]) -> Optional[bool]:
+def config_parse_boolean(value: str | None, old: bool | None) -> bool | None:
     if value is None:
         return False
 
@@ -893,7 +893,7 @@ def parse_feature(value: str) -> ConfigFeature:
         return ConfigFeature.enabled if parse_boolean(value) else ConfigFeature.disabled
 
 
-def config_parse_feature(value: Optional[str], old: Optional[ConfigFeature]) -> Optional[ConfigFeature]:
+def config_parse_feature(value: str | None, old: ConfigFeature | None) -> ConfigFeature | None:
     if value is None:
         return ConfigFeature.auto
 
@@ -907,7 +907,7 @@ def config_match_feature(match: str, value: ConfigFeature) -> bool:
     return value == parse_feature(match)
 
 
-def config_parse_compression(value: Optional[str], old: Optional[Compression]) -> Optional[Compression]:
+def config_parse_compression(value: str | None, old: Compression | None) -> Compression | None:
     if not value:
         return None
 
@@ -917,7 +917,7 @@ def config_parse_compression(value: Optional[str], old: Optional[Compression]) -
         return Compression.zstd if parse_boolean(value) else Compression.none
 
 
-def config_parse_uuid(value: Optional[str], old: Optional[str]) -> Optional[uuid.UUID]:
+def config_parse_uuid(value: str | None, old: str | None) -> uuid.UUID | None:
     if not value:
         return None
 
@@ -930,7 +930,7 @@ def config_parse_uuid(value: Optional[str], old: Optional[str]) -> Optional[uuid
         die(f"{value} is not a valid UUID")
 
 
-def config_parse_source_date_epoch(value: Optional[str], old: Optional[int]) -> Optional[int]:
+def config_parse_source_date_epoch(value: str | None, old: int | None) -> int | None:
     if not value:
         return None
 
@@ -945,7 +945,7 @@ def config_parse_source_date_epoch(value: Optional[str], old: Optional[int]) ->
     return timestamp
 
 
-def config_parse_compress_level(value: Optional[str], old: Optional[int]) -> Optional[int]:
+def config_parse_compress_level(value: str | None, old: int | None) -> int | None:
     if not value:
         return None
 
@@ -960,7 +960,7 @@ def config_parse_compress_level(value: Optional[str], old: Optional[int]) -> Opt
     return level
 
 
-def config_parse_mode(value: Optional[str], old: Optional[int]) -> Optional[int]:
+def config_parse_mode(value: str | None, old: int | None) -> int | None:
     if not value:
         return None
 
@@ -1022,8 +1022,8 @@ def config_default_distribution(namespace: dict[str, Any]) -> Distribution:
 
 
 def config_default_release(namespace: dict[str, Any]) -> str:
-    hd: Union[Distribution, str, None]
-    hr: Optional[str]
+    hd: Distribution | str | None
+    hr: str | None
 
     if (
         (d := os.getenv("MKOSI_HOST_DISTRIBUTION"))
@@ -1069,7 +1069,7 @@ def config_default_repository_key_fetch(namespace: dict[str, Any]) -> bool:
     )
 
 
-def config_default_source_date_epoch(namespace: dict[str, Any]) -> Optional[int]:
+def config_default_source_date_epoch(namespace: dict[str, Any]) -> int | None:
     for env in namespace["environment"]:
         if s := startswith(env, "SOURCE_DATE_EPOCH="):
             break
@@ -1078,7 +1078,7 @@ def config_default_source_date_epoch(namespace: dict[str, Any]) -> Optional[int]
     return config_parse_source_date_epoch(s, None)
 
 
-def config_default_proxy_url(namespace: dict[str, Any]) -> Optional[str]:
+def config_default_proxy_url(namespace: dict[str, Any]) -> str | None:
     names = ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY")
 
     for env in namespace["environment"]:
@@ -1093,7 +1093,7 @@ def config_default_proxy_url(namespace: dict[str, Any]) -> Optional[str]:
     return None
 
 
-def config_default_proxy_exclude(namespace: dict[str, Any]) -> Optional[list[str]]:
+def config_default_proxy_exclude(namespace: dict[str, Any]) -> list[str] | None:
     names = ("no_proxy", "NO_PROXY")
 
     for env in namespace["environment"]:
@@ -1108,7 +1108,7 @@ def config_default_proxy_exclude(namespace: dict[str, Any]) -> Optional[list[str
     return None
 
 
-def config_default_proxy_peer_certificate(namespace: dict[str, Any]) -> Optional[Path]:
+def config_default_proxy_peer_certificate(namespace: dict[str, Any]) -> Path | None:
     for p in (Path("/etc/pki/tls/certs/ca-bundle.crt"), Path("/etc/ssl/certs/ca-certificates.crt")):
         if p.exists():
             return p
@@ -1152,14 +1152,14 @@ def make_enum_parser(type: type[SE]) -> Callable[[str], SE]:
 
 
 def config_make_enum_parser(type: type[SE]) -> ConfigParseCallback[SE]:
-    def config_parse_enum(value: Optional[str], old: Optional[SE]) -> Optional[SE]:
+    def config_parse_enum(value: str | None, old: SE | None) -> SE | None:
         return make_enum_parser(type)(value) if value else None
 
     return config_parse_enum
 
 
 def config_make_enum_parser_with_boolean(type: type[SE], *, yes: SE, no: SE) -> ConfigParseCallback[SE]:
-    def config_parse_enum(value: Optional[str], old: Optional[SE]) -> Optional[SE]:
+    def config_parse_enum(value: str | None, old: SE | None) -> SE | None:
         if not value:
             return None
 
@@ -1197,13 +1197,13 @@ def package_sort_key(package: str) -> tuple[int, str]:
 
 def config_make_list_parser(
     *,
-    delimiter: Optional[str] = None,
+    delimiter: str | None = None,
     parse: Callable[[str], T] = str,  # type: ignore # see mypy#3737
     unescape: bool = False,
     reset: bool = True,
-    key: Optional[Callable[[T], Any]] = None,
+    key: Callable[[T], Any] | None = None,
 ) -> ConfigParseCallback[list[T]]:
-    def config_parse_list(value: Optional[str], old: Optional[list[T]]) -> Optional[list[T]]:
+    def config_parse_list(value: str | None, old: list[T] | None) -> list[T] | None:
         new = old.copy() if old else []
 
         if value is None:
@@ -1265,16 +1265,16 @@ def config_match_version(match: str, value: str) -> bool:
 
 def config_make_dict_parser(
     *,
-    delimiter: Optional[str] = None,
+    delimiter: str | None = None,
     parse: Callable[[str], tuple[str, PathString]],
     unescape: bool = False,
     allow_paths: bool = False,
     reset: bool = True,
 ) -> ConfigParseCallback[dict[str, PathString]]:
     def config_parse_dict(
-        value: Optional[str],
-        old: Optional[dict[str, PathString]],
-    ) -> Optional[dict[str, PathString]]:
+        value: str | None,
+        old: dict[str, PathString] | None,
+    ) -> dict[str, PathString] | None:
         new = old.copy() if old else {}
 
         if value is None:
@@ -1357,7 +1357,7 @@ def config_make_path_parser(
     absolute: bool = False,
     constants: Sequence[str] = (),
 ) -> ConfigParseCallback[Path]:
-    def config_parse_path(value: Optional[str], old: Optional[Path]) -> Optional[Path]:
+    def config_parse_path(value: str | None, old: Path | None) -> Path | None:
         if not value:
             return None
 
@@ -1381,7 +1381,7 @@ def is_valid_filename(s: str) -> bool:
 
 
 def config_make_filename_parser(hint: str) -> ConfigParseCallback[str]:
-    def config_parse_filename(value: Optional[str], old: Optional[str]) -> Optional[str]:
+    def config_parse_filename(value: str | None, old: str | None) -> str | None:
         if not value:
             return None
 
@@ -1404,8 +1404,9 @@ def match_path_exists(image: str, value: str) -> bool:
 
 
 def config_parse_root_password(
-    value: Optional[str], old: Optional[tuple[str, bool]]
-) -> Optional[tuple[str, bool]]:
+    value: str | None,
+    old: tuple[str, bool] | None,
+) -> tuple[str, bool] | None:
     if not value:
         return None
 
@@ -1456,14 +1457,14 @@ def parse_bytes(value: str) -> int:
     return result
 
 
-def config_parse_bytes(value: Optional[str], old: Optional[int] = None) -> Optional[int]:
+def config_parse_bytes(value: str | None, old: int | None = None) -> int | None:
     if not value:
         return None
 
     return parse_bytes(value)
 
 
-def config_parse_number(value: Optional[str], old: Optional[int] = None) -> Optional[int]:
+def config_parse_number(value: str | None, old: int | None = None) -> int | None:
     if not value:
         return None
 
@@ -1512,7 +1513,7 @@ def parse_drive(value: str) -> Drive:
     )
 
 
-def config_parse_sector_size(value: Optional[str], old: Optional[int]) -> Optional[int]:
+def config_parse_sector_size(value: str | None, old: int | None) -> int | None:
     if not value:
         return None
 
@@ -1530,7 +1531,7 @@ def config_parse_sector_size(value: Optional[str], old: Optional[int]) -> Option
     return size
 
 
-def config_parse_vsock_cid(value: Optional[str], old: Optional[int]) -> Optional[int]:
+def config_parse_vsock_cid(value: str | None, old: int | None) -> int | None:
     if not value:
         return None
 
@@ -1551,7 +1552,7 @@ def config_parse_vsock_cid(value: Optional[str], old: Optional[int]) -> Optional
     return cid
 
 
-def config_parse_minimum_version(value: Optional[str], old: Optional[str]) -> Optional[str]:
+def config_parse_minimum_version(value: str | None, old: str | None) -> str | None:
     if not value:
         return old
 
@@ -1628,7 +1629,7 @@ class KeySource:
         return f"{self.type}:{self.source}" if self.source else str(self.type)
 
 
-def config_parse_key_source(value: Optional[str], old: Optional[KeySource]) -> Optional[KeySource]:
+def config_parse_key_source(value: str | None, old: KeySource | None) -> KeySource | None:
     if not value:
         return KeySource(type=KeySourceType.file)
 
@@ -1656,9 +1657,9 @@ class CertificateSource:
 
 
 def config_parse_certificate_source(
-    value: Optional[str],
-    old: Optional[CertificateSource],
-) -> Optional[CertificateSource]:
+    value: str | None,
+    old: CertificateSource | None,
+) -> CertificateSource | None:
     if not value:
         return CertificateSource(type=CertificateSourceType.file)
 
@@ -1672,8 +1673,8 @@ def config_parse_certificate_source(
 
 
 def config_parse_artifact_output_list(
-    value: Optional[str], old: Optional[list[ArtifactOutput]]
-) -> Optional[list[ArtifactOutput]]:
+    value: str | None, old: list[ArtifactOutput] | None
+) -> list[ArtifactOutput] | None:
     if not value:
         return []
 
@@ -1721,10 +1722,10 @@ class ConfigSetting(Generic[T]):
     dest: str
     section: str
     parse: ConfigParseCallback[T] = config_parse_string  # type: ignore # see mypy#3737
-    match: Optional[ConfigMatchCallback[T]] = None
+    match: ConfigMatchCallback[T] | None = None
     name: str = ""
-    default: Optional[T] = None
-    default_factory: Optional[ConfigDefaultCallback[T]] = None
+    default: T | None = None
+    default_factory: ConfigDefaultCallback[T] | None = None
     default_factory_depends: tuple[str, ...] = tuple()
     path_suffixes: tuple[str, ...] = ()
     recursive_path_suffixes: tuple[str, ...] = ()
@@ -1734,12 +1735,12 @@ class ConfigSetting(Generic[T]):
     scope: SettingScope = SettingScope.local
 
     # settings for argparse
-    short: Optional[str] = None
+    short: str | None = None
     long: str = ""
-    choices: Optional[list[str]] = None
-    metavar: Optional[str] = None
-    const: Optional[Any] = None
-    help: Optional[str] = None
+    choices: list[str] | None = None
+    metavar: str | None = None
+    const: Any | None = None
+    help: str | None = None
 
     # backward compatibility
     compat_names: tuple[str, ...] = ()
@@ -1792,7 +1793,7 @@ class CustomHelpFormatter(argparse.HelpFormatter):
         )
 
 
-def parse_chdir(path: str) -> Optional[Path]:
+def parse_chdir(path: str) -> Path | None:
     if not path:
         # The current directory should be ignored
         return None
@@ -1817,9 +1818,9 @@ class IgnoreAction(argparse.Action):
         self,
         option_strings: Sequence[str],
         dest: str,
-        nargs: Union[int, str, None] = None,
+        nargs: int | str | None = None,
         default: Any = argparse.SUPPRESS,
-        help: Optional[str] = argparse.SUPPRESS,
+        help: str | None = argparse.SUPPRESS,
     ) -> None:
         super().__init__(option_strings, dest, nargs=nargs, default=default, help=help)
 
@@ -1827,8 +1828,8 @@ class IgnoreAction(argparse.Action):
         self,
         parser: argparse.ArgumentParser,
         namespace: argparse.Namespace,
-        values: Union[str, Sequence[Any], None],
-        option_string: Optional[str] = None,
+        values: str | Sequence[Any] | None,
+        option_string: str | None = None,
     ) -> None:
         logging.warning(f"{option_string} is no longer supported")
 
@@ -1838,8 +1839,8 @@ class PagerHelpAction(argparse._HelpAction):
         self,
         parser: argparse.ArgumentParser,
         namespace: argparse.Namespace,
-        values: Union[str, Sequence[Any], None] = None,
-        option_string: Optional[str] = None,
+        values: str | Sequence[Any] | None = None,
+        option_string: str | None = None,
     ) -> None:
         page(parser.format_help(), namespace.pager)
         parser.exit()
@@ -1859,7 +1860,7 @@ class Args:
     verb: Verb
     cmdline: list[str]
     force: int
-    directory: Optional[Path]
+    directory: Path | None
     debug: bool
     debug_shell: bool
     debug_workspace: bool
@@ -1898,7 +1899,7 @@ class Args:
         return dataclasses.asdict(self, dict_factory=dict_with_capitalised_keys_factory)
 
     @classmethod
-    def from_json(cls, s: Union[str, dict[str, Any], SupportsRead[str], SupportsRead[bytes]]) -> "Args":
+    def from_json(cls, s: str | dict[str, Any] | SupportsRead[str] | SupportsRead[bytes]) -> "Args":
         """Instantiate a Args object from a (partial) JSON dump."""
 
         if isinstance(s, str):
@@ -2006,15 +2007,15 @@ class Config:
     profiles: list[str]
     files: list[Path]
     dependencies: list[str]
-    minimum_version: Optional[str]
+    minimum_version: str | None
     pass_environment: list[str]
 
     distribution: Distribution
     release: str
     architecture: Architecture
-    mirror: Optional[str]
-    snapshot: Optional[str]
-    local_mirror: Optional[str]
+    mirror: str | None
+    snapshot: str | None
+    local_mirror: str | None
     repository_key_check: bool
     repository_key_fetch: bool
     repositories: list[str]
@@ -2023,19 +2024,19 @@ class Config:
     manifest_format: list[ManifestFormat]
     output: str
     output_extension: str
-    output_size: Optional[int]
+    output_size: int | None
     compress_output: Compression
     compress_level: int
-    output_dir: Optional[Path]
-    output_mode: Optional[int]
-    image_id: Optional[str]
-    image_version: Optional[str]
+    output_dir: Path | None
+    output_mode: int | None
+    image_id: str | None
+    image_version: str | None
     oci_labels: dict[str, str]
     oci_annotations: dict[str, str]
     split_artifacts: list[ArtifactOutput]
     repart_dirs: list[Path]
-    sysupdate_dir: Optional[Path]
-    sector_size: Optional[int]
+    sysupdate_dir: Path | None
+    sector_size: int | None
     overlay: bool
     seed: uuid.UUID
 
@@ -2054,7 +2055,7 @@ class Config:
     remove_packages: list[str]
     remove_files: list[str]
     clean_package_metadata: ConfigFeature
-    source_date_epoch: Optional[int]
+    source_date_epoch: int | None
 
     configure_scripts: list[Path]
     sync_scripts: list[Path]
@@ -2078,7 +2079,7 @@ class Config:
     initrd_volatile_packages: list[str]
     microcode_host: bool
     devicetrees: list[str]
-    splash: Optional[Path]
+    splash: Path | None
     kernel_command_line: list[str]
     kernel_modules_include: list[str]
     kernel_modules_exclude: list[str]
@@ -2091,14 +2092,14 @@ class Config:
     kernel_modules_initrd_exclude: list[str]
     kernel_modules_initrd_include_host: bool
 
-    locale: Optional[str]
-    locale_messages: Optional[str]
-    keymap: Optional[str]
-    timezone: Optional[str]
-    hostname: Optional[str]
-    root_password: Optional[tuple[str, bool]]
-    root_shell: Optional[str]
-    machine_id: Optional[uuid.UUID]
+    locale: str | None
+    locale_messages: str | None
+    keymap: str | None
+    timezone: str | None
+    hostname: str | None
+    root_password: tuple[str, bool] | None
+    root_shell: str | None
+    machine_id: uuid.UUID | None
 
     autologin: bool
     make_initrd: bool
@@ -2107,38 +2108,38 @@ class Config:
 
     secure_boot: bool
     secure_boot_auto_enroll: bool
-    secure_boot_key: Optional[Path]
+    secure_boot_key: Path | None
     secure_boot_key_source: KeySource
-    secure_boot_certificate: Optional[Path]
+    secure_boot_certificate: Path | None
     secure_boot_certificate_source: CertificateSource
     secure_boot_sign_tool: SecureBootSignTool
     verity: Verity
-    verity_key: Optional[Path]
+    verity_key: Path | None
     verity_key_source: KeySource
-    verity_certificate: Optional[Path]
+    verity_certificate: Path | None
     verity_certificate_source: CertificateSource
     sign_expected_pcr: ConfigFeature
-    sign_expected_pcr_key: Optional[Path]
+    sign_expected_pcr_key: Path | None
     sign_expected_pcr_key_source: KeySource
-    sign_expected_pcr_certificate: Optional[Path]
+    sign_expected_pcr_certificate: Path | None
     sign_expected_pcr_certificate_source: CertificateSource
-    passphrase: Optional[Path]
+    passphrase: Path | None
     checksum: bool
     sign: bool
     openpgp_tool: str
-    key: Optional[str]
+    key: str | None
 
-    tools_tree: Optional[Path]
+    tools_tree: Path | None
     tools_tree_certificates: bool
     extra_search_paths: list[Path]
     incremental: Incremental
     cacheonly: Cacheonly
     sandbox_trees: list[ConfigTree]
-    workspace_dir: Optional[Path]
-    cache_dir: Optional[Path]
+    workspace_dir: Path | None
+    cache_dir: Path | None
     cache_key: str
-    package_cache_dir: Optional[Path]
-    build_dir: Optional[Path]
+    package_cache_dir: Path | None
+    build_dir: Path | None
     build_key: str
     use_subvolumes: ConfigFeature
     repart_offline: bool
@@ -2149,28 +2150,28 @@ class Config:
     environment_files: list[Path]
     with_tests: bool
     with_network: bool
-    proxy_url: Optional[str]
+    proxy_url: str | None
     proxy_exclude: list[str]
-    proxy_peer_certificate: Optional[Path]
-    proxy_client_certificate: Optional[Path]
-    proxy_client_key: Optional[Path]
+    proxy_peer_certificate: Path | None
+    proxy_client_certificate: Path | None
+    proxy_client_key: Path | None
     make_scripts_executable: bool
 
-    nspawn_settings: Optional[Path]
+    nspawn_settings: Path | None
     ephemeral: bool
     credentials: dict[str, PathString]
     kernel_command_line_extra: list[str]
     register: ConfigFeature
     storage_target_mode: ConfigFeature
     runtime_trees: list[ConfigTree]
-    runtime_size: Optional[int]
+    runtime_size: int | None
     runtime_network: Network
     runtime_build_sources: bool
     bind_user: bool
-    ssh_key: Optional[Path]
-    ssh_certificate: Optional[Path]
-    machine: Optional[str]
-    forward_journal: Optional[Path]
+    ssh_key: Path | None
+    ssh_certificate: Path | None
+    machine: str | None
+    forward_journal: Path | None
 
     vmm: Vmm
     console: ConsoleMode
@@ -2184,8 +2185,8 @@ class Config:
     tpm: ConfigFeature
     removable: bool
     firmware: Firmware
-    firmware_variables: Optional[Path]
-    linux: Optional[str]
+    firmware_variables: Path | None
+    linux: str | None
     drives: list[Drive]
     qemu_args: list[str]
 
@@ -2472,7 +2473,7 @@ class Config:
     @classmethod
     def from_partial_json(
         cls,
-        s: Union[str, dict[str, Any], SupportsRead[str], SupportsRead[bytes]],
+        s: str | dict[str, Any] | SupportsRead[str] | SupportsRead[bytes],
     ) -> dict[str, Any]:
         """Instantiate a Config object from a (partial) JSON dump."""
         if isinstance(s, str):
@@ -2506,12 +2507,12 @@ class Config:
         return {(tk := key_transformer(k)): value_transformer(tk, v) for k, v in j.items()}
 
     @classmethod
-    def from_json(cls, s: Union[str, dict[str, Any], SupportsRead[str], SupportsRead[bytes]]) -> "Config":
+    def from_json(cls, s: str | dict[str, Any] | SupportsRead[str] | SupportsRead[bytes]) -> "Config":
         return dataclasses.replace(
             cls.default(), **{k: v for k, v in cls.from_partial_json(s).items() if k in cls.fields()}
         )
 
-    def find_binary(self, *names: PathString, tools: bool = True) -> Optional[Path]:
+    def find_binary(self, *names: PathString, tools: bool = True) -> Path | None:
         return find_binary(*names, root=self.tools() if tools else Path("/"), extra=self.extra_search_paths)
 
     def sandbox(
@@ -2521,8 +2522,8 @@ class Config:
         devices: bool = False,
         relaxed: bool = False,
         tools: bool = True,
-        scripts: Optional[Path] = None,
-        overlay: Optional[Path] = None,
+        scripts: Path | None = None,
+        overlay: Path | None = None,
         options: Sequence[PathString] = (),
     ) -> AbstractContextManager[list[PathString]]:
         opt: list[PathString] = [*options]
@@ -2554,9 +2555,9 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
     We have our own parser instead of using configparser as the latter does not support specifying the same
     setting multiple times in the same configuration file.
     """
-    section: Optional[str] = None
-    setting: Optional[str] = None
-    value: Optional[str] = None
+    section: str | None = None
+    setting: str | None = None
+    value: str | None = None
 
     for line in textwrap.dedent(path.read_text()).splitlines():
         comment = line.find("#")
@@ -4597,7 +4598,7 @@ def create_argument_parser(chdir: bool = True) -> argparse.ArgumentParser:
         help=argparse.SUPPRESS,
     )
 
-    last_section: Optional[str] = None
+    last_section: str | None = None
 
     for s in SETTINGS:
         if s.section != last_section:
@@ -4664,8 +4665,8 @@ class ConfigAction(argparse.Action):
         self,
         parser: argparse.ArgumentParser,
         namespace: argparse.Namespace,
-        values: Union[str, Sequence[Any], None],
-        option_string: Optional[str] = None,
+        values: str | Sequence[Any] | None,
+        option_string: str | None = None,
     ) -> None:
         assert option_string is not None
 
@@ -4800,11 +4801,11 @@ class ParseContext:
             with chdir(path if path.is_dir() else Path.cwd()):
                 self.parse_config_one(path if path.is_file() else Path.cwd(), parse_profiles=path.is_dir())
 
-    def finalize_value(self, setting: ConfigSetting[T]) -> Optional[T]:
+    def finalize_value(self, setting: ConfigSetting[T]) -> T | None:
         # If a value was specified on the CLI, it always takes priority. If the setting is a collection of
         # values, we merge the value from the CLI with the value from the configuration, making sure that the
         # value from the CLI always takes priority.
-        if (v := cast(Optional[T], self.cli.get(setting.dest))) is not None:
+        if (v := cast(T | None, self.cli.get(setting.dest))) is not None:
             cfg_value = self.config.get(setting.dest)
             # We either have no corresponding value in the config files
             # or the values was assigned the empty string on the CLI
@@ -4834,7 +4835,7 @@ class ParseContext:
         if (
             setting.dest not in self.cli
             and setting.dest in self.config
-            and (v := cast(Optional[T], self.config[setting.dest])) is not None
+            and (v := cast(T | None, self.config[setting.dest])) is not None
         ):
             return v
 
@@ -4877,8 +4878,8 @@ class ParseContext:
         return default
 
     def match_config(self, path: Path, asserts: bool = False) -> bool:
-        condition_triggered: Optional[bool] = None
-        match_triggered: Optional[bool] = None
+        condition_triggered: bool | None = None
+        match_triggered: bool | None = None
         skip = False
 
         # If the config file does not exist, we assume it matches so that we look at the other files in the
@@ -4959,7 +4960,7 @@ class ParseContext:
         return match_triggered is not False
 
     def parse_config_one(self, path: Path, parse_profiles: bool = False, parse_local: bool = False) -> bool:
-        s: Optional[ConfigSetting[object]]  # Hint to mypy that we might assign None
+        s: ConfigSetting[object] | None  # Hint to mypy that we might assign None
         assert path.is_absolute()
 
         extras = path.is_dir()
@@ -5145,7 +5146,7 @@ def finalize_default_tools(
     main: ParseContext,
     finalized: dict[str, Any],
     *,
-    configdir: Optional[Path],
+    configdir: Path | None,
     resources: Path,
 ) -> Config:
     context = ParseContext(resources)
@@ -5238,7 +5239,7 @@ def finalize_default_initrd(
     return Config.from_dict(context.finalize())
 
 
-def finalize_configdir(directory: Optional[Path]) -> Optional[Path]:
+def finalize_configdir(directory: Path | None) -> Path | None:
     """Allow locating all mkosi configuration in a mkosi/ subdirectory
     instead of in the top-level directory of a git repository.
     """
@@ -5308,7 +5309,7 @@ def parse_config(
     argv: Sequence[str] = (),
     *,
     resources: Path = Path("/"),
-) -> tuple[Args, Optional[Config], tuple[Config, ...]]:
+) -> tuple[Args, Config | None, tuple[Config, ...]]:
     argv = list(argv)
 
     context = ParseContext(resources)
@@ -5421,7 +5422,7 @@ def parse_config(
     # To make this work, we can't use default_factory as it is evaluated too early, so
     # we check here to see if dependencies were explicitly provided and if not we gather
     # the list of default dependencies while we parse the subimages.
-    dependencies: Optional[list[str]] = (
+    dependencies: list[str] | None = (
         None if "dependencies" in context.cli or "dependencies" in context.config else []
     )
 
@@ -5536,7 +5537,7 @@ def finalize_term() -> str:
     return term if sys.stderr.isatty() else "dumb"
 
 
-def finalize_git_config(proxy_url: Optional[str], env: dict[str, str]) -> dict[str, str]:
+def finalize_git_config(proxy_url: str | None, env: dict[str, str]) -> dict[str, str]:
     if proxy_url is None:
         return {}
 
@@ -5561,19 +5562,19 @@ def yes_no(b: bool) -> str:
     return "yes" if b else "no"
 
 
-def none_to_na(s: Optional[object]) -> str:
+def none_to_na(s: object | None) -> str:
     return "n/a" if s is None else str(s)
 
 
-def none_to_random(s: Optional[object]) -> str:
+def none_to_random(s: object | None) -> str:
     return "random" if s is None else str(s)
 
 
-def none_to_none(s: Optional[object]) -> str:
+def none_to_none(s: object | None) -> str:
     return "none" if s is None else str(s)
 
 
-def none_to_default(s: Optional[object]) -> str:
+def none_to_default(s: object | None) -> str:
     return "default" if s is None else str(s)
 
 
@@ -5592,7 +5593,7 @@ def format_bytes(num_bytes: int) -> str:
     return f"{num_bytes}B"
 
 
-def format_bytes_or_none(num_bytes: Optional[int]) -> str:
+def format_bytes_or_none(num_bytes: int | None) -> str:
     return format_bytes(num_bytes) if num_bytes is not None else "none"
 
 
@@ -5600,7 +5601,7 @@ def format_octal(oct_value: int) -> str:
     return f"{oct_value:>04o}"
 
 
-def format_octal_or_default(oct_value: Optional[int]) -> str:
+def format_octal_or_default(oct_value: int | None) -> str:
     return format_octal(oct_value) if oct_value is not None else "default"
 
 
@@ -5879,20 +5880,20 @@ class JsonEncoder(json.JSONEncoder):
         return super().default(o)
 
 
-def dump_json(dict: dict[str, Any], indent: Optional[int] = 4) -> str:
+def dump_json(dict: dict[str, Any], indent: int | None = 4) -> str:
     return json.dumps(dict, cls=JsonEncoder, indent=indent, sort_keys=True)
 
 
 E = TypeVar("E", bound=StrEnum)
 
 
-def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[str, Any], Any]:
+def json_type_transformer(refcls: type[Args] | type[Config]) -> Callable[[str, Any], Any]:
     fields_by_name = {field.name: field for field in dataclasses.fields(refcls)}
 
     def path_transformer(path: str, fieldtype: type[Path]) -> Path:
         return Path(path)
 
-    def optional_path_transformer(path: Optional[str], fieldtype: type[Optional[Path]]) -> Optional[Path]:
+    def optional_path_transformer(path: str | None, fieldtype: type[Path | None]) -> Path | None:
         return Path(path) if path is not None else None
 
     def path_list_transformer(pathlist: list[str], fieldtype: type[list[Path]]) -> list[Path]:
@@ -5902,13 +5903,13 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[
         return uuid.UUID(uuidstr)
 
     def optional_uuid_transformer(
-        uuidstr: Optional[str], fieldtype: type[Optional[uuid.UUID]]
-    ) -> Optional[uuid.UUID]:
+        uuidstr: str | None, fieldtype: type[uuid.UUID | None]
+    ) -> uuid.UUID | None:
         return uuid.UUID(uuidstr) if uuidstr is not None else None
 
     def root_password_transformer(
-        rootpw: Optional[list[Union[str, bool]]], fieldtype: type[Optional[tuple[str, bool]]]
-    ) -> Optional[tuple[str, bool]]:
+        rootpw: list[str | bool] | None, fieldtype: type[tuple[str, bool] | None]
+    ) -> tuple[str, bool] | None:
         if rootpw is None:
             return None
         return (cast(str, rootpw[0]), cast(bool, rootpw[1]))
@@ -5932,7 +5933,7 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[
     def enum_transformer(enumval: str, fieldtype: type[E]) -> E:
         return fieldtype(enumval)
 
-    def optional_enum_transformer(enumval: Optional[str], fieldtype: type[Optional[E]]) -> Optional[E]:
+    def optional_enum_transformer(enumval: str | None, fieldtype: type[E | None]) -> E | None:
         return typing.get_args(fieldtype)[0](enumval) if enumval is not None else None
 
     def enum_list_transformer(enumlist: list[str], fieldtype: type[list[E]]) -> list[E]:
@@ -5960,9 +5961,9 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[
         return ret
 
     def generic_version_transformer(
-        version: Optional[str],
-        fieldtype: type[Optional[GenericVersion]],
-    ) -> Optional[GenericVersion]:
+        version: str | None,
+        fieldtype: type[GenericVersion | None],
+    ) -> GenericVersion | None:
         return GenericVersion(version) if version is not None else None
 
     def certificate_source_transformer(
@@ -6003,11 +6004,11 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[
     # to shut up the type checkers and rely on the tests.
     transformers: dict[Any, Callable[[Any, Any], Any]] = {
         Path: path_transformer,
-        Optional[Path]: optional_path_transformer,
+        Path | None: optional_path_transformer,
         list[Path]: path_list_transformer,
         uuid.UUID: uuid_transformer,
-        Optional[uuid.UUID]: optional_uuid_transformer,
-        Optional[tuple[str, bool]]: root_password_transformer,
+        uuid.UUID | None: optional_uuid_transformer,
+        tuple[str, bool] | None: root_password_transformer,
         list[ConfigTree]: config_tree_transformer,
         Architecture: enum_transformer,
         BiosBootloader: enum_transformer,
@@ -6022,7 +6023,7 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[
         SecureBootSignTool: enum_transformer,
         Incremental: enum_transformer,
         BuildSourcesEphemeral: enum_transformer,
-        Optional[Distribution]: optional_enum_transformer,
+        Distribution | None: optional_enum_transformer,
         list[ManifestFormat]: enum_list_transformer,
         Verb: enum_transformer,
         DocFormat: enum_transformer,
@@ -6041,7 +6042,7 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[
     }
 
     def json_transformer(key: str, val: Any) -> Any:
-        fieldtype: Optional[dataclasses.Field[Any]] = fields_by_name.get(key)
+        fieldtype: dataclasses.Field[Any] | None = fields_by_name.get(key)
         # It is unlikely that the type of a field will be None only, so let's not bother with a different
         # sentinel value
         if fieldtype is None:
@@ -6065,7 +6066,7 @@ def want_selinux_relabel(
     config: Config,
     root: Path,
     fatal: bool = True,
-) -> Optional[tuple[Path, str, Path, Path]]:
+) -> tuple[Path, str, Path, Path] | None:
     if config.selinux_relabel == ConfigFeature.disabled:
         return None
 
@@ -6152,8 +6153,8 @@ def systemd_tool_version(*tool: PathString, sandbox: SandboxProtocol = nosandbox
 def systemd_pty_forward(
     config: Config,
     *,
-    background: Optional[str] = None,
-    title: Optional[str] = None,
+    background: str | None = None,
+    title: str | None = None,
 ) -> list[str]:
     tint_bg = parse_boolean(config.environment.get("SYSTEMD_TINT_BACKGROUND", "1")) and parse_boolean(
         os.environ.get("SYSTEMD_TINT_BACKGROUND", "1")
index 665b75ec863404c67d881eab0f3ee0048d395b58..5263bf87c87df1e3841a94299f4adc30be7b3750 100644 (file)
@@ -4,7 +4,6 @@ import os
 from collections.abc import Sequence
 from contextlib import AbstractContextManager
 from pathlib import Path
-from typing import Optional
 
 from mkosi.config import Args, Config
 from mkosi.util import PathString, flatten
@@ -22,7 +21,7 @@ class Context:
         resources: Path,
         keyring_dir: Path,
         metadata_dir: Path,
-        package_dir: Optional[Path] = None,
+        package_dir: Path | None = None,
     ) -> None:
         self.args = args
         self.config = config
@@ -32,8 +31,8 @@ class Context:
         self.metadata_dir = metadata_dir
         self.package_dir = package_dir or (self.workspace / "packages")
         self.lowerdirs: list[PathString] = []
-        self.upperdir: Optional[PathString] = None
-        self.workdir: Optional[PathString] = None
+        self.upperdir: PathString | None = None
+        self.workdir: PathString | None = None
 
         self.package_dir.mkdir(exist_ok=True)
         self.staging.mkdir()
@@ -87,7 +86,7 @@ class Context:
         *,
         network: bool = False,
         devices: bool = False,
-        scripts: Optional[Path] = None,
+        scripts: Path | None = None,
         options: Sequence[PathString] = (),
     ) -> AbstractContextManager[list[PathString]]:
         return self.config.sandbox(
index 0db38229af278aeff134cfd7d4a97b97f723fc81..0a225a8afd4750de764d60a6fab55f08a8001d28 100644 (file)
@@ -3,7 +3,7 @@
 import os
 import subprocess
 from pathlib import Path
-from typing import Optional, overload
+from typing import overload
 
 from mkosi.config import Config
 from mkosi.mounts import finalize_certificate_mounts
@@ -15,7 +15,7 @@ def curl(
     config: Config,
     url: str,
     *,
-    output_dir: Optional[Path],
+    output_dir: Path | None,
     log: bool = True,
 ) -> None: ...
 
@@ -30,7 +30,7 @@ def curl(
 ) -> str: ...
 
 
-def curl(config: Config, url: str, *, output_dir: Optional[Path] = None, log: bool = True) -> Optional[str]:
+def curl(config: Config, url: str, *, output_dir: Path | None = None, log: bool = True) -> str | None:
     result = run(
         [
             "curl",
index 5d42b8f5b3e07f6b83c8961d18106d12b1ae3d0b..b9cb1c4ae447d894299a0471ece69d42551abdfe 100644 (file)
@@ -5,7 +5,7 @@ import importlib
 import urllib.parse
 from collections.abc import Sequence
 from pathlib import Path
-from typing import TYPE_CHECKING, Optional, Union
+from typing import TYPE_CHECKING
 
 from mkosi.log import die
 from mkosi.util import StrEnum, read_env_file
@@ -135,7 +135,7 @@ class DistributionInstaller:
         return ""
 
     @classmethod
-    def default_tools_tree_distribution(cls) -> Optional[Distribution]:
+    def default_tools_tree_distribution(cls) -> Distribution | None:
         return None
 
     @classmethod
@@ -151,7 +151,7 @@ class DistributionInstaller:
         return False
 
 
-def detect_distribution(root: Path = Path("/")) -> tuple[Union[Distribution, str, None], Optional[str]]:
+def detect_distribution(root: Path = Path("/")) -> tuple[Distribution | str | None, str | None]:
     try:
         os_release = read_env_file(root / "etc/os-release")
     except FileNotFoundError:
@@ -169,7 +169,7 @@ def detect_distribution(root: Path = Path("/")) -> tuple[Union[Distribution, str
         "azurelinux": "azure",
     }
 
-    d: Optional[Distribution] = None
+    d: Distribution | None = None
     for the_id in [dist_id, *dist_id_like]:
         if not the_id:
             continue
index 428c9b27a775efb4bfdc7265dda37598799f74df..2a929c1b344d8c3f08e478f4eb01dc656b0bb4a7 100644 (file)
@@ -4,7 +4,6 @@ import os
 import tempfile
 from collections.abc import Iterable, Sequence
 from pathlib import Path
-from typing import Union
 from xml.etree import ElementTree
 
 from mkosi.config import Architecture, Config, parse_ini
@@ -42,7 +41,7 @@ class Installer(DistributionInstaller, distribution=Distribution.opensuse):
         return "grub2"
 
     @classmethod
-    def package_manager(cls, config: Config) -> Union[type[Dnf], type[Zypper]]:
+    def package_manager(cls, config: Config) -> type[Dnf] | type[Zypper]:
         if config.find_binary("zypper"):
             return Zypper
         else:
index cf7aa7fa4ec253d8529f7c141e71356d45d9bff8..752fd37d0310e4a011d271a391e98ccd48d518e8 100644 (file)
@@ -2,7 +2,7 @@
 
 from collections.abc import Iterable
 from pathlib import Path
-from typing import Any, Optional
+from typing import Any
 
 from mkosi.context import Context
 from mkosi.distribution import Distribution, centos, join_mirror
@@ -27,7 +27,7 @@ class Installer(centos.Installer, distribution=Distribution.rhel):
         )
 
     @staticmethod
-    def sslcacert(context: Context) -> Optional[Path]:
+    def sslcacert(context: Context) -> Path | None:
         if context.config.mirror:
             return None
 
@@ -41,7 +41,7 @@ class Installer(centos.Installer, distribution=Distribution.rhel):
         return path
 
     @staticmethod
-    def sslclientkey(context: Context) -> Optional[Path]:
+    def sslclientkey(context: Context) -> Path | None:
         if context.config.mirror:
             return None
 
@@ -56,7 +56,7 @@ class Installer(centos.Installer, distribution=Distribution.rhel):
         return paths[0]
 
     @staticmethod
-    def sslclientcert(context: Context) -> Optional[Path]:
+    def sslclientcert(context: Context) -> Path | None:
         if context.config.mirror:
             return None
 
index 5343a055dfbb1ae57fc3b41d9973de1314ff0557..fbdf2056c6a27afe4dfe7c173c904ffa8a2d498a 100644 (file)
@@ -11,7 +11,7 @@ import subprocess
 import sys
 import tempfile
 from pathlib import Path
-from typing import Optional, cast
+from typing import cast
 
 import mkosi.resources
 from mkosi.config import DocFormat, InitrdProfile, OutputFormat
@@ -33,8 +33,8 @@ class KernelInstallContext:
     staging_area: Path
     layout: str
     image_type: str
-    initrd_generator: Optional[str]
-    uki_generator: Optional[str]
+    initrd_generator: str | None
+    uki_generator: str | None
     verbose: bool
 
     @staticmethod
@@ -228,7 +228,7 @@ def vconsole_config() -> list[str]:
     ]
 
 
-def initrd_finalize(staging_dir: Path, output: str, output_dir: Optional[Path]) -> None:
+def initrd_finalize(staging_dir: Path, output: str, output_dir: Path | None) -> None:
     if output_dir:
         with umask(~0o700) if os.getuid() == 0 else cast(umask, contextlib.nullcontext()):
             Path(output_dir).mkdir(parents=True, exist_ok=True)
index a6338dfd27f671b091252cd8274c94f4e8d0319f..0c5903e7318df075a62b5d561ac2914167cb26cf 100644 (file)
@@ -4,7 +4,7 @@ import dataclasses
 import textwrap
 from collections.abc import Sequence
 from pathlib import Path
-from typing import Final, Optional
+from typing import Final
 
 from mkosi.config import Config, ConfigFeature
 from mkosi.context import Context
@@ -21,8 +21,8 @@ class AptRepository:
     url: str
     suite: str
     components: tuple[str, ...]
-    signedby: Optional[Path]
-    snapshot: Optional[str] = None
+    signedby: Path | None
+    snapshot: str | None = None
 
     def __str__(self) -> str:
         return textwrap.dedent(
index 670a77822e42c79eb491d03ada00d0f096be7967..5490505ceddd8b4b548f7f83d77051e1ad87c642 100644 (file)
@@ -3,7 +3,6 @@
 import textwrap
 from collections.abc import Sequence
 from pathlib import Path
-from typing import Optional
 
 from mkosi.config import Cacheonly, Config
 from mkosi.context import Context
@@ -63,7 +62,7 @@ class Dnf(PackageManager):
         context: Context,
         repositories: Sequence[RpmRepository],
         filelists: bool = True,
-        metadata_expire: Optional[str] = None,
+        metadata_expire: str | None = None,
     ) -> None:
         (context.sandbox_tree / "etc/dnf/vars").mkdir(parents=True, exist_ok=True)
         (context.sandbox_tree / "etc/yum.repos.d").mkdir(parents=True, exist_ok=True)
index a90c59cdf743f90bd8404acbab5e402d69331a8e..d67927aa2e842fed3e73a3732dad125e7d76ee1b 100644 (file)
@@ -3,7 +3,7 @@
 import dataclasses
 import textwrap
 from pathlib import Path
-from typing import Literal, Optional, overload
+from typing import Literal, overload
 
 from mkosi.context import Context
 from mkosi.distribution import Distribution
@@ -18,17 +18,17 @@ class RpmRepository:
     url: str
     gpgurls: tuple[str, ...]
     enabled: bool = True
-    sslcacert: Optional[Path] = None
-    sslclientkey: Optional[Path] = None
-    sslclientcert: Optional[Path] = None
-    priority: Optional[int] = None
+    sslcacert: Path | None = None
+    sslclientkey: Path | None = None
+    sslclientcert: Path | None = None
+    priority: int | None = None
 
 
 @overload
 def find_rpm_gpgkey(
     context: Context,
     key: str,
-    fallback: Optional[str] = None,
+    fallback: str | None = None,
     *,
     required: Literal[True] = True,
 ) -> str: ...
@@ -38,19 +38,19 @@ def find_rpm_gpgkey(
 def find_rpm_gpgkey(
     context: Context,
     key: str,
-    fallback: Optional[str] = None,
+    fallback: str | None = None,
     *,
     required: bool,
-) -> Optional[str]: ...
+) -> str | None: ...
 
 
 def find_rpm_gpgkey(
     context: Context,
     key: str,
-    fallback: Optional[str] = None,
+    fallback: str | None = None,
     *,
     required: bool = True,
-) -> Optional[str]:
+) -> str | None:
     # We assume here that GPG keys will only ever be relative symlinks and never absolute symlinks.
 
     paths = glob_in_sandbox(
@@ -78,7 +78,7 @@ def setup_rpm(
     context: Context,
     *,
     dbpath: str = "/usr/lib/sysimage/rpm",
-    dbbackend: Optional[str] = None,
+    dbbackend: str | None = None,
 ) -> None:
     confdir = context.sandbox_tree / "etc/rpm"
     confdir.mkdir(parents=True, exist_ok=True)
index ddfd185b2e8a75bb47fbe42a63cf69cde89a7d0b..cffa9bdb8cdf364056f642de33f4c635f07c6f1d 100644 (file)
@@ -7,7 +7,7 @@ import os
 import sys
 import time
 from collections.abc import Iterator
-from typing import Any, NoReturn, Optional
+from typing import Any, NoReturn
 
 from mkosi.sandbox import ANSI_BOLD, ANSI_GRAY, ANSI_RED, ANSI_RESET, ANSI_YELLOW, terminal_is_dumb
 
@@ -18,7 +18,7 @@ ARG_DEBUG_SANDBOX = contextvars.ContextVar("debug-sandbox", default=False)
 LEVEL = 0
 
 
-def die(message: str, *, hint: Optional[str] = None) -> NoReturn:
+def die(message: str, *, hint: str | None = None) -> NoReturn:
     logging.error(f"{message}")
     if hint:
         logging.info(f"({hint})")
@@ -95,7 +95,7 @@ def log_notice(text: str) -> None:
 
 
 @contextlib.contextmanager
-def complete_step(text: str, text2: Optional[str] = None) -> Iterator[list[Any]]:
+def complete_step(text: str, text2: str | None = None) -> Iterator[list[Any]]:
     global LEVEL
 
     log_step(text)
@@ -116,7 +116,7 @@ def complete_step(text: str, text2: Optional[str] = None) -> Iterator[list[Any]]
 
 
 class Formatter(logging.Formatter):
-    def __init__(self, fmt: Optional[str] = None, *args: Any, **kwargs: Any) -> None:
+    def __init__(self, fmt: str | None = None, *args: Any, **kwargs: Any) -> None:
         fmt = fmt or "%(message)s"
 
         self.formatters = {
index df363938614b7111020c2a34a8974bb6bcc8e07d..41e4c40c4b91f2eba0550d4c02d3bb67db7fcdb9 100644 (file)
@@ -5,7 +5,7 @@ import datetime
 import json
 import subprocess
 import textwrap
-from typing import IO, Any, Optional
+from typing import IO, Any
 
 from mkosi.config import ManifestFormat, OutputFormat
 from mkosi.context import Context
@@ -43,7 +43,7 @@ class PackageManifest:
 @dataclasses.dataclass
 class SourcePackageManifest:
     name: str
-    changelog: Optional[str]
+    changelog: str | None
     packages: list[PackageManifest] = dataclasses.field(default_factory=list)
 
     def add(self, package: PackageManifest) -> None:
index f15791890347a1b234d334dce4f199d51467666e..88ab5608acef8e8cb390d44771c4364fb6c39c7e 100644 (file)
@@ -6,7 +6,6 @@ import stat
 import tempfile
 from collections.abc import Iterator, Sequence
 from pathlib import Path
-from typing import Optional, Union
 
 from mkosi.config import BuildSourcesEphemeral, Config
 from mkosi.log import die
@@ -49,7 +48,7 @@ def mount_overlay(
     lowerdirs: Sequence[Path],
     dst: Path,
     *,
-    upperdir: Optional[Path] = None,
+    upperdir: Path | None = None,
 ) -> Iterator[Path]:
     with contextlib.ExitStack() as stack:
         if upperdir is None:
@@ -86,7 +85,7 @@ def mount_overlay(
 def finalize_source_mounts(
     config: Config,
     *,
-    ephemeral: Union[BuildSourcesEphemeral, bool],
+    ephemeral: BuildSourcesEphemeral | bool,
 ) -> Iterator[list[PathString]]:
     with contextlib.ExitStack() as stack:
         options: list[PathString] = []
index 7077fb0a4ccce3a299a07babedb5e8cc216f5e61..84de95983be47f2ecaada2f4c3f23a57dec3187a 100644 (file)
@@ -2,10 +2,9 @@
 
 import os
 import pydoc
-from typing import Optional
 
 
-def page(text: str, enabled: Optional[bool]) -> None:
+def page(text: str, enabled: bool | None) -> None:
     if enabled:
         # Initialize less options from $MKOSI_LESS or provide a suitable fallback.
         # F: don't page if one screen
index 535b6cdd527db22df9a042558bffea35904ae07d..397022179f3ab04ae02cdc7c56a6752bc00c5dd8 100644 (file)
@@ -5,7 +5,7 @@ import json
 import subprocess
 from collections.abc import Mapping, Sequence
 from pathlib import Path
-from typing import Any, Final, Optional
+from typing import Any, Final
 
 from mkosi.log import die
 from mkosi.run import SandboxProtocol, nosandbox, run, workdir
@@ -15,9 +15,9 @@ from mkosi.run import SandboxProtocol, nosandbox, run, workdir
 class Partition:
     type: str
     uuid: str
-    partno: Optional[int]
-    split_path: Optional[Path]
-    roothash: Optional[str]
+    partno: int | None
+    split_path: Path | None
+    roothash: str | None
 
     @classmethod
     def from_dict(cls, dict: Mapping[str, Any]) -> "Partition":
@@ -47,9 +47,9 @@ def find_partitions(image: Path, *, sandbox: SandboxProtocol = nosandbox) -> lis
     return [Partition.from_dict(d) for d in output]
 
 
-def finalize_roothash(partitions: Sequence[Partition]) -> Optional[str]:
-    roothash: Optional[str] = None
-    usrhash: Optional[str] = None
+def finalize_roothash(partitions: Sequence[Partition]) -> str | None:
+    roothash: str | None = None
+    usrhash: str | None = None
 
     for p in partitions:
         if (h := p.roothash) is None:
@@ -71,7 +71,7 @@ def finalize_roothash(partitions: Sequence[Partition]) -> Optional[str]:
     return f"roothash={roothash}" if roothash else f"usrhash={usrhash}" if usrhash else None
 
 
-def finalize_root(partitions: Sequence[Partition]) -> Optional[str]:
+def finalize_root(partitions: Sequence[Partition]) -> str | None:
     root = finalize_roothash(partitions)
     if not root:
         root = next((f"root=PARTUUID={p.uuid}" for p in partitions if p.type.startswith("root")), None)
index 6617677bfd5eaa5f3fb2446553737e13d91e93a9..8eac9a30939722f9992a333f84c69381390677ea 100644 (file)
@@ -25,7 +25,6 @@ import textwrap
 import uuid
 from collections.abc import Iterator, Sequence
 from pathlib import Path
-from typing import Optional
 
 from mkosi.bootloader import KernelType
 from mkosi.config import (
@@ -184,7 +183,7 @@ class OvmfConfig:
     vars_format: str
 
 
-def find_ovmf_firmware(config: Config, firmware: Firmware) -> Optional[OvmfConfig]:
+def find_ovmf_firmware(config: Config, firmware: Firmware) -> OvmfConfig | None:
     if not firmware.is_uefi():
         return None
 
@@ -301,7 +300,7 @@ def start_swtpm(config: Config) -> Iterator[Path]:
                 proc.terminate()
 
 
-def find_virtiofsd(*, root: Path = Path("/"), extra: Sequence[Path] = ()) -> Optional[Path]:
+def find_virtiofsd(*, root: Path = Path("/"), extra: Sequence[Path] = ()) -> Path | None:
     if p := find_binary("virtiofsd", root=root, extra=extra):
         return p
 
@@ -605,8 +604,8 @@ def qemu_version(config: Config, binary: Path) -> GenericVersion:
 
 def finalize_firmware(
     config: Config,
-    kernel: Optional[Path],
-    kerneltype: Optional[KernelType] = None,
+    kernel: Path | None,
+    kerneltype: KernelType | None = None,
 ) -> Firmware:
     if config.firmware != Firmware.auto:
         return config.firmware
@@ -732,7 +731,7 @@ def finalize_drive(config: Config, drive: Drive) -> Iterator[Path]:
 
 
 @contextlib.contextmanager
-def finalize_initrd(config: Config) -> Iterator[Optional[Path]]:
+def finalize_initrd(config: Config) -> Iterator[Path | None]:
     with contextlib.ExitStack() as stack:
         if (config.output_dir_or_cwd() / config.output_split_initrd).exists():
             yield config.output_dir_or_cwd() / config.output_split_initrd
@@ -902,7 +901,7 @@ def finalize_register(config: Config) -> bool:
     return True
 
 
-def register_machine(config: Config, pid: int, fname: Path, cid: Optional[int]) -> None:
+def register_machine(config: Config, pid: int, fname: Path, cid: int | None) -> None:
     if not finalize_register(config):
         return
 
@@ -1104,7 +1103,7 @@ def run_qemu(args: Args, config: Config) -> None:
 
     cmdline += ["-accel", accel]
 
-    cid: Optional[int] = None
+    cid: int | None = None
     if QemuDeviceNode.vhost_vsock in qemu_device_fds:
         if config.vsock_cid == VsockCID.auto:
             cid = find_unused_vsock_cid(config, qemu_device_fds[QemuDeviceNode.vhost_vsock])
@@ -1152,8 +1151,8 @@ def run_qemu(args: Args, config: Config) -> None:
         assert ovmf
         cmdline += ["-drive", f"if=pflash,format={ovmf.format},readonly=on,file={ovmf.firmware}"]
 
-    vsock: Optional[socket.socket] = None
-    notify: Optional[AsyncioThread[tuple[str, str]]] = None
+    vsock: socket.socket | None = None
+    notify: AsyncioThread[tuple[str, str]] | None = None
 
     with contextlib.ExitStack() as stack:
         if firmware.is_uefi():
index 2081b52119b86a27551efa6d58a38d46982c9a06..405f17c0c03b30bb80002b7c84bd1f76f0377286 100644 (file)
@@ -3301,7 +3301,7 @@ In this scenario, the kernel is loaded from the ESP in the image by **systemd-bo
   `ubuntu-archive-keyring`, `kali-archive-keyring` and/or `debian-archive-keyring`
   packages explicitly, in addition to **apt**, depending on what kind of distribution
   images you want to build.
-- The minimum required Python version is 3.9.
+- The minimum required Python version is 3.10.
 
 ## Unprivileged User Namespaces
 
index 26598b724132d368c2058e3b20b6091d04b8c8d7..187016752bce3b70600b626e0590386d699e4a3a 100644 (file)
@@ -17,7 +17,7 @@ from collections.abc import Awaitable, Collection, Iterator, Mapping, Sequence
 from contextlib import AbstractContextManager
 from pathlib import Path
 from types import TracebackType
-from typing import TYPE_CHECKING, Any, Callable, Generic, NoReturn, Optional, Protocol, TypeVar
+from typing import TYPE_CHECKING, Any, Callable, Generic, NoReturn, Protocol, TypeVar
 
 import mkosi.sandbox
 from mkosi.log import ARG_DEBUG, ARG_DEBUG_SANDBOX, ARG_DEBUG_SHELL, die
@@ -156,7 +156,7 @@ def run(
     stdin: _FILE = None,
     stdout: _FILE = None,
     stderr: _FILE = None,
-    input: Optional[str] = None,
+    input: str | None = None,
     env: Mapping[str, str] = {},
     log: bool = True,
     success_exit_status: Sequence[int] = (0,),
@@ -188,7 +188,7 @@ def _preexec(
     cmd: list[str],
     env: dict[str, Any],
     sandbox: list[str],
-    preexec: Optional[Callable[[], None]],
+    preexec: Callable[[], None] | None,
 ) -> None:
     if preexec:
         preexec()
@@ -231,12 +231,12 @@ def spawn(
     stdin: _FILE = None,
     stdout: _FILE = None,
     stderr: _FILE = None,
-    user: Optional[int] = None,
-    group: Optional[int] = None,
+    user: int | None = None,
+    group: int | None = None,
     pass_fds: Collection[int] = (),
     env: Mapping[str, str] = {},
     log: bool = True,
-    preexec: Optional[Callable[[], None]] = None,
+    preexec: Callable[[], None] | None = None,
     success_exit_status: Sequence[int] = (0,),
     setup: Sequence[PathString] = (),
     sandbox: AbstractContextManager[Sequence[PathString]] = nosandbox(),
@@ -344,7 +344,7 @@ def spawn(
 
 
 def finalize_path(
-    root: Optional[Path] = None,
+    root: Path | None = None,
     extra: Sequence[Path] = (),
     prefix_usr: bool = False,
     relaxed: bool = False,
@@ -376,9 +376,9 @@ def finalize_path(
 
 def find_binary(
     *names: PathString,
-    root: Optional[Path] = None,
+    root: Path | None = None,
     extra: Sequence[Path] = (),
-) -> Optional[Path]:
+) -> Path | None:
     root = root or Path("/")
     path = finalize_path(root=root, extra=extra, prefix_usr=True)
 
@@ -458,9 +458,9 @@ class AsyncioThread(threading.Thread, Generic[T]):
 
     def __exit__(
         self,
-        type: Optional[type[BaseException]],
-        value: Optional[BaseException],
-        traceback: Optional[TracebackType],
+        type: type[BaseException] | None,
+        value: BaseException | None,
+        traceback: TracebackType | None,
     ) -> None:
         self.cancel()
         self.join()
@@ -473,7 +473,7 @@ class AsyncioThread(threading.Thread, Generic[T]):
                 pass
 
 
-def workdir(path: Path, sandbox: Optional[SandboxProtocol] = None) -> str:
+def workdir(path: Path, sandbox: SandboxProtocol | None = None) -> str:
     subdir = "/" if sandbox and sandbox == nosandbox else "/work"
     return joinpath(subdir, os.fspath(path))
 
@@ -536,10 +536,10 @@ def sandbox_cmd(
     *,
     network: bool = False,
     devices: bool = False,
-    scripts: Optional[Path] = None,
+    scripts: Path | None = None,
     tools: Path = Path("/"),
     relaxed: bool = False,
-    overlay: Optional[Path] = None,
+    overlay: Path | None = None,
     options: Sequence[PathString] = (),
     extra: Sequence[Path] = (),
 ) -> Iterator[list[PathString]]:
@@ -645,7 +645,7 @@ def sandbox_cmd(
         if scripts:
             cmdline += ["--ro-bind", scripts, "/scripts"]
 
-        tmp: Optional[Path]
+        tmp: Path | None
 
         if not overlay and not relaxed:
             tmp = stack.enter_context(vartmpdir())
index 19f0b4c7900889a677384deb70953307a1b46e7d..01bf394ac0935970b16fb383a53664a32e8d1a2f 100644 (file)
@@ -21,7 +21,7 @@ import tempfile
 from collections.abc import Hashable, Iterable, Iterator, Mapping, Sequence
 from pathlib import Path
 from types import ModuleType
-from typing import IO, Any, Callable, Optional, Protocol, TypeVar, Union
+from typing import IO, Any, Callable, Protocol, TypeVar
 
 from mkosi.log import die
 from mkosi.resources import as_file
@@ -32,8 +32,8 @@ V = TypeVar("V")
 S = TypeVar("S", bound=Hashable)
 
 # Borrowed from https://github.com/python/typeshed/blob/3d14016085aed8bcf0cf67e9e5a70790ce1ad8ea/stdlib/3/subprocess.pyi#L24
-_FILE = Union[None, int, IO[Any]]
-PathString = Union[Path, str]
+_FILE = None | int | IO[Any]
+PathString = Path | str
 
 # Borrowed from
 # https://github.com/python/typeshed/blob/ec52bf1adde1d3183d0595d2ba982589df48dff1/stdlib/_typeshed/__init__.pyi#L19
@@ -72,7 +72,7 @@ def round_up(x: int, blocksize: int = 4096) -> int:
     return (x + blocksize - 1) // blocksize * blocksize
 
 
-def startswith(s: str, prefix: str) -> Optional[str]:
+def startswith(s: str, prefix: str) -> str | None:
     if s.startswith(prefix):
         return s.removeprefix(prefix)
     return None
index 379825e412bf05d9a52786d4309b0ebc203ea7b8..bbad8a58690a00b8e7319964dcceaab050997304 100644 (file)
@@ -10,7 +10,7 @@ authors = [
 version = "26"
 description = "Build Bespoke OS Images"
 readme = "README.md"
-requires-python = ">=3.9"
+requires-python = ">=3.10"
 license = {text = "LGPL-2.1-or-later"}
 
 [project.optional-dependencies]
@@ -53,7 +53,7 @@ multi_line_output = 3
 py_version = "39"
 
 [tool.pyright]
-pythonVersion = "3.9"
+pythonVersion = "3.10"
 include = [
     "mkosi/**/*.py",
     "tests/**/*.py",
@@ -61,7 +61,7 @@ include = [
 ]
 
 [tool.mypy]
-python_version = 3.9
+python_version = "3.10"
 # belonging to --strict
 warn_unused_configs = true
 disallow_any_generics = true
index 3134a7a28e8dd71f9dd4c1e1787c1dcd8cc55b88..96f06552f7c43c773fa2a4db4626a15dd6f9e6c3 100644 (file)
@@ -9,7 +9,7 @@ import uuid
 from collections.abc import Iterator, Mapping, Sequence
 from pathlib import Path
 from types import TracebackType
-from typing import Any, Optional
+from typing import Any
 
 import pytest
 
@@ -44,9 +44,9 @@ class Image:
 
     def __exit__(
         self,
-        type: Optional[type[BaseException]],
-        value: Optional[BaseException],
-        traceback: Optional[TracebackType],
+        type: type[BaseException] | None,
+        value: BaseException | None,
+        traceback: TracebackType | None,
     ) -> None:
         def clean() -> None:
             acquire_privileges()
index d1f367c7142c8d2059d7dfceb3879ff0bcce21f4..6d4b9d0d8976e3a332c3e776be1e3787649a0f0d 100644 (file)
@@ -4,7 +4,6 @@ import os
 import textwrap
 import uuid
 from pathlib import Path
-from typing import Optional
 
 import pytest
 
@@ -49,7 +48,7 @@ from mkosi.distribution import Distribution
 
 
 @pytest.mark.parametrize("path", [None, "/baz/qux"])
-def test_args(path: Optional[Path]) -> None:
+def test_args(path: Path | None) -> None:
     dump = textwrap.dedent(
         f"""\
         {{