Skip to content

Commit

Permalink
Add type hints for state.py (#4294)
Browse files Browse the repository at this point in the history
Co-authored-by: Sorin Sbarnea <[email protected]>
  • Loading branch information
Qalthos and ssbarnea authored Oct 14, 2024
1 parent 04a608f commit 1ae132f
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 75 deletions.
8 changes: 0 additions & 8 deletions .config/pydoclint-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -435,14 +435,6 @@ src/molecule/shell.py
DOC107: Function `main`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC103: Function `main`: Docstring arguments are different from function arguments. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Arguments in the function signature but not in the docstring: [base_config: , ctx: , debug: , env_file: , verbose: ].
--------------------
src/molecule/state.py
DOC106: Method `State.__init__`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Method `State.__init__`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC106: Method `State.change_state`: The option `--arg-type-hints-in-signature` is `True` but there are no argument type hints in the signature
DOC107: Method `State.change_state`: The option `--arg-type-hints-in-signature` is `True` but not all args in the signature have type hints
DOC501: Method `State.change_state` has "raise" statements, but the docstring does not have a "Raises" section
DOC503: Method `State.change_state` exceptions in the "Raises" section in the docstring do not match those in the function body Raises values in the docstring: []. Raised exceptions in the body: ['InvalidState'].
--------------------
src/molecule/status.py
DOC601: Class `Status`: Class docstring contains fewer class attributes than actual class attributes. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.)
DOC603: Class `Status`: Class docstring attributes are different from actual class attributes. (Or could be other formatting issues: https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). Attributes in the class definition but not in the docstring: [converged: str, created: str, driver_name: str, instance_name: str, provisioner_name: str, scenario_name: str]. (Please read https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to correctly document class attributes.)
Expand Down
134 changes: 89 additions & 45 deletions src/molecule/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@
from __future__ import annotations

import logging
import os

from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING, TypedDict, TypeVar, cast

from molecule import util


if TYPE_CHECKING:

from molecule.config import Config


LOG = logging.getLogger(__name__)
VALID_KEYS = [
"created",
Expand All @@ -36,12 +44,52 @@
"is_parallel",
"molecule_yml_date_modified",
]
F = TypeVar("F", bound=Callable[..., None])


class InvalidState(Exception): # noqa: N818
"""Exception class raised when an error occurs in :class:`.State`."""


class StateData(TypedDict):
"""Valid state values and types.
Attributes:
converged: Has scenario converged.
created: Has scenario been created:
driver: Driver for scenario.
prepared: Has scenario prepare run.
molecule_yml_date_modified: Modified date of molecule.yml file.
run_uuid: UUID of active run.
is_parallel: Is this run parallel.
"""

converged: bool
created: bool
driver: str | None
prepared: bool
molecule_yml_date_modified: float | None
run_uuid: str
is_parallel: bool


def marshal(func: F) -> F:
"""Decorator to immediately write state to file after call finishes.
Args:
func: Function to decorate.
Returns:
Decorated function.
"""

def wrapper(self: State, *args: object, **kwargs: object) -> None:
func(self, *args, **kwargs)
self._write_state_file()

return cast(F, wrapper)


class State:
"""A class which manages the state file.
Expand All @@ -60,95 +108,91 @@ class State:
Molecule.
"""

def __init__(self, config) -> None: # type: ignore[no-untyped-def] # noqa: ANN001
def __init__(self, config: Config) -> None:
"""Initialize a new state class and returns None.
Args:
config: An instance of a Molecule config.
"""
self._config = config
self._state_file = self._get_state_file() # type: ignore[no-untyped-call]
self._data = self._get_data() # type: ignore[no-untyped-call]
self._write_state_file() # type: ignore[no-untyped-call]

def marshal(func): # type: ignore[no-untyped-def] # noqa: ANN201, N805, D102
def wrapper(self, *args, **kwargs): # type: ignore[no-untyped-def] # noqa: ANN001, ANN002, ANN003, ANN202
func(self, *args, **kwargs) # type: ignore[operator] # pylint: disable=not-callable
self._write_state_file()

