pyinfra 2.9.1__py2.py3-none-any.whl → 3.0__py2.py3-none-any.whl

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 (156) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +265 -253
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +68 -53
  5. pyinfra/api/config.py +139 -32
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +7 -26
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +102 -137
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +21 -25
  13. pyinfra/api/operation.py +240 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +79 -86
  17. pyinfra/connectors/base.py +147 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +220 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +196 -208
  22. pyinfra/connectors/ssh.py +530 -613
  23. pyinfra/connectors/ssh_util.py +114 -0
  24. pyinfra/connectors/sshuserclient/client.py +5 -3
  25. pyinfra/connectors/terraform.py +86 -65
  26. pyinfra/connectors/util.py +211 -137
  27. pyinfra/connectors/vagrant.py +60 -53
  28. pyinfra/context.py +4 -2
  29. pyinfra/facts/apk.py +2 -0
  30. pyinfra/facts/apt.py +2 -0
  31. pyinfra/facts/brew.py +2 -0
  32. pyinfra/facts/bsdinit.py +2 -0
  33. pyinfra/facts/cargo.py +2 -0
  34. pyinfra/facts/choco.py +2 -0
  35. pyinfra/facts/deb.py +7 -2
  36. pyinfra/facts/dnf.py +2 -0
  37. pyinfra/facts/docker.py +19 -0
  38. pyinfra/facts/files.py +47 -32
  39. pyinfra/facts/gem.py +2 -0
  40. pyinfra/facts/git.py +3 -1
  41. pyinfra/facts/gpg.py +3 -1
  42. pyinfra/facts/hardware.py +34 -24
  43. pyinfra/facts/iptables.py +5 -3
  44. pyinfra/facts/launchd.py +2 -0
  45. pyinfra/facts/lxd.py +2 -0
  46. pyinfra/facts/mysql.py +13 -6
  47. pyinfra/facts/npm.py +1 -0
  48. pyinfra/facts/openrc.py +2 -0
  49. pyinfra/facts/pacman.py +6 -2
  50. pyinfra/facts/pip.py +2 -0
  51. pyinfra/facts/pkg.py +2 -0
  52. pyinfra/facts/pkgin.py +2 -0
  53. pyinfra/facts/postgres.py +168 -0
  54. pyinfra/facts/postgresql.py +6 -160
  55. pyinfra/facts/rpm.py +12 -9
  56. pyinfra/facts/runit.py +68 -0
  57. pyinfra/facts/selinux.py +3 -1
  58. pyinfra/facts/server.py +80 -36
  59. pyinfra/facts/snap.py +2 -0
  60. pyinfra/facts/systemd.py +31 -12
  61. pyinfra/facts/sysvinit.py +10 -10
  62. pyinfra/facts/upstart.py +2 -0
  63. pyinfra/facts/util/packaging.py +7 -4
  64. pyinfra/facts/vzctl.py +2 -0
  65. pyinfra/facts/xbps.py +2 -0
  66. pyinfra/facts/yum.py +2 -0
  67. pyinfra/facts/zypper.py +2 -0
  68. pyinfra/local.py +4 -5
  69. pyinfra/operations/apk.py +6 -4
  70. pyinfra/operations/apt.py +46 -65
  71. pyinfra/operations/brew.py +17 -22
  72. pyinfra/operations/bsdinit.py +9 -7
  73. pyinfra/operations/cargo.py +4 -2
  74. pyinfra/operations/choco.py +4 -2
  75. pyinfra/operations/dnf.py +19 -23
  76. pyinfra/operations/docker.py +339 -0
  77. pyinfra/operations/files.py +188 -386
  78. pyinfra/operations/gem.py +4 -2
  79. pyinfra/operations/git.py +24 -53
  80. pyinfra/operations/iptables.py +29 -35
  81. pyinfra/operations/launchd.py +6 -7
  82. pyinfra/operations/lxd.py +8 -13
  83. pyinfra/operations/mysql.py +62 -81
  84. pyinfra/operations/npm.py +9 -2
  85. pyinfra/operations/openrc.py +6 -4
  86. pyinfra/operations/pacman.py +7 -8
  87. pyinfra/operations/pip.py +25 -24
  88. pyinfra/operations/pkg.py +4 -2
  89. pyinfra/operations/pkgin.py +6 -4
  90. pyinfra/operations/postgres.py +349 -0
  91. pyinfra/operations/postgresql.py +18 -379
  92. pyinfra/operations/puppet.py +3 -1
  93. pyinfra/operations/python.py +8 -19
  94. pyinfra/operations/runit.py +182 -0
  95. pyinfra/operations/selinux.py +47 -44
  96. pyinfra/operations/server.py +111 -127
  97. pyinfra/operations/snap.py +4 -4
  98. pyinfra/operations/ssh.py +20 -33
  99. pyinfra/operations/systemd.py +19 -15
  100. pyinfra/operations/sysvinit.py +9 -16
  101. pyinfra/operations/upstart.py +9 -7
  102. pyinfra/operations/util/__init__.py +12 -0
  103. pyinfra/operations/util/docker.py +177 -0
  104. pyinfra/operations/util/files.py +24 -16
  105. pyinfra/operations/util/packaging.py +55 -57
  106. pyinfra/operations/util/service.py +39 -51
  107. pyinfra/operations/vzctl.py +12 -10
  108. pyinfra/operations/xbps.py +6 -4
  109. pyinfra/operations/yum.py +18 -22
  110. pyinfra/operations/zypper.py +12 -13
  111. pyinfra/version.py +5 -2
  112. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
  113. pyinfra-3.0.dist-info/RECORD +167 -0
  114. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
  115. pyinfra-3.0.dist-info/entry_points.txt +11 -0
  116. pyinfra_cli/__main__.py +4 -3
  117. pyinfra_cli/commands.py +7 -2
  118. pyinfra_cli/exceptions.py +78 -42
  119. pyinfra_cli/inventory.py +40 -6
  120. pyinfra_cli/log.py +17 -3
  121. pyinfra_cli/main.py +133 -90
  122. pyinfra_cli/prints.py +95 -127
  123. pyinfra_cli/util.py +62 -29
  124. tests/test_api/test_api.py +2 -0
  125. tests/test_api/test_api_arguments.py +13 -13
  126. tests/test_api/test_api_deploys.py +28 -29
  127. tests/test_api/test_api_facts.py +60 -98
  128. tests/test_api/test_api_operations.py +101 -201
  129. tests/test_cli/test_cli.py +18 -49
  130. tests/test_cli/test_cli_deploy.py +11 -37
  131. tests/test_cli/test_cli_exceptions.py +50 -19
  132. tests/test_cli/util.py +1 -1
  133. tests/test_connectors/test_chroot.py +6 -6
  134. tests/test_connectors/test_docker.py +4 -4
  135. tests/test_connectors/test_dockerssh.py +38 -50
  136. tests/test_connectors/test_local.py +11 -12
  137. tests/test_connectors/test_ssh.py +105 -93
  138. tests/test_connectors/test_terraform.py +9 -15
  139. tests/test_connectors/test_util.py +24 -46
  140. tests/test_connectors/test_vagrant.py +7 -7
  141. pyinfra/api/operation.pyi +0 -117
  142. pyinfra/connectors/ansible.py +0 -171
  143. pyinfra/connectors/mech.py +0 -186
  144. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  145. pyinfra/connectors/winrm.py +0 -320
  146. pyinfra/facts/windows.py +0 -366
  147. pyinfra/facts/windows_files.py +0 -90
  148. pyinfra/operations/windows.py +0 -59
  149. pyinfra/operations/windows_files.py +0 -551
  150. pyinfra-2.9.1.dist-info/RECORD +0 -170
  151. pyinfra-2.9.1.dist-info/entry_points.txt +0 -14
  152. tests/test_connectors/test_ansible.py +0 -64
  153. tests/test_connectors/test_mech.py +0 -126
  154. tests/test_connectors/test_winrm.py +0 -76
  155. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
  156. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
