docker-composer 2.79.7__tar.gz → 5.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {docker_composer-2.79.7 → docker_composer-5.0.2}/PKG-INFO +22 -12
  2. {docker_composer-2.79.7 → docker_composer-5.0.2}/README.md +21 -8
  3. {docker_composer-2.79.7 → docker_composer-5.0.2}/pyproject.toml +18 -8
  4. docker_composer-5.0.2/src/docker_composer/__init__.py +6 -0
  5. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/_utils/argument.py +33 -48
  6. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/_utils/generate_class.py +65 -63
  7. docker_composer-5.0.2/src/docker_composer/_utils/sync_version.py +21 -0
  8. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/base.py +19 -16
  9. docker_composer-5.0.2/src/docker_composer/runner/cmd/__init__.py +0 -0
  10. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/attach.py +15 -12
  11. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/build.py +32 -18
  12. docker_composer-5.0.2/src/docker_composer/runner/cmd/commit.py +37 -0
  13. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/config.py +38 -23
  14. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/cp.py +18 -12
  15. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/create.py +23 -17
  16. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/down.py +19 -14
  17. docker_composer-5.0.2/src/docker_composer/runner/cmd/events.py +33 -0
  18. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/exec.py +18 -14
  19. docker_composer-5.0.2/src/docker_composer/runner/cmd/export.py +30 -0
  20. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/images.py +14 -11
  21. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/kill.py +14 -11
  22. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/logs.py +17 -14
  23. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/ls.py +16 -13
  24. docker_composer-5.0.2/src/docker_composer/runner/cmd/pause.py +26 -0
  25. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/port.py +13 -10
  26. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/ps.py +22 -17
  27. docker_composer-5.0.2/src/docker_composer/runner/cmd/publish.py +40 -0
  28. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/pull.py +17 -14
  29. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/push.py +16 -13
  30. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/restart.py +14 -11
  31. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/rm.py +16 -13
  32. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/run.py +34 -21
  33. docker_composer-5.0.2/src/docker_composer/runner/cmd/scale.py +29 -0
  34. docker_composer-5.0.2/src/docker_composer/runner/cmd/start.py +31 -0
  35. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/stats.py +18 -14
  36. docker_composer-5.0.2/src/docker_composer/runner/cmd/stop.py +28 -0
  37. docker_composer-5.0.2/src/docker_composer/runner/cmd/top.py +26 -0
  38. docker_composer-5.0.2/src/docker_composer/runner/cmd/unpause.py +26 -0
  39. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/up.py +55 -40
  40. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/cmd/version.py +14 -11
  41. docker_composer-5.0.2/src/docker_composer/runner/cmd/volumes.py +37 -0
  42. docker_composer-5.0.2/src/docker_composer/runner/cmd/wait.py +29 -0
  43. docker_composer-5.0.2/src/docker_composer/runner/cmd/watch.py +35 -0
  44. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/root.py +205 -42
  45. docker_composer-2.79.7/src/docker_composer/__init__.py +0 -3
  46. docker_composer-2.79.7/src/docker_composer/runner/cmd/events.py +0 -26
  47. docker_composer-2.79.7/src/docker_composer/runner/cmd/pause.py +0 -23
  48. docker_composer-2.79.7/src/docker_composer/runner/cmd/scale.py +0 -26
  49. docker_composer-2.79.7/src/docker_composer/runner/cmd/start.py +0 -23
  50. docker_composer-2.79.7/src/docker_composer/runner/cmd/stop.py +0 -25
  51. docker_composer-2.79.7/src/docker_composer/runner/cmd/top.py +0 -23
  52. docker_composer-2.79.7/src/docker_composer/runner/cmd/unpause.py +0 -23
  53. docker_composer-2.79.7/src/docker_composer/runner/cmd/wait.py +0 -26
  54. docker_composer-2.79.7/src/docker_composer/runner/cmd/watch.py +0 -32
  55. {docker_composer-2.79.7 → docker_composer-5.0.2}/.gitignore +0 -0
  56. {docker_composer-2.79.7 → docker_composer-5.0.2}/LICENSE.txt +0 -0
  57. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/_utils/__init__.py +0 -0
  58. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/py.typed +0 -0
  59. {docker_composer-2.79.7 → docker_composer-5.0.2}/src/docker_composer/runner/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docker-composer