return wrapper
self._state_file = self._get_state_file()
self._data = self._get_data()
self._write_state_file()

@property
def state_file(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102
return self._state_file
def state_file(self) -> str: # noqa: D102
return str(self._state_file)

@property
def converged(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102
return self._data.get("converged")
def converged(self) -> bool: # noqa: D102
return self._data["converged"]

@property
def created(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102
return self._data.get("created")
def created(self) -> bool: # noqa: D102
return self._data["created"]

@property
def driver(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102
return self._data.get("driver")
def driver(self) -> str | None: # noqa: D102
return self._data["driver"]

@property
def prepared(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102
return self._data.get("prepared")
def prepared(self) -> bool: # noqa: D102
return self._data["prepared"]

@property
def run_uuid(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102
return self._data.get("run_uuid")
def run_uuid(self) -> str: # noqa: D102
return self._data["run_uuid"]

@property
def is_parallel(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102
return self._data.get("is_parallel")
def is_parallel(self) -> bool: # noqa: D102
return self._data["is_parallel"]

@property
def molecule_yml_date_modified(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102
return self._data.get("molecule_yml_date_modified")
def molecule_yml_date_modified(self) -> float | None: # noqa: D102
return self._data["molecule_yml_date_modified"]

@marshal # type: ignore[arg-type]
def reset(self): # type: ignore[no-untyped-def] # noqa: ANN201, D102
self._data = self._default_data() # type: ignore[no-untyped-call]
@marshal
def reset(self) -> None: # noqa: D102
self._data = self._default_data()

@marshal # type: ignore[arg-type]
def change_state(self, key, value): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201
@marshal
def change_state(self, key: str, value: object) -> None:
"""Change the state of the instance data with the given `key` and the provided ``value`.
Wrapping with a decorator is probably not necessary.
Args:
key: A ``str`` containing the key to update
value: A value to change the ``key`` to
Raises:
InvalidState: if an invalid key is requested.
"""
if key not in VALID_KEYS:
raise InvalidState
self._data[key] = value
self._data[key] = value # type: ignore[literal-required]

def _get_data(self): # type: ignore[no-untyped-def] # noqa: ANN202
if os.path.isfile(self.state_file): # noqa: PTH113
return self._load_file() # type: ignore[no-untyped-call]
return self._default_data() # type: ignore[no-untyped-call]
def _get_data(self) -> StateData:
if self._state_file.is_file():
return self._load_file()
return self._default_data()

def _default_data(self): # type: ignore[no-untyped-def] # noqa: ANN202
def _default_data(self) -> StateData:
return {
"converged": False,
"created": False,
"driver": None,
"prepared": None,
"prepared": False,
"molecule_yml_date_modified": None,
"run_uuid": self._config._run_uuid, # noqa: SLF001
"is_parallel": self._config.is_parallel,
}

def _load_file(self): # type: ignore[no-untyped-def] # noqa: ANN202
return util.safe_load_file(self.state_file)
def _load_file(self) -> StateData:
return cast(StateData, util.safe_load_file(self._state_file))

def _write_state_file(self): # type: ignore[no-untyped-def] # noqa: ANN202
def _write_state_file(self) -> None:
util.write_file(self.state_file, util.safe_dump(self._data))

def _get_state_file(self): # type: ignore[no-untyped-def] # noqa: ANN202
return os.path.join(self._config.scenario.ephemeral_directory, "state.yml") # noqa: PTH118
def _get_state_file(self) -> Path:
return Path(self._config.scenario.ephemeral_directory) / "state.yml"
11 changes: 6 additions & 5 deletions src/molecule/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,19 +246,20 @@ def render_template(template: str, **kwargs: str) -> str:
return t.from_string(template).render(kwargs)


def write_file(filename: str, content: str, header: str | None = None) -> None:
"""Write a file with the given filename and content and returns None.
def write_file(filename: str | Path, content: str, header: str | None = None) -> None:
"""Write a file with the given filename and content.
Args:
filename: A string containing the target filename.
filename: The target file.
content: A string containing the data to be written.
header: A header, if None it will use default header.
"""
if header is None:
content = MOLECULE_HEADER + "\n\n" + content

with open(filename, "w") as f: # noqa: PTH123
f.write(content)
if isinstance(filename, str):
filename = Path(filename)
filename.write_text(content)


def molecule_prepender(content: str) -> str:
Expand Down
40 changes: 23 additions & 17 deletions tests/unit/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,39 +21,45 @@

import os

from typing import TYPE_CHECKING

import pytest

from molecule import config, state, util


if TYPE_CHECKING:
from typing import Any


@pytest.fixture()
def _instance(config_instance: config.Config): # type: ignore[no-untyped-def] # noqa: ANN202
def _instance(config_instance: config.Config) -> state.State:
return state.State(config_instance)


def test_state_file_property(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_state_file_property(_instance: state.State) -> None: # noqa: PT019, D103
x = os.path.join(_instance._config.scenario.ephemeral_directory, "state.yml") # noqa: PTH118

assert x == _instance.state_file


def test_converged(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_converged(_instance: state.State) -> None: # noqa: PT019, D103
assert not _instance.converged


def test_created(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_created(_instance: state.State) -> None: # noqa: PT019, D103
assert not _instance.created


def test_state_driver(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_state_driver(_instance: state.State) -> None: # noqa: PT019, D103
assert not _instance.driver


def test_prepared(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_prepared(_instance: state.State) -> None: # noqa: PT019, D103
assert not _instance.prepared


def test_reset(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_reset(_instance: state.State) -> None: # noqa: PT019, D103
assert not _instance.converged

_instance.change_state("converged", True) # noqa: FBT003
Expand All @@ -63,7 +69,7 @@ def test_reset(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN2
assert not _instance.converged


def test_reset_persists(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_reset_persists(_instance: state.State) -> None: # noqa: PT019, D103
assert not _instance.converged

_instance.change_state("converged", True) # noqa: FBT003
Expand All @@ -76,40 +82,40 @@ def test_reset_persists(_instance): # type: ignore[no-untyped-def] # noqa: ANN
assert not d.get("converged")


def test_change_state_converged(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_change_state_converged(_instance: state.State) -> None: # noqa: PT019, D103
_instance.change_state("converged", True) # noqa: FBT003

assert _instance.converged


def test_change_state_created(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_change_state_created(_instance: state.State) -> None: # noqa: PT019, D103
_instance.change_state("created", True) # noqa: FBT003

assert _instance.created


def test_change_state_driver(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_change_state_driver(_instance: state.State) -> None: # noqa: PT019, D103
_instance.change_state("driver", "foo")

assert _instance.driver == "foo"


def test_change_state_prepared(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_change_state_prepared(_instance: state.State) -> None: # noqa: PT019, D103
_instance.change_state("prepared", True) # noqa: FBT003

assert _instance.prepared


def test_change_state_raises(_instance): # type: ignore[no-untyped-def] # noqa: ANN001, ANN201, PT019, D103
def test_change_state_raises(_instance: state.State) -> None: # noqa: PT019, D103
with pytest.raises(state.InvalidState):
_instance.change_state("invalid-state", True) # noqa: FBT003


def test_get_data_loads_existing_state_file( # type: ignore[no-untyped-def] # noqa: ANN201, D103
_instance, # noqa: ANN001, PT019
molecule_data, # noqa: ANN001, ARG001
def test_get_data_loads_existing_state_file( # noqa: D103
_instance: state.State, # noqa: PT019
molecule_data: dict[str, Any], # noqa: ARG001
config_instance: config.Config,
):
) -> None:
data = {"converged": False, "created": True, "driver": None, "prepared": None}
util.write_file(_instance._state_file, util.safe_dump(data))

Expand Down

0 comments on commit 1ae132f

Please sign in to comment.