pyinfra/api/operation.py CHANGED
@@ -5,83 +5,150 @@ to the deploy state. This is then run later by pyinfra's ``__main__`` or the
5
5
  :doc:`./pyinfra.api.operations` module.
6
6
  """
7
7
 
8
+ from __future__ import annotations
9
+
8
10
  from functools import wraps
11
+ from inspect import signature
12
+ from io import StringIO
9
13
  from types import FunctionType
10
- from typing import TYPE_CHECKING
14
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, Optional, cast
15
+
16
+ from typing_extensions import ParamSpec
11
17
 
12
18
  import pyinfra
13
19
  from pyinfra import context, logger
14
20
  from pyinfra.context import ctx_host, ctx_state
15
21
 
16
- from .arguments import get_execution_kwarg_keys, pop_global_arguments
17
- from .command import StringCommand
22
+ from .arguments import EXECUTION_KWARG_KEYS, AllArguments, pop_global_arguments
23
+ from .arguments_typed import PyinfraOperation
24
+ from .command import PyinfraCommand, StringCommand
18
25
  from .exceptions import OperationValueError, PyinfraError
19
26
  from .host import Host
20
27
  from .operations import run_host_op
28
+ from .state import State, StateOperationHostData, StateOperationMeta
21
29
  from .util import (
22
- get_args_kwargs_spec,
23
30
  get_call_location,
31
+ get_file_sha1,
24
32
  get_operation_order_from_stack,
25
33
  log_operation_start,
26
34
  make_hash,
27
- memoize,
28
35
  )
29
36
 
30
- if TYPE_CHECKING:
31
- from pyinfra.api.state import State
32
-
33
37
  op_meta_default = object()
34
38
 
39
+ if TYPE_CHECKING:
40
+ from pyinfra.connectors.util import CommandOutput
41
+
35
42
 
36
43
  class OperationMeta:
37
- combined_output_lines = None
44
+ _hash: str
38
45
 
39
- def __init__(self, hash=None, commands=None):
40
- # Wrap all the attributes
41
- commands = commands or []
42
- self.commands = commands
43
- self.hash = hash
46
+ _combined_output: Optional["CommandOutput"] = None
47
+ _commands: Optional[list[Any]] = None
48
+ _maybe_is_change: Optional[bool] = None
49
+ _success: Optional[bool] = None
44
50
 
45
- # Changed flag = did we do anything?
46
- self.changed = len(self.commands) > 0
51
+ def __init__(self, hash, is_change: Optional[bool]):
52
+ self._hash = hash
53
+ self._maybe_is_change = is_change
47
54
 
48
- def __repr__(self):
55
+ def __repr__(self) -> str:
49
56
  """