3
- Version: 2.79.7
3
+ Version: 5.0.2
4
4
  Summary: Use docker-compose (V2) from within Python
5
5
  Project-URL: Homepage, https://github.com/schollm/docker-composer
6
6
  Project-URL: Repository, https://github.com/schollm/docker-composer
@@ -16,9 +16,6 @@ Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Topic :: Software Development :: Build Tools
17
17
  Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
18
18
  Requires-Python: <4.0,>=3.9
19
- Requires-Dist: attrs>=20.3.0
20
- Requires-Dist: loguru>=0.5.3
21
- Requires-Dist: ruff>=0.11.10
22
19
  Description-Content-Type: text/markdown
23
20
 
24
21
  # Docker Composer
@@ -27,6 +24,8 @@ All commands and parameters are exposed as python classes and attributes
27
24
  to allow for full auto-completion of its parameters with IDEs
28
25
  that support it.
29
26
 
27
+ **Runtime footprint:** This library is **stdlib-only** (zero third-party runtime dependencies).
28
+
30
29
 
31
30
  ## Install
32
31
  ```shell script
@@ -67,13 +66,24 @@ print(process.stdout.encode("UTF-8"))
67
66
 
68
67
  ## Develop
69
68
 
69
+ ### Development Setup
70
+
71
+ When developing, the `_utils` module is available for generating the docker_composer
72
+ module from the `docker compose`CLI help output.
73
+ This is not required for normal package usage.
74
+
75
+ ```bash
76
+ uv sync --group dev
77
+ uv run poe generate
78
+ ```
79
+
70
80
  ### Coding Standards
71
81
 
72
- | **Type** | Package | Comment |
73
- | -------------- |----------| ------------------------------- |
74
- | **Linter** | `black` | Also for auto-formatted modules |
75
- | **Logging** | `loguru` | |
76
- | **Packaging** | `uv` | |
77
- | **Tests** | `pytest` | |
78
- | **Typing** | `mypy` | Type all methods |
79
- | **Imports** | `isort` | |
82
+ | **Type** | Package | Comment |
83
+ | -------------- |-----------| ------------------------------- |
84
+ | **Linter** | `ruff` | Also for auto-formatted modules |
85
+ | **Logging** | `logging` | |
86
+ | **Packaging** | `uv` | |
87
+ | **Tests** | `pytest` | |
88
+ | **Typing** | `mypy` | Type all methods |
89
+ | **Imports** | `ruff` | |
@@ -4,6 +4,8 @@ All commands and parameters are exposed as python classes and attributes
4
4
  to allow for full auto-completion of its parameters with IDEs
5
5
  that support it.
6
6
 
7
+ **Runtime footprint:** This library is **stdlib-only** (zero third-party runtime dependencies).
8
+
7
9
 
8
10
  ## Install
9
11
  ```shell script
@@ -44,13 +46,24 @@ print(process.stdout.encode("UTF-8"))
44
46
 
45
47
  ## Develop
46
48
 
49
+ ### Development Setup
50
+
51
+ When developing, the `_utils` module is available for generating the docker_composer
52
+ module from the `docker compose`CLI help output.
53
+ This is not required for normal package usage.
54
+
55
+ ```bash
56
+ uv sync --group dev
57
+ uv run poe generate
58
+ ```
59
+
47
60
  ### Coding Standards
48
61
 
