Full type coverage with mypy

This commit is contained in:
Cian Hughes
2023-11-01 16:41:15 +00:00
parent baf5962f34
commit df07bc2bc4
7 changed files with 70 additions and 29 deletions

17
poetry.lock generated
View File

@@ -328,6 +328,21 @@ websocket-client = ">=0.32.0"
[package.extras] [package.extras]
ssh = ["paramiko (>=2.4.3)"] ssh = ["paramiko (>=2.4.3)"]
[[package]]
name = "docker-stubs"
version = "0.1.0"
description = "Stubs package for the Python Docker API."
optional = false
python-versions = "^3.8"
files = []
develop = false
[package.source]
type = "git"
url = "https://github.com/rdozier-work/docker-stubs"
reference = "HEAD"
resolved_reference = "9de7906804ae912f1d644c97b617ac77e784fca8"
[[package]] [[package]]
name = "executing" name = "executing"
version = "2.0.1" version = "2.0.1"
@@ -1092,4 +1107,4 @@ h11 = ">=0.9.0,<1"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "c74d1dcf762e5266d41c81b62bef444f065124fede9b3a34176cd9a6b93d9c2b" content-hash = "0da96ff6654ff4c33b776829e34f78ba2725a0de30980ec5eaab86a8b7abbead"

View File

@@ -25,6 +25,7 @@ black = "^23.10.1"
snoop = "^0.4.3" snoop = "^0.4.3"
pytest = "^7.4.3" pytest = "^7.4.3"
mypy = "^1.6.1" mypy = "^1.6.1"
docker-stubs = {git = "https://github.com/rdozier-work/docker-stubs"}
[tool.poetry.scripts] [tool.poetry.scripts]
node_deployer = "node_deployer.__main__:main" node_deployer = "node_deployer.__main__:main"

View File