50
57
  Return Operation object as a string.
51
58
  """
52
59
 
60
+ if self._commands is not None:
61
+ return (
62
+ "OperationMeta(executed=True, "
63
+ f"success={self.did_succeed}, hash={self._hash}, commands={len(self._commands)})"
64
+ )
53
65
  return (
54
- f"OperationMeta(commands={len(self.commands)}, "
55
- f"changed={self.changed}, hash={self.hash})"
66
+ "OperationMeta(executed=False, "
67
+ f"maybeChange={self._maybe_is_change}, hash={self._hash})"
56
68
  )
57
69
 
58
- def set_combined_output_lines(self, combined_output_lines):
59
- self.combined_output_lines = combined_output_lines
70
+ # Completion & status checks
71
+ def set_complete(
72
+ self,
73
+ success: bool,
74
+ commands: list[Any],
75
+ combined_output: "CommandOutput",
76
+ ) -> None:
77
+ if self.is_complete():
78
+ raise RuntimeError("Cannot complete an already complete operation")
79
+ self._success = success
80
+ self._commands = commands
81
+ self._combined_output = combined_output
82
+
83
+ def is_complete(self) -> bool:
84
+ return self._success is not None
85
+
86
+ def _raise_if_not_complete(self) -> None:
87
+ if not self.is_complete():
88
+ raise RuntimeError("Cannot evaluate operation result before execution")
89
+
90
+ @property
91
+ def will_change(self) -> bool:
92
+ if self._maybe_is_change is not None:
93
+ return self._maybe_is_change
94
+
95
+ op_data = context.state.get_op_data_for_host(context.host, self._hash)
96
+ cmd_gen = op_data.command_generator
97
+ for _ in cmd_gen():
98
+ self._maybe_is_change = True
99
+ return True
100
+ self._maybe_is_change = False
101
+ return False
102
+
103
+ def _did_change(self) -> bool:
104
+ return bool(self._success and len(self._commands or []) > 0)
105
+
106
+ @property
107
+ def did_change(self):
108
+ return context.host.when(self._did_change)
109
+
110
+ @property
111
+ def did_not_change(self):
112
+ return context.host.when(lambda: not self._did_change())
113
+
114
+ def did_succeed(self, _raise_if_not_complete=True) -> bool:
115
+ if _raise_if_not_complete:
116
+ self._raise_if_not_complete()
117
+ return self._success is True
60
118
 
61
- def _get_lines(self, types=("stdout", "stderr")):
62
- if self.combined_output_lines is None:
63
- raise AttributeError("Output is not available until operations have been executed")
119
+ def did_error(self) -> bool:
120
+ self._raise_if_not_complete()
121
+ return self._success is False
64
122
 
65
- return [line for type_, line in self.combined_output_lines if type_ in types]
123
+ # TODO: deprecated, remove in v4
124
+ @property
125
+ def changed(self) -> bool:
126
+ if self.is_complete():
127
+ return self._did_change()
128
+ return self.will_change
66
129
 
67
130
  @property
68
- def stdout_lines(self):
69
- return self._get_lines(types=("stdout",))
131
+ def stdout_lines(self) -> list[str]:
132
+ self._raise_if_not_complete()
133
+ assert self._combined_output is not None
134
+ return self._combined_output.stdout_lines
70
135
 
71
136
  @property
72
- def stderr_lines(self):
73
- return self._get_lines(types=("stderr",))
137
+ def stderr_lines(self) -> list[str]:
138
+ self._raise_if_not_complete()
139
+ assert self._combined_output is not None
140
+ return self._combined_output.stderr_lines
74
141
 
75
142
  @property
76
- def stdout(self):
143
+ def stdout(self) -> str:
77
144
  return "\n".join(self.stdout_lines)
78
145
 
79
146
  @property
80
- def stderr(self):
147
+ def stderr(self) -> str:
81
148
  return "\n".join(self.stderr_lines)
82
149
 
83
150
 
84
- def add_op(state: "State", op_func, *args, **kwargs):
151
+ def add_op(state: State, op_func, *args, **kwargs):
85
152
  """