49
- | **Type** | Package | Comment |
50
- | -------------- |----------| ------------------------------- |
51
- | **Linter** | `black` | Also for auto-formatted modules |
52
- | **Logging** | `loguru` | |
53
- | **Packaging** | `uv` | |
54
- | **Tests** | `pytest` | |
55
- | **Typing** | `mypy` | Type all methods |
56
- | **Imports** | `isort` | |
62
+ | **Type** | Package | Comment |
63
+ | -------------- |-----------| ------------------------------- |
64
+ | **Linter** | `ruff` | Also for auto-formatted modules |
65
+ | **Logging** | `logging` | |
66
+ | **Packaging** | `uv` | |
67
+ | **Tests** | `pytest` | |
68
+ | **Typing** | `mypy` | Type all methods |
69
+ | **Imports** | `ruff` | |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "docker-composer"
3
- version = "2.79.7"
3
+ version = "5.0.2"
4
4
  description = "Use docker-compose (V2) from within Python"
5
5
  authors = [{ name = "Micha", email = "schollm-git@gmx.com" }]
6
6
  requires-python = ">=3.9,<4.0"
@@ -16,11 +16,7 @@ classifiers = [
16
16
  "Operating System :: OS Independent",
17
17
  "Development Status :: 5 - Production/Stable",
18
18
  ]
19
- dependencies = [
20
- "attrs>=20.3.0",
21
- "loguru>=0.5.3",
22
- "ruff>=0.11.10",
23
- ]
19
+ dependencies = []
24
20
 
25
21
  [project.urls]
26
22
  Homepage = "https://github.com/schollm/docker-composer"
@@ -29,10 +25,11 @@ Repository = "https://github.com/schollm/docker-composer"
29
25
  [dependency-groups]
30
26
  dev = [
31
27
  "black>=25.1.0",
32
- "flake8>=6.0.0,<7",
33
28
  "isort>=6.0.1",
29
+ "poethepoet>=0.30.0",
34
30
  "pytest>=6.1.2",
35
31
  "pytest-cov>=6.1.1",
32
+ "ruff>=0.11.10",
36
33
  ]
37
34
 
38
35
  [tool.hatch.build.targets.sdist]
@@ -40,6 +37,7 @@ include = ["src/docker_composer"]
40
37
 
41
38
  [tool.hatch.build.targets.wheel]
42
39
  include = ["src/docker_composer"]
40
+ exclude = ["src/docker_composer/_utils"]
43
41
 
44
42
  [tool.hatch.build.targets.wheel.sources]
45
43
  "src/docker_composer" = "docker_composer"
@@ -54,8 +52,20 @@ addopts = """
54
52
  --cov=src/docker_composer
55
53
  --cov-report=xml:.out/coverage.xml
56
54
  --cov-report=html:.out/coverage-html
57
- --cov-report term-missing
55
+ --cov-report term-missing:skip-covered
58
56
  --cov-branch
59
57
  --doctest-modules
60
58
  """
61
59
 
60
+ [tool.poe.tasks]
61
+ ruff-check = { cmd = "uv run ruff check" }
62
+ ruff-format-check = { cmd = "uv run ruff format --check" }
63
+ pytest = { cmd = "uv run pytest" }
64
+ format = { cmd = "uv run ruff format" }
65
+ build = { cmd = "uv build" }
66
+ publish-dist = { cmd = "uv publish" }
67
+ generate-code = { cmd = "uv run python -m docker_composer._utils.generate_class" }
68
+ sync-version = { cmd = "uv run python -m docker_composer._utils.sync_version" }
69
+ publish = { sequence = ["build", "publish-dist"] }
70
+ test = { sequence = ["ruff-check", "ruff-format-check", "pytest $POE_EXTRA_ARGS"] }
71
+ generate = { sequence = ["generate-code", "sync-version"] }
@@ -0,0 +1,6 @@
1
+ try:
2
+ from docker_composer.runner.root import DockerComposeRoot as DockerCompose
3
+ except ImportError:
4
+ pass
5
+
6
+ __all__ = ["DockerCompose"]
@@ -1,8 +1,12 @@
1
- from typing import Iterable, Iterator, List, Tuple, Type
1
+ """Helper modules to parse docker compoose --help arguments"""
2
2
 
