Coverage for src/rhiza_task/tasks/docker.py: 100%

26 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 04:13 +0000

1"""The container tasks: docker.mk, as tasks. 

2 

3Three wrappers over the docker CLI, and the shortest of the five fragments. The only 

4thing worth stating is what the image is called: docker.mk defaults it to 

5``$(shell basename $(CURDIR))``, so an unset :attr:`~rhiza_task.config.Config.docker_image` 

6resolves to the repository directory's name here too -- moving a checkout would rename the 

7image, which is surprising but is the behaviour consumers already have. 

8 

9``docker-build`` skips rather than fails on a missing Dockerfile, as the fragment does. 

10That is not the same judgement as the tool guard's: a repository with no ``docker/`` 

11folder has adopted the bundle and not used it yet, whereas a machine with no docker 

12cannot answer the question at all. Both are a skip, and ``--strict`` fails both. 

13""" 

14 

15from __future__ import annotations 

16 

17from ..config import Config 

18from ..spec import Guard, Skip, task 

19from ..uv import tool 

20 

21SECTION = "Docker" 

22 

23HAVE_DOCKER = Guard(tool="docker", reason="docker not found; install from https://docs.docker.com/get-docker/") 

24 

25 

26def image_name(cfg: Config) -> str: 

27 """Return the tag to build and run. 

28 

29 Args: 

30 cfg: The resolved config. 

31 

32 Returns: 

33 The configured image name, or the repository directory's name. 

34 """ 

35 return cfg.docker_image or cfg.root.name 

36 

37 

38@task("docker-build", "build the Docker image", section=SECTION, guards=(HAVE_DOCKER,)) 

39def docker_build(cfg: Config) -> None: 

40 """Build ``<docker_folder>/Dockerfile`` with the repository root as the context. 

41 

42 ``PYTHON_VERSION`` is passed as a build argument whatever the layer, as docker.mk 

43 does. A Dockerfile that declares no such ``ARG`` gets a warning from docker and 

44 nothing else, which is cheaper than making the flag conditional on a language. 

45 

46 Args: 

47 cfg: The resolved config. 

48 

49 Raises: 

50 Skip: When the folder holds no Dockerfile. 

51 """ 

52 dockerfile = cfg.root / cfg.docker_folder / "Dockerfile" 

53 if not dockerfile.is_file(): 

54 raise Skip(f"no {cfg.docker_folder}/Dockerfile") 

55 

56 tag = f"{image_name(cfg)}:latest" 

57 print(f"[INFO] building {tag} with Python {cfg.python_version}") 

58 tool( 

59 "docker", 

60 "buildx", 

61 "build", 

62 "--file", 

63 f"{cfg.docker_folder}/Dockerfile", 

64 "--build-arg", 

65 f"PYTHON_VERSION={cfg.python_version}", 

66 "--tag", 

67 tag, 

68 "--load", 

69 ".", 

70 cwd=cfg.root, 

71 ) 

72 

73 

74@task("docker-run", "run the Docker container", section=SECTION, needs=("docker-build",), guards=(HAVE_DOCKER,)) 

75def docker_run(cfg: Config) -> None: 

76 """Run the built image interactively, removing the container on exit. 

77 

78 Args: 

79 cfg: The resolved config. 

80 """ 

81 tag = f"{image_name(cfg)}:latest" 

82 print(f"[INFO] running {tag}") 

83 tool("docker", "run", "--rm", "-it", tag, cwd=cfg.root) 

84 

85 

86@task("docker-clean", "remove the Docker image", section=SECTION, guards=(HAVE_DOCKER,)) 

87def docker_clean(cfg: Config) -> None: 

88 """Delete the image, tolerating its absence. 

89 

90 ``check=False`` is docker.mk's ``2>/dev/null || true``: removing an image that was 

91 never built is the expected state of a clean target, not a failure. 

92 

93 Args: 

94 cfg: The resolved config. 

95 """ 

96 tag = f"{image_name(cfg)}:latest" 

97 print(f"[INFO] removing {tag}") 

98 tool("docker", "rmi", tag, cwd=cfg.root, check=False)