86
153
  Prepare & add an operation to ``pyinfra.state`` by executing it on all hosts.
87
154
 
@@ -112,85 +179,71 @@ def add_op(state: "State", op_func, *args, **kwargs):
112
179
  return results
113
180
 
114
181
 
115
- @memoize
116
- def show_state_host_arguments_warning(call_location):
117
- logger.warning(
118
- (
119
- "{0}:\n\tLegacy operation function detected! Operations should no longer define "
120
- "`state` and `host` arguments."
121
- ).format(call_location),
122
- )
182
+ P = ParamSpec("P")
123
183
 
124
184
 
125
185
  def operation(
126
- func=None,
127
- pipeline_facts=None,
128
186
  is_idempotent: bool = True,
129
- idempotent_notice=None,
130
- frame_offset: int = 1,
131
- ):
187
+ idempotent_notice: Optional[str] = None,
188
+ is_deprecated: bool = False,
189
+ deprecated_for: Optional[str] = None,
190
+ _set_in_op: bool = True,
191
+ ) -> Callable[[Callable[P, Generator]], PyinfraOperation[P]]:
132
192
  """
133
193
  Decorator that takes a simple module function and turn it into the internal
134
194
  operation representation that consists of a list of commands + options
135
195
  (sudo, (sudo|su)_user, env).
136
196
  """