3
- import attr
4
- from loguru import logger
3
+ from __future__ import annotations
4
+ from typing import Iterable, Iterator
5
5
 
6
+ import logging
7
+ from dataclasses import dataclass
8
+
9
+ logger = logging.getLogger(__name__)
6
10
  OPTION = "OPTION"
7
11
 
8
12
  _TYPE_CONVERSIONS = {
@@ -16,7 +20,6 @@ _TYPE_CONVERSIONS = {
16
20
  "LEVEL": int,
17
21
  "MEM": int,
18
22
  "NAME": str,
19
- OPTION: bool,
20
23
  "PATH": str,
21
24
  "SERVICE": str,
22
25
  "SIGNAL": int,
@@ -24,28 +27,34 @@ _TYPE_CONVERSIONS = {
24
27
  "TIMEOUT": int,
25
28
  "TLS_KEY_PATH": str,
26
29
  "USER": str,
27
- "type": str,
28
- "string": str,
30
+ "bytes": int,
31
+ "docker": bool,
32
+ "filter": str,
29
33
  "int": int,
30
34
  "list": list,
35
+ "scale": int,
31
36
  "str": str,
37
+ "string": str,
32
38
  "stringArray": list[str],
39
+ "type": str,
33
40
  "volumes": list[str],
34
- "docker": bool,
41
+ OPTION: bool,
35
42
  }
36
43
 
37
44
 
38
- @attr.s(auto_attribs=True, frozen=True, eq=False)
45
+ @dataclass(frozen=True)
39
46
  class Argument:
40
47
  arg: str
41
48
  type_desc: str
42
- type: Type
49
+ type_: type
43
50
  description: str
44
51
  default: str = ""
45
52
 
46
53
  @property
47
54
  def type_str(self) -> str:
48
- return "Any" if self.type is object else self.type.__name__
55
+ if self.type_ is object:
56
+ raise ValueError(self.type_)
57
+ return self.type_.__name__
49
58
 
50
59
  @property
51
60
  def is_option(self) -> bool:
@@ -55,28 +64,7 @@ class Argument:
55
64
  def from_line(line: str) -> "Argument":
56
65
  if " " in line:
57
66
  return _from_line_has_sep(line)
58
-
59
- words = iter(line.split())
60
- has_more = True
61
- while has_more:
62
- arg, default_str, has_more = _parse_arg(next(words))
63
- type_desc = next(words)
64
- type_from_default = _get_type_name_from_default(default_str)
65
- desc = " ".join(words)
66
- if type_desc[1:].islower() and "=" not in type_desc:
67
- desc = f"{type_desc} {desc}"
68
- type_desc = type_from_default
69
-
70
- type_ = _get_type(type_from_default if default_str else type_desc)
71
- return Argument(arg, type_desc, type_, desc, default_str)
72
-
73
- def __eq__(self, other) -> bool:
74
- if not isinstance(other, type(self)):
75
- return False
76
- return self.arg == other.arg
77
-
78
- def __hash__(self):
79
- return hash(self.arg)
67
+ raise ValueError(line)
80
68
 
81
69
 
82
70
  def _collect_arguments(arguments: Iterable[str]) -> Iterator[str]:
@@ -91,7 +79,7 @@ def _collect_arguments(arguments: Iterable[str]) -> Iterator[str]:
91
79
  yield res.strip()
92
80
 
93
81
 
94
- def parse_dc_argument(lines: List[str]) -> List[Argument]:
82
+ def parse_dc_argument(lines: list[str]) -> list[Argument]:
95
83
  """
96
84
  Parse arguments from lines of docker-compose specifications
97
85
  :param lines: Lines of the Options sections from `docker-compose --help`.
@@ -101,18 +89,18 @@ def parse_dc_argument(lines: List[str]) -> List[Argument]:
101
89
  return [Argument.from_line(line) for line in iter_lines if "--version" not in line]
102
90
 
103
91
 
104
- def _get_type(type_name) -> Type:
92
+ def _get_type(type_name) -> type:
105
93
  res = _TYPE_CONVERSIONS.get(type_name, None)
106
94
  if res is None:
107
95
  if "=" in type_name:
108
96
  res = dict
109
97
  else:
110
- logger.warning("Unknown type {}, use str", type_name)
98
+ logger.warning("Unknown type %s, use str", type_name)
111
99
  res = str
112
100
  return res
113
101
 
114
102
 
115
- def _parse_arg(arg: str) -> Tuple[str, str, bool]:
103
+ def _parse_arg(arg: str) -> tuple[str, str, bool]:
116
104
  while arg.startswith("-"):
117
105
  arg = arg[1:]
118
106
  # if argument ends with a comma, it is followed by an alias. (usually -f, --foo)
@@ -138,11 +126,7 @@ def _get_type_name_from_default(default: str) -> str:
138
126
  elif default == "proto":
139
127
  return "str"
140
128
  else:
141
- try:
142
- return eval(default).__class__.__name__
143
- except NameError:
144
- logger.warning("Could not get type for value '{}'", default)
145
- return "str"
129
+ raise NotImplementedError(default)
146
130
 
147
131
 
148
132
  def _from_line_has_sep(line) -> "Argument":
@@ -153,17 +137,18 @@ def _from_line_has_sep(line) -> "Argument":
153
137
  :param line: a single line with the description separated from the definition by at least two blanks
154
138
  :return: Argument
155
139
  """
156
- desc_idx = line[4:].index(" ")
157
- desc = line[desc_idx + 2 + 4 :].strip()
158
- args = iter(line[: desc_idx + 4].split())
140
+ min_arg_chars = 2 # Simple argument with single dash (e.g. -x)
141
+ min_full_chars = 4 # Named argument with double-dash (e.g. --xy)
142
+ desc_idx = line[min_arg_chars:].index(" ")
143
+ # Help output can wrap differently depending on terminal width.
144
+ # Normalize all whitespace so wrapped descriptions are stable.
145
+ desc = line[desc_idx + min_full_chars :].strip()
146
+ args = iter(line[: desc_idx + min_full_chars].split())
159
147
  arg, default, has_more = "", "", True
160
148
  while has_more:
161
149
  arg, default, has_more = _parse_arg(next(args))
162
150
  type_from_default = _get_type_name_from_default(default)
163
- try:
164
- type_desc = next(args)
165
- except StopIteration:
166
- type_desc = type_from_default
151
+ type_desc = next(args, type_from_default)
167
152
 
168
153
  type_ = _get_type(type_from_default if default else type_desc)
169
154
  return Argument(arg, type_desc, type_, desc, default=default)
@@ -3,54 +3,64 @@ from collections import defaultdict
3
3
  from functools import lru_cache, reduce
4
4
  from operator import add
5
5
  from pathlib import Path
6
- from typing import Iterable, Iterator, List, Mapping, Set, Tuple, Union
6
+ from typing import Iterator, Mapping, Union
7
7
 
8
8
  import black
9
9
  import isort
10
10
  from isort.exceptions import ISortError
11
- from loguru import logger
11
+
12
12
 
13
13
  from docker_composer._utils.argument import Argument, parse_dc_argument
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # must be larger than 50 (otherwise it's ignored by docker compose)
19
+ _DEFAULT_HELP_COLUMNS = 120
14
20
 
15
21
 
16
22
  @lru_cache()
17
23
  def project_root():
18
24
  for path in Path(__file__).parents:
19
25
  if "pyproject.toml" in (p.name for p in path.iterdir()):
20
- logger.debug("Project Path root: {}", path)
26
+ logger.debug("Project Path root: %s", path)
21
27
  return path
22
28
  raise EnvironmentError("No pyproject.toml found in path hierarchy")
23
29
 
24
30
 
25
- @lru_cache(None)
26
- def _version(prog: str) -> str:
27
- return (
28
- subprocess.run((prog, "--version"), capture_output=True)
29
- .stdout.strip()
30
- .decode("UTF-8")
31
- )
31
+ @lru_cache()
32
+ def _version() -> str:
33
+ return subprocess.run(
34
+ ("docker", "compose", "version"), capture_output=True, text=True
35
+ ).stdout.strip()
32
36
 
33
37
 
34
38
  @lru_cache()
35
39
  def get_help_message(subcommand: str = "") -> str:
36
40
  """Obtain the help message for subcommand from docker-compose."""
37
- args = [arg for arg in ["docker-compose", subcommand, "--help"] if arg]
38
- process = subprocess.run(args, capture_output=True, text=True)
39
- if process.returncode:
40
- logger.error(
41
- "docker-compose {} --help exited with {}:", subcommand, process.returncode
41
+ cmd = " ".join(arg for arg in ["docker", "compose", subcommand, "--help"] if arg)
42
+ full_cmd = f"stty cols {_DEFAULT_HELP_COLUMNS}; {cmd}"
43
+ try:
44
+ process = subprocess.run(
45
+ ["script", "-q", "-c", full_cmd, "/dev/null"],
46
+ capture_output=True,
47
+ text=True,
42
48
  )
43
- logger.error(process.stderr)
44
-
49
+ except Exception:
50
+ logger.error("FAILED to run %s.", full_cmd)
51
+ raise
52
+ if process.returncode:
53
+ logger.error("%s exited with %s:", cmd, process.returncode)
54
+ raise RuntimeError(process.stderr)
45
55
  return process.stdout
46
56
 
47
57
 
48
- def collect_help_lines(msg: str) -> Mapping[str, List[str]]:
58
+ def collect_help_lines(msg: str) -> Mapping[str, list[str]]:
49
59
  """Collect help messages into sections.
50
60
  :param msg: Output from docker-compose <cmd> --help
51
61
  :returns Mapping from section header to lines as lists. First (unnamed) section gets name "general"
52
62
  """
53
- parts: Mapping[str, List[str]] = defaultdict(list)
63
+ parts: Mapping[str, list[str]] = defaultdict(list)
54
64
  part = "general"
55
65
  for line in msg.split("\n"):
56
66
  if not line:
@@ -62,19 +72,14 @@ def collect_help_lines(msg: str) -> Mapping[str, List[str]]:
62
72
  return parts
63
73
 
64
74
 
65
- def parse_help(msg: str) -> Tuple[Mapping[str, List[str]], List[Argument]]:
75
+ def parse_help(msg: str) -> tuple[Mapping[str, list[str]], list[Argument]]:
66
76
  """Helper function, get sections and arguments from docker-compose <cmd> --help text"""
67
77
  sections = collect_help_lines(msg)
68
78
  arguments = parse_dc_argument(sections["options"])
69
79
  return sections, arguments
70
80
 
71
81
 
72
- def _flatten(lists: Iterable[list]):
73
- """Flatten an iterable of lists"""
74
- return reduce(add, lists, [])
75
-
76
-
77
- def indent(lines: Union[str, List[str]], level: int = 4) -> str:
82
+ def indent(lines: Union[str, list[str]], level: int = 4) -> str:
78
83
  """Indent lines by `level`.
79
84
  :param lines: List of strings or a single string. A single string is splt up into individual lines.
80
85
  :param level: number of spaces to indent
@@ -82,7 +87,7 @@ def indent(lines: Union[str, List[str]], level: int = 4) -> str:
82
87
  """
83
88
  if isinstance(lines, str):
84
89
  lines = lines.split("\n")
85
- lines = _flatten([line.split("\n") for line in lines])
90
+ lines = reduce(add, (line.split("\n") for line in lines), [])
86
91
  prefix = " " * level
87
92
  if lines:
88
93
  return prefix + f"\n{prefix}".join(lines)
@@ -90,26 +95,26 @@ def indent(lines: Union[str, List[str]], level: int = 4) -> str:
90
95
  return ""
91
96
 
92
97
 
93
- def get_docstring(sections: Mapping[str, List[str]]) -> List[str]:
98
+ def get_docstring(sections: Mapping[str, list[str]]) -> list[str]:
94
99
  """Get (unindeted) docstring from docker-compose <cmd> --help. Use general and usage section.
95
100
  :param sections: Output from `collect_help_lines`
96
101
  """
97
102
  lines = sections["general"]
98
103
  usages = sections.get("usage", [])
99
104
  if usages:
100
- lines += ["Usage:"] + usages
105
+ return [*lines, "Usage:", *usages]
101
106
  return lines
102
107
 
103
108
 
104
- def type_arg(arg: Argument) -> Tuple[str, str]:
109
+ def type_arg(arg: Argument) -> tuple[str, str]:
105
110
  """Generate the argument for docker-compose as a string, together with the doc-string"""
106
111
  type_str = f"Optional[{arg.type_str}]"
107
112
  return f"{arg.arg}: {type_str} = None", f'"""{arg.description}"""'
108
113
 
109
114
 
110
115
  def get_def_commands(
111
- sections: Mapping[str, List[str]], level: int = 0
112
- ) -> Iterator[Tuple[str, List[str]]]:
116
+ sections: Mapping[str, list[str]], level: int = 0
117
+ ) -> Iterator[tuple[str, list[str]]]:
113
118
  """Generate command functions as string"""
114
119
  commands = sections.get("commands", None)
115
120
  if not commands:
@@ -118,7 +123,7 @@ def get_def_commands(
118
123
  if not line.strip():
119
124
  continue
120
125
  cmd = line.split()[0]
121
- logger.debug("Generate run for {}", cmd)
126
+ logger.debug("Generate run for %s", cmd)
122
127
  docker_lines = get_help_message(cmd)
123
128
  nl = level + 4
124
129
  sections, arguments = parse_help(docker_lines)
@@ -147,47 +152,47 @@ def {cmd}(self, {args}) -> {class_name}:
147
152
  )
148
153
 
149
154
 
150
- def generate_class(class_name: str, cmd: str, level=0) -> str:
155
+ def generate_class(cmd: str) -> str:
156
+ class_name = f"DockerCompose{(cmd or 'Root').capitalize()}"
151
157
  docker_lines = get_help_message(cmd)
152
- nl = level + 4
158
+ nl = 4
153
159
  sections, arguments = parse_help(docker_lines)
154
160
  _add_custom_arguments(cmd, arguments)
155
161
  cmd_fns = ""
156
- add_imports: Set[str] = set()
162
+ add_imports: set[str] = set()
157
163
  if "commands" in sections:
158
- logger.info("Found commands in section {}", cmd)
164
+ logger.info("Found commands in section %s", cmd)
159
165
  for cmd_fn, add_import in get_def_commands(sections):
160
166
  cmd_fns += cmd_fn
161
167
  add_imports = add_imports.union(add_import)
162
- args: List[str] = _flatten(list(type_arg(arg)) for arg in arguments)
168
+ args: list[str] = reduce(add, (list(type_arg(arg)) for arg in arguments), [])
163
169
 
164
170
  # List of argument that are options only
165
171
  options = ", ".join([f'"{arg.arg}"' for arg in arguments if arg.is_option])
166
172
  if options:
167
173
  options = options + ","
168
- new_line = "\n"
169
- res = indent(
170
- f'''
171
- # DO NOT EDIT: Autogenerated by {__file__}
172
- # for {_version("docker-compose")}
173
-
174
- import attr
175
- from typing import Optional, List
174
+ new_line = "\n" # Python 3.9 does not support backslashes in format-strings.
175
+
176
+ res = f'''
177
+ # DO NOT EDIT: Autogenerated by {"/".join(Path(__file__).parts[-3:])}
178
+ # for {_version()}
179
+
180
+ import dataclasses as _dc
181
+ from typing import Optional
182
+
176
183
  from docker_composer.base import DockerBaseRunner
177
184
  {new_line.join(add_imports)}
178
185
 
179
- @attr.s(auto_attribs=True)
186
+ @_dc.dataclass()
180
187
  class {class_name}(DockerBaseRunner):
181
188
  """
182
189
  {indent(get_docstring(sections), level=nl)}
183
190
  """
184
191
  {indent(args, level=nl)}
185
- _cmd: str = "{cmd or ""}"
186
- _options: List[str] = [{options}]
192
+ _cmd: str = _dc.field(default="{cmd or ""}", repr=False, init=False)
193
+ _options: list[str] = _dc.field(default_factory=lambda: [{options}], repr=False, init=False)
187
194
  {indent(cmd_fns, level=nl)}
188
- ''',
189
- level=level,
190
- )
195
+ '''
191
196
  try:
192
197
  res = isort.code(
193
198
  res, config=isort.Config(settings_path=project_root().as_posix())
@@ -201,7 +206,7 @@ class {class_name}(DockerBaseRunner):
201
206
  return res
202
207
 
203
208
 
204
- def write_class(cmd: str) -> None:
209
+ def write_class(cmd: str = "") -> None:
205
210
  """
206
211
  Generate a class for `cmd` and write it to `file_name`
207
212
 
@@ -210,17 +215,14 @@ def write_class(cmd: str) -> None:
210
215
  :return:
211
216
  """
212
217
  base_path = Path(__file__).parents[1] / "runner"
213
- if cmd:
214
- file_name = base_path / "cmd" / f"{cmd}.py"
215
- else:
216
- file_name = base_path / "root.py"
217
-
218
- class_str = generate_class(f"DockerCompose{(cmd or 'Root').capitalize()}", cmd)
219
- logger.info("Write {:<8s} -> {}", cmd, file_name)
218
+ file_name = (base_path / "cmd" / f"{cmd}.py") if cmd else (base_path / "root.py")
219
+ class_str = generate_class(cmd)
220
+ logger.info("Write %s -> %s", cmd, file_name)
220
221
  file_name.write_text(class_str, encoding="utf-8")
221
222
 
222
223
 
223
- def _add_custom_arguments(cmd: str, arguments: list[Argument]):
224
+ def _add_custom_arguments(cmd: str, arguments: list[Argument]) -> None:
225
+ """Add the verbose option to arguments."""
224
226
  if cmd == "":
225
227
  verbose = Argument("verbose", "OPTION", bool, "Use verbose output")
226
228
  if verbose not in arguments:
@@ -228,8 +230,8 @@ def _add_custom_arguments(cmd: str, arguments: list[Argument]):
228
230
 
229
231
 
230
232
  def main() -> None:
231
- write_class("")
232
- docker_lines = get_help_message("")
233
+ write_class()
234
+ docker_lines = get_help_message()
233
235
  sections, _ = parse_help(docker_lines)
234
236
  for cmd_line in sections["commands"]:
235
237
  if not cmd_line:
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+
5
+
6
+ def docker_compose_version_short() -> str:
7
+ return subprocess.check_output(
8
+ ["docker", "compose", "version", "--short"],
9
+ text=True,
10
+ ).strip()
11
+
12
+
13
+ def main() -> None:
14
+ version = docker_compose_version_short()
15
+ if not version:
16
+ raise RuntimeError("docker compose version --short returned an empty version")
17
+ subprocess.check_call(["uv", "version", version])
18
+
19
+
20
+ if __name__ == "__main__":
21
+ main() # pragma: no cover