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