137
197
 
138
- # If not decorating, return function with config attached
139
- if func is None:
198
+ def decorator(f: Callable[P, Generator]) -> PyinfraOperation[P]:
199
+ f.is_idempotent = is_idempotent # type: ignore[attr-defined]
200
+ f.idempotent_notice = idempotent_notice # type: ignore[attr-defined]
201
+ f.is_deprecated = is_deprecated # type: ignore[attr-defined]
202
+ f.deprecated_for = deprecated_for # type: ignore[attr-defined]
203
+ return _wrap_operation(f, _set_in_op=_set_in_op)
140
204
 
141
- def decorator(f):
142
- f.pipeline_facts = pipeline_facts
143
- f.is_idempotent = is_idempotent
144
- f.idempotent_notice = idempotent_notice
145
- return operation(f, frame_offset=2)
205
+ return decorator
146
206
 
147
- return decorator
148
207
 
149
- # Check whether an operation is "legacy" - ie contains state=None, host=None kwargs
150
- # TODO: remove this in v3
151
- is_legacy = False
152
- args, kwargs = get_args_kwargs_spec(func)
153
- if all(key in kwargs and kwargs[key] is None for key in ("state", "host")):
154
- show_state_host_arguments_warning(get_call_location(frame_offset=frame_offset))
155
- is_legacy = True
156
- func.is_legacy = is_legacy
157
-
158
- # Actually decorate!
208
+ def _wrap_operation(func: Callable[P, Generator], _set_in_op: bool = True) -> PyinfraOperation[P]:
159
209
  @wraps(func)
160
- def decorated_func(*args, **kwargs):
210
+ def decorated_func(*args: P.args, **kwargs: P.kwargs) -> OperationMeta:
161
211
  state = context.state
162
212
  host = context.host
163
213
 
214
+ if host.in_op:
215
+ raise Exception(
216
+ "Operation called within another operation, this is not allowed! Use the `_inner` "
217
+ + "function to call the underlying operation."
218
+ )
219
+
220
+ if func.is_deprecated: # type: ignore[attr-defined]
221
+ if func.deprecated_for: # type: ignore[attr-defined]
222
+ logger.warning(
223
+ f"The {get_operation_name_from_func(func)} operation is "
224
+ + f"deprecated, please use: {func.deprecated_for}", # type: ignore[attr-defined] # noqa
225
+ )
226
+ else:
227
+ logger.warning(f"The {get_operation_name_from_func(func)} operation is deprecated")
228
+
164
229
  # Configure operation
165
230
  #
166
231
  # Get the meta kwargs (globals that apply to all hosts)
167
- global_kwargs, global_kwarg_keys = pop_global_arguments(kwargs)
168
-
169
- # If this op is being called inside another, just return here
170
- # (any unwanted/op-related kwargs removed above).
171
- if host.in_op:
172
- if global_kwarg_keys:
173
- _error_msg = "Nested operation called with global arguments: {0} ({1})".format(
174
- global_kwarg_keys,
175
- get_call_location(),
176
- )
177
- raise PyinfraError(_error_msg)
178
- return func(*args, **kwargs) or []
232
+ global_arguments, global_argument_keys = pop_global_arguments(kwargs)
179
233
 
180
- kwargs = _solve_legacy_operation_arguments(func, state, host, kwargs)
181
- names, add_args = _generate_operation_name(func, host, kwargs, global_kwargs)
182
- op_order, op_hash = _solve_operation_consistency(names, state, host)
234
+ names, add_args = generate_operation_name(func, host, kwargs, global_arguments)
235
+ op_order, op_hash = solve_operation_consistency(names, state, host)
183
236
 
184
237
  # Ensure shared (between servers) operation meta, mutates state