@@ -81,8 +81,11 @@ def convert_json_via_fuelignition(
bytestream = io.BytesIO(b"".join(chunk for chunk in filestream)) bytestream = io.BytesIO(b"".join(chunk for chunk in filestream))
bytestream.seek(0) bytestream.seek(0)
tar = tarfile.open(fileobj=bytestream) tar = tarfile.open(fileobj=bytestream)
container_image = tar.extractfile(tar.getmembers()[0].name)
if container_image is None:
raise Exception("Failed to extract image from tarfile")
with open(host_image_path, "wb+") as f: with open(host_image_path, "wb+") as f:
f.write(tar.extractfile(tar.getmembers()[0].name).read()) f.write(container_image.read())
def build_fuelignition() -> docker.models.images.Image: def build_fuelignition() -> docker.models.images.Image:

View File

@@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from typing import Union
import docker import docker
import tomllib import tomllib
@@ -9,7 +10,7 @@ CLIENT = docker.from_env(version="auto")
MAX_PORT: int = 65535 MAX_PORT: int = 65535
PROJECT_ROOT: Path = Path(__file__).parent.parent.parent.absolute() PROJECT_ROOT: Path = Path(__file__).parent.parent.parent.absolute()
type ConfigLabel = str | list[str] ConfigLabel = Union[str, list[str]] # After PEP695 support: type ConfigLabel = str | list[str]
class Config(SimpleNamespace): class Config(SimpleNamespace):

View File

@@ -1,19 +1,20 @@
from fnmatch import fnmatch from fnmatch import fnmatch
import ipaddress import ipaddress
from typing import Annotated from typing import Annotated, Optional, Union
from docker.types import Mount from docker.types import Mount
import typer import typer
from typing import Tuple
from .config import config
from .cli import cli_spinner from .cli import cli_spinner
from .config import config
from .create_img import create_img from .create_img import create_img
from .debug import debug_guard from .debug import debug_guard
from .utils import ensure_build_dir from .utils import ensure_build_dir
type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address # When PEP695 is supported this line should be:
# type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address
IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
def filter_validation_response(response: str) -> str: def filter_validation_response(response: str) -> str:
@@ -60,18 +61,18 @@ def validation_result() -> str:
) )
if config.CLEANUP_IMAGES: if config.CLEANUP_IMAGES:
image.remove(force=True) image.remove(force=True)
return response return response.decode()
@cli_spinner(description="Validating ignition image", total=None) @cli_spinner(description="Validating ignition image", total=None)
def validate() -> Tuple[bool, str]: def validate() -> tuple[bool, str]:
"""Validates the ignition image """Validates the ignition image
Returns: Returns:
Tuple[bool, str]: A tuple containing a boolean indicating whether tuple[bool, str]: A tuple containing a boolean indicating whether
the validation was successful and the response from the validation the validation was successful and the response from the validation
""" """
response = validation_result().decode() response = validation_result()
response = filter_validation_response(response) response = filter_validation_response(response)
return (not bool(response), response) return (not bool(response), response)
@@ -96,7 +97,7 @@ def write_disk(disk: str) -> None:
@ensure_build_dir @ensure_build_dir
def create_ignition_disk( def create_ignition_disk(
disk: Annotated[ disk: Annotated[
str, Optional[str],
typer.Option( typer.Option(
"--disk", "--disk",
"-d", "-d",
@@ -114,7 +115,7 @@ def create_ignition_disk(
), ),
] = "node", ] = "node",
password: Annotated[ password: Annotated[
str, Optional[str],
typer.Option( typer.Option(
"--password", "--password",
"-p", "-p",
@@ -125,7 +126,7 @@ def create_ignition_disk(
), ),
] = None, ] = None,
switch_ip: Annotated[ switch_ip: Annotated[
IPAddress, Optional[IPAddress],
typer.Option( typer.Option(
"--switch-ip", "--switch-ip",
"-ip", "-ip",
@@ -146,7 +147,7 @@ def create_ignition_disk(
), ),
] = 4789, ] = 4789,
swarm_token: Annotated[ swarm_token: Annotated[
str, Optional[str],
typer.Option( typer.Option(
"--swarm-token", "--swarm-token",
"-t", "-t",
@@ -194,6 +195,10 @@ def create_ignition_disk(
Raises: Raises:
typer.Exit: Exit CLI if the ignition image is invalid typer.Exit: Exit CLI if the ignition image is invalid
""" """
# Guard against the user specifying no disk
if disk is None:
raise typer.BadParameter("No disk specified")
create_img( create_img(
hostname = hostname, hostname = hostname,
password = password, password = password,

View File

@@ -1,18 +1,19 @@
import ipaddress import ipaddress
import json import json
from pathlib import Path from pathlib import Path
from typing import Annotated from typing import Annotated, Optional, Union
import typer import typer
from .config import config
from .autoignition import json_to_img from .autoignition import json_to_img
from .cli import cli_spinner from .cli import cli_spinner
from .config import config
from .debug import debug_guard from .debug import debug_guard
from .utils import ensure_build_dir from .utils import ensure_build_dir
# When PEP695 is supported this line should be:
type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address # type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address
IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
def load_template() -> dict: def load_template() -> dict:
@@ -99,7 +100,7 @@ def create_img(
), ),
] = "node", ] = "node",
password: Annotated[ password: Annotated[
str, Optional[str],
typer.Option( typer.Option(
"--password", "--password",
"-p", "-p",
@@ -110,7 +111,7 @@ def create_img(
), ),
] = None, ] = None,
switch_ip: Annotated[ switch_ip: Annotated[
IPAddress, Optional[IPAddress],
typer.Option( typer.Option(
"--switch-ip", "--switch-ip",
"-ip", "-ip",
@@ -131,7 +132,7 @@ def create_img(
), ),
] = 4789, ] = 4789,
swarm_token: Annotated[ swarm_token: Annotated[
str, Optional[str],
typer.Option( typer.Option(
"--swarm-token", "--swarm-token",
"-t", "-t",
@@ -194,10 +195,13 @@ def create_img(
} }
) )
# Guards against the user not specifying a password
if password is None:
raise typer.BadParameter("Password must be specified")
# Create ignition configuration # Create ignition configuration
ignition_config = load_template()
ignition_config = apply_ignition_settings( ignition_config = apply_ignition_settings(
ignition_config, load_template(),
hostname, hostname,
password, password,
swarm_config, swarm_config,

View File

@@ -1,12 +1,14 @@
from typing import Any, Dict
import typer import typer
from .config import config
from .autoignition import json_to_img from .autoignition import json_to_img
from .config import config
from .create_disk import create_ignition_disk from .create_disk import create_ignition_disk
from .create_img import create_img from .create_img import create_img
cmd_params = { cmd_params: Dict[Any, Any] = {
"no_args_is_help": True, "no_args_is_help": True,
} }
@@ -15,9 +17,19 @@ app = typer.Typer(
**cmd_params, **cmd_params,
) )
app.command(**cmd_params)(create_img) # Register commands
app.command(**cmd_params)(create_ignition_disk) app.command(
app.command(**cmd_params)(json_to_img) help = str(create_ignition_disk.__doc__).split("Args:")[0].strip(),
**cmd_params
)(create_ignition_disk)
app.command(
help = str(create_img.__doc__).split("Args:")[0].strip(),
**cmd_params
)(create_img)
app.command(
help = str(json_to_img.__doc__).split("Args:")[0].strip(),
**cmd_params
)(json_to_img)
if __name__ == "__main__": if __name__ == "__main__":
config.update_config("cli") config.update_config("cli")