185
- op_meta = _ensure_shared_op_meta(state, op_hash, op_order, global_kwargs, names)
238
+ op_meta = ensure_shared_op_meta(state, op_hash, op_order, global_arguments, names)
186
239
 
187
240
  # Attach normal args, if we're auto-naming this operation
188
241
  if add_args:
189
- op_meta = _attach_args(op_meta, args, kwargs)
242
+ op_meta = attach_args(op_meta, args, kwargs)
190
243
 
191
244
  # Check if we're actually running the operation on this host
192
245
  # Run once and we've already added meta for this op? Stop here.
193
- if op_meta["run_once"]:
246
+ if op_meta.global_arguments["_run_once"]:
194
247
  has_run = False
195
248
  for ops in state.ops.values():
196
249
  if op_hash in ops:
@@ -198,75 +251,96 @@ def operation(
198
251
  break
199
252
 
200
253
  if has_run:
201
- return OperationMeta(op_hash)
202
-
203
- # "Run" operation
204
- #
254
+ return OperationMeta(op_hash, is_change=False)
255
+
256
+ # Grab a reference to any *current* deploy data as this may change when
257
+ # we later evaluate the operation at runtime.This means we put back the
258
+ # expected deploy data.
259
+ current_deploy_data = host.current_deploy_data
260
+
261
+ # "Run" operation - here we make a generator that will yield out actual commands to execute
262
+ # and, if we're diff-ing, we then iterate the generator now to determine if any changes
263
+ # *would* be made based on the *current* remote state.
264
+
265
+ def command_generator() -> Iterator[PyinfraCommand]:
266
+ # Check global _if argument function and do nothing if returns False
267
+ if state.is_executing:
268
+ _ifs = global_arguments.get("_if")
269
+ if isinstance(_ifs, list) and not all(_if() for _if in _ifs):
270
+ return
271
+ elif callable(_ifs) and not _ifs():
272
+ return
273
+
274
+ host.in_op = _set_in_op
275
+ host.current_op_hash = op_hash
276
+ host.current_op_global_arguments = global_arguments
277
+ host.current_op_deploy_data = current_deploy_data
278
+
279
+ try:
280
+ for command in func(*args, **kwargs):
281
+ if isinstance(command, str):
282
+ command = StringCommand(command.strip())
283
+ yield command
284
+ finally:
285
+ host.in_op = False
286
+ host.current_op_hash = None
287
+ host.current_op_global_arguments = None
288
+ host.current_op_deploy_data = None
289
+
290
+ op_is_change = None
291
+ if state.should_check_for_changes():
292
+ op_is_change = False
293
+ for _ in command_generator():
294
+ op_is_change = True
295
+ break
296
+ else:
297
+ # If not calling the op function to check for change we still want to ensure the args
298
+ # are valid, so use Signature.bind to trigger any TypeError.
299
+ signature(func).bind(*args, **kwargs)
205
300
 
206
- # Otherwise, flag as in-op and run it to get the commands
207
- host.in_op = True
208
- host.current_op_hash = op_hash
209
- host.current_op_global_kwargs = global_kwargs
301
+ # Add host-specific operation data to state, this mutates state
302
+ host_meta = state.get_meta_for_host(host)
303
+ host_meta.ops += 1
304
+ if op_is_change:
305
+ host_meta.ops_change += 1
306
+ else:
307
+ host_meta.ops_no_change += 1
210
308
 
211
- # Convert to list as the result may be a generator
212
- commands = func(*args, **kwargs)
213
- commands = [ # convert any strings -> StringCommand's
214
- StringCommand(command.strip()) if isinstance(command, str) else command
215
- for command in commands
216
- ]
309
+ operation_meta = OperationMeta(op_hash, op_is_change)
217
310
 
218
- host.in_op = False
219
- host.current_op_hash = None
220
- host.current_op_global_kwargs = None
311
+ # Add the server-relevant commands
312
+ op_data = StateOperationHostData(command_generator, global_arguments, operation_meta)
313
+ state.set_op_data_for_host(host, op_hash, op_data)
221
314
 
222
- # Add host-specific operation data to state, this mutates state
223
- operation_meta = _update_state_meta(state, host, commands, op_hash, op_meta, global_kwargs)
315
+ # If we're already in the execution phase, execute this operation immediately
316
+ if state.is_executing:
317
+ execute_immediately(state, host, op_hash)
224
318
 
225
319
  # Return result meta for use in deploy scripts
226
320
  return operation_meta
227
321
 
228
- decorated_func._pyinfra_op = func # type: ignore
229
- return decorated_func
322
+ decorated_func._inner = func # type: ignore[attr-defined]
323
+ return cast(PyinfraOperation[P], decorated_func)
230
324
 
231
325
 
232
- def _solve_legacy_operation_arguments(op_func, state, host, kwargs):
233
- """
234
- Solve legacy operation arguments.
235
- """
236
-
237
- # If this is a legacy operation function (ie - state & host arg kwargs), ensure that state
238
- # and host are included as kwargs.
239
-
240
- # Legacy operation arguments
241
- if op_func.is_legacy:
242
- if "state" not in kwargs:
243
- kwargs["state"] = state
244
- if "host" not in kwargs:
245
- kwargs["host"] = host
246
- # If not legacy, pop off any state/host kwargs that may come from legacy @deploy functions
326
+ def get_operation_name_from_func(func):
327
+ if func.__module__:
328
+ module_bits = func.__module__.split(".")
329
+ module_name = module_bits[-1]
330
+ return "{0}.{1}".format(module_name, func.__name__)
247
331
  else:
248
- kwargs.pop("state", None)
249
- kwargs.pop("host", None)
250
-
251
- return kwargs
332
+ return func.__name__
252
333
 
253
334
 
254
- def _generate_operation_name(func, host, kwargs, global_kwargs):
335
+ def generate_operation_name(func, host, kwargs, global_arguments):
255
336
  # Generate an operation name if needed (Module/Operation format)
256
- name = global_kwargs.get("name")
337
+ name = global_arguments.get("name")
257
338
  add_args = False
258
339
  if name:
259
340
  names = {name}
260
341
  else:
261
342
  add_args = True
262
-
263
- if func.__module__:
264
- module_bits = func.__module__.split(".")
265
- module_name = module_bits[-1]
266
- name = "{0}/{1}".format(module_name.title(), func.__name__.title())
267
- else:
268
- name = func.__name__
269
-
343
+ name = get_operation_name_from_func(func)
270
344
  names = {name}
271
345
 
272
346
  if host.current_deploy_name:
@@ -275,7 +349,7 @@ def _generate_operation_name(func, host, kwargs, global_kwargs):
275
349
  return names, add_args
276
350
 
277
351
 
278
- def _solve_operation_consistency(names, state, host):
352
+ def solve_operation_consistency(names, state, host):
279
353
  # Operation order is used to tie-break available nodes in the operation DAG, in CLI mode
280
354
  # we use stack call order so this matches as defined by the user deploy code.
281
355
  if pyinfra.is_cli:
@@ -310,87 +384,55 @@ def _solve_operation_consistency(names, state, host):
310
384
 
311
385
 
312
386
  # NOTE: this function mutates state.op_meta for this hash
313
- def _ensure_shared_op_meta(state, op_hash, op_order, global_kwargs, names):
314
- op_meta = state.op_meta.setdefault(
315
- op_hash,
316
- {
317
- "names": set(),
318
- "args": [],
319
- "op_order": op_order,
320
- },
321
- )
322
-
323
- for key in get_execution_kwarg_keys():
324
- global_value = global_kwargs.pop(key)
325
- op_meta_value = op_meta.get(key, op_meta_default)
387
+ def ensure_shared_op_meta(
388
+ state: State,
389
+ op_hash: str,
390
+ op_order: tuple[int, ...],
391
+ global_arguments: AllArguments,
392
+ names: set[str],
393
+ ):
394
+ op_meta = state.op_meta.setdefault(op_hash, StateOperationMeta(op_order))
395
+
396
+ for key in EXECUTION_KWARG_KEYS:
397
+ global_value = global_arguments.pop(key) # type: ignore[misc]
398
+ op_meta_value = op_meta.global_arguments.get(key, op_meta_default)
326
399
 
327
400
  if op_meta_value is not op_meta_default and global_value != op_meta_value:
328
401
  raise OperationValueError("Cannot have different values for `{0}`.".format(key))
329
402
 
330
- op_meta[key] = global_value
403
+ op_meta.global_arguments[key] = global_value # type: ignore[literal-required]
331
404
 
332
405
  # Add any new names to the set
333
- op_meta["names"].update(names)
406
+ op_meta.names.update(names)
334
407
 
335
408
  return op_meta
336
409
 
337
410
 
338
- def _execute_immediately(state, host, op_data, op_meta, op_hash):
339
- logger.warning(
340
- f"Note: nested operations are currently in beta ({get_call_location()})\n"
341
- " More information: "
342
- "https://docs.pyinfra.com/en/2.x/using-operations.html#nested-operations",
343
- )
344
- op_data["parent_op_hash"] = host.executing_op_hash
411
+ def execute_immediately(state, host, op_hash):
412
+ op_meta = state.get_op_meta(op_hash)
413
+ op_data = state.get_op_data_for_host(host, op_hash)
414
+ op_data.parent_op_hash = host.executing_op_hash
345
415
  log_operation_start(op_meta, op_types=["nested"], prefix="")
346
- status = run_host_op(state, host, op_hash)
347
- if status is False:
348
- state.fail_hosts({host})
416
+ run_host_op(state, host, op_hash)
349
417
 
350
418
 
351
- def _attach_args(op_meta, args, kwargs):
352
- for arg in args:
353
- if isinstance(arg, FunctionType):
354
- arg = arg.__name__
419
+ def _get_arg_value(arg):
420
+ if isinstance(arg, FunctionType):
421
+ return arg.__name__
422
+ if isinstance(arg, StringIO):
423
+ return f"StringIO(hash={get_file_sha1(arg)})"
424
+ return arg
355
425
 
356
- if arg not in op_meta["args"]:
357
- op_meta["args"].append(arg)
426
+
427
+ def attach_args(op_meta, args, kwargs):
428
+ for arg in args:
429
+ if arg not in op_meta.args:
430
+ op_meta.args.append(str(_get_arg_value(arg)))
358
431
 
359
432
  # Attach keyword args
360
433
  for key, value in kwargs.items():
361
- if isinstance(value, FunctionType):
362
- value = value.__name__
363
-
364
- arg = "=".join((str(key), str(value)))
365
- if arg not in op_meta["args"]:
366
- op_meta["args"].append(arg)
434
+ arg = "=".join((str(key), str(_get_arg_value(value))))
435
+ if arg not in op_meta.args:
436
+ op_meta.args.append(arg)
367
437
 
368
438
  return op_meta
369
-
370
-
371
- # NOTE: this function mutates state.meta for this host
372
- def _update_state_meta(state, host, commands, op_hash, op_meta, global_kwargs):
373
- # We're doing some commands, meta/ops++
374
- state.meta[host]["ops"] += 1
375
- state.meta[host]["commands"] += len(commands)
376
-
377
- if commands:
378
- state.meta[host]["ops_change"] += 1
379
- else:
380
- state.meta[host]["ops_no_change"] += 1
381
-
382
- operation_meta = OperationMeta(op_hash, commands)
383
-
384
- # Add the server-relevant commands
385
- op_data = {
386
- "commands": commands,
387
- "global_kwargs": global_kwargs,
388
- "operation_meta": operation_meta,
389
- }
390
- state.set_op_data(host, op_hash, op_data)
391
-
392
- # If we're already in the execution phase, execute this operation immediately
393
- if state.is_executing:
394
- _execute_immediately(state, host, op_data, op_meta, op_hash)
395
-
396
- return operation_meta