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/state.py CHANGED
@@ -1,8 +1,11 @@
1
- from contextlib import contextmanager
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from dataclasses import dataclass
5
+ from enum import IntEnum
2
6
  from graphlib import CycleError, TopologicalSorter
3
7
  from multiprocessing import cpu_count
4
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set
5
- from uuid import uuid4
8
+ from typing import TYPE_CHECKING, Callable, Iterator, Optional
6
9
 
7
10
  from gevent.pool import Pool
8
11
  from paramiko import PKey
@@ -10,12 +13,14 @@ from paramiko import PKey
10
13
  from pyinfra import logger
11
14
 
12
15
  from .config import Config
13
- from .exceptions import NoMoreHostsError, PyinfraError
14
- from .util import sha1_hash
16
+ from .exceptions import PyinfraError
15
17
 
16
18
  if TYPE_CHECKING:
19
+ from pyinfra.api.arguments import AllArguments
20
+ from pyinfra.api.command import PyinfraCommand
17
21
  from pyinfra.api.host import Host
18
22
  from pyinfra.api.inventory import Inventory
23
+ from pyinfra.api.operation import OperationMeta
19
24
 
20
25
 
21
26
  # Work out the max parallel we can achieve with the open files limit of the user/process,
@@ -77,6 +82,58 @@ class BaseStateCallback:
77
82
  pass
78
83
 
79
84
 
85
+ class StateStage(IntEnum):
86
+ # Setup - collect inventory & data
87
+ Setup = 1
88
+ # Connect - connect to the inventory
89
+ Connect = 2
90
+ # Prepare - detect operation changes
91
+ Prepare = 3
92
+ # Execute - execute operations
93
+ Execute = 4
94
+ # Disconnect - disconnect from the inventory
95
+ Disconnect = 5
96
+
97
+
98
+ class StateOperationMeta:
99
+ names: set[str]
100
+ args: list[str]
101
+ op_order: tuple[int, ...]
102
+ global_arguments: "AllArguments"
103
+
104
+ def __init__(self, op_order: tuple[int, ...]):
105
+ self.op_order = op_order
106
+ self.names = set()
107
+ self.args = []
108
+ self.global_arguments = {} # type: ignore
109
+
110
+
111
+ @dataclass
112
+ class StateOperationHostData:
113
+ command_generator: Callable[[], Iterator["PyinfraCommand"]]
114
+ global_arguments: "AllArguments"
115
+ operation_meta: "OperationMeta"
116
+ parent_op_hash: Optional[str] = None
117
+
118
+
119
+ class StateHostMeta:
120
+ ops = 0
121
+ ops_change = 0
122
+ ops_no_change = 0
123
+ op_hashes: set[str]
124
+
125
+ def __init__(self):
126
+ self.op_hashes = set()
127
+
128
+
129
+ class StateHostResults:
130
+ ops = 0
131
+ success_ops = 0
132
+ error_ops = 0
133
+ ignored_error_ops = 0
134
+ partial_ops = 0
135
+
136
+
80
137
  class State:
81
138
  """
82
139
  Manages state for a pyinfra deploy.
@@ -93,9 +150,18 @@ class State:
93
150
  # Main gevent pool
94
151
  pool: "Pool"
95
152
 
153
+ # Current stage this state is in
154
+ current_stage: StateStage = StateStage.Setup
155
+ # Warning counters by stage
156
+ stage_warnings: dict[StateStage, int] = defaultdict(int)
157
+
96
158
  # Whether we are executing operations (ie hosts are all ready)
97
159
  is_executing: bool = False
98
160
 
161
+ # Whether we should check for operation changes as part of the operation ordering phase, this
162
+ # allows us to guesstimate which ops will result in changes on which hosts.
163
+ check_for_changes: bool = True
164
+
99
165
  print_noop_info: bool = False # print "[host] noop: reason for noop"
100
166
  print_fact_info: bool = False # print "loaded fact X"
101
167
  print_input: bool = False
@@ -108,20 +174,33 @@ class State:
108
174
  current_deploy_filename: Optional[str] = None
109
175
  current_exec_filename: Optional[str] = None
110
176
  current_op_file_number: int = 0
177
+ should_raise_failed_hosts: Optional[Callable[["State"], bool]] = None
111
178
 
112
179
  def __init__(
113
- self, inventory: Optional["Inventory"] = None, config: Optional["Config"] = None, **kwargs
180
+ self,
181
+ inventory: Optional["Inventory"] = None,
182
+ config: Optional["Config"] = None,
183
+ check_for_changes: bool = True,
184
+ **kwargs,
114
185
  ):
115
- """Initializes the state, the main Pyinfra
186
+ """
187
+ Initializes the state, the main Pyinfra
116
188
 
117
189
  Args:
118
190
  inventory (Optional[Inventory], optional): The inventory. Defaults to None.
119
191
  config (Optional[Config], optional): The config object. Defaults to None.
120
192
  """
193
+ self.check_for_changes = check_for_changes
194
+
121
195
  if inventory:
122
196
  self.init(inventory, config, **kwargs)
123
197
 
124
- def init(self, inventory: "Inventory", config: Optional["Config"], initial_limit=None):
198
+ def init(
199
+ self,
200
+ inventory: "Inventory",
201
+ config: Optional["Config"],
202
+ initial_limit=None,
203
+ ):
125
204
  # Config validation
126
205
  #
127
206
 
@@ -150,79 +229,63 @@ class State:
150
229
  # Actually initialise the state object
151
230
  #
152
231
 
153
- self.callback_handlers: List[BaseStateCallback] = []
232
+ self.callback_handlers: list[BaseStateCallback] = []
154
233
 
155
234
  # Setup greenlet pools
156
235
  self.pool = Pool(config.PARALLEL)
157
236
  self.fact_pool = Pool(config.PARALLEL)
158
237
 
159
238
  # Private keys
160
- self.private_keys: Dict[str, PKey] = {}
239
+ self.private_keys: dict[str, PKey] = {}
161
240
 
162
241
  # Assign inventory/config
163
242
  self.inventory = inventory
164
243
  self.config = config
165
244
 
166
245
  # Hosts we've activated at any time
167
- self.activated_hosts: Set["Host"] = set()
246
+ self.activated_hosts: set["Host"] = set()
168
247
  # Active hosts that *haven't* failed yet
169
- self.active_hosts: Set["Host"] = set()
248
+ self.active_hosts: set["Host"] = set()
170
249
  # Hosts that have failed
171
- self.failed_hosts: Set["Host"] = set()
250
+ self.failed_hosts: set["Host"] = set()
172
251
 
173
252
  # Limit hosts changes dynamically to limit operations to a subset of hosts
174
- self.limit_hosts: List["Host"] = initial_limit
253
+ self.limit_hosts: list["Host"] = initial_limit
175
254
 
176
255
  # Op basics
177
- self.op_meta: Dict[str, dict] = {} # maps operation hash -> names/etc
178
- self.ops_run: Set[str] = set() # list of ops which have been started/run
256
+ self.op_meta: dict[str, StateOperationMeta] = {} # maps operation hash -> names/etc
179
257
 
180
258
  # Op dict for each host
181
- self.ops: Dict["Host", dict] = {host: {} for host in inventory}
182
-
183
- # Facts dict for each host
184
- self.facts: Dict["Host", Any] = {host: {} for host in inventory}
259
+ self.ops: dict["Host", dict[str, StateOperationHostData]] = {host: {} for host in inventory}
185
260
 
186
261
  # Meta dict for each host
187
- self.meta = {
188
- host: {
189
- "ops": 0, # one function call in a deploy file
190
- "ops_change": 0,
191
- "ops_no_change": 0,
192
- "commands": 0, # actual # of commands to run
193
- "op_hashes": set(),
194
- }
195
- for host in inventory
196
- }
262
+ self.meta: dict["Host", StateHostMeta] = {host: StateHostMeta() for host in inventory}
197
263
 
198
264
  # Results dict for each host
199
- self.results = {
200
- host: {
201
- "ops": 0, # success_ops + failed ops w/ignore_errors
202
- "success_ops": 0,
203
- "error_ops": 0,
204
- "ignored_error_ops": 0,
205
- "partial_ops": 0, # operations that had an error, but did do something
206
- "commands": 0,
207
- }
208
- for host in inventory
265
+ self.results: dict["Host", StateHostResults] = {
266
+ host: StateHostResults() for host in inventory
209
267
  }
210
268
 
211
269
  # Assign state back references to inventory & config
212
270
  inventory.state = config.state = self
213
271
  for host in inventory:
214
- host.state = self
272
+ host.init(self)
215
273
 
216
274
  self.initialised = True
217
275
 
218
- def to_dict(self):
219
- return {
220
- "op_order": self.get_op_order(),
221
- "ops": self.ops,
222
- "facts": self.facts,
223
- "meta": self.meta,
224
- "results": self.results,
225
- }
276
+ def set_stage(self, stage: StateStage) -> None:
277
+ if stage < self.current_stage:
278
+ raise Exception("State stage cannot go backwards!")
279
+ self.current_stage = stage
280
+
281
+ def increment_warning_counter(self) -> None:
282
+ self.stage_warnings[self.current_stage] += 1
283
+
284
+ def get_warning_counter(self) -> int:
285
+ return self.stage_warnings[self.current_stage]
286
+
287
+ def should_check_for_changes(self):
288
+ return self.check_for_changes
226
289
 
227
290
  def add_callback_handler(self, handler):
228
291
  if not isinstance(handler, BaseStateCallback):
@@ -236,18 +299,8 @@ class State:
236
299
  func = getattr(handler, method_name)
237
300
  func(self, *args, **kwargs)
238
301
 
239
- @contextmanager
240
- def preserve_loop_order(self, items):
241
- logger.warning(
242
- (
243
- "Using `state.preserve_loop_order` is not longer required for operations to be "
244
- "executed in correct loop order and can be safely removed."
245
- ),
246
- )
247
- yield lambda: items
248
-
249
302
  def get_op_order(self):
250
- ts = TopologicalSorter()
303
+ ts: TopologicalSorter = TopologicalSorter()
251
304
 
252
305
  for host in self.inventory:
253
306
  for i, op_hash in enumerate(host.op_hash_order):
@@ -275,20 +328,35 @@ class State:
275
328
  # dependency order we order them by line numbers.
276
329
  node_group = sorted(
277
330
  ts.get_ready(),
278
- key=lambda op_hash: self.op_meta[op_hash]["op_order"],
331
+ key=lambda op_hash: self.op_meta[op_hash].op_order,
279
332
  )
280
333
  ts.done(*node_group)
281
334
  final_op_order.extend(node_group)
282
335
 
283
336
  return final_op_order
284
337
 
285
- def get_op_meta(self, op_hash: str):
338
+ def get_op_meta(self, op_hash: str) -> StateOperationMeta:
286
339
  return self.op_meta[op_hash]
287
340
 
288
- def get_op_data(self, host: "Host", op_hash: str):
341
+ def get_meta_for_host(self, host: "Host") -> StateHostMeta:
342
+ return self.meta[host]
343
+
344
+ def get_results_for_host(self, host: "Host") -> StateHostResults:
345
+ return self.results[host]
346
+
347
+ def get_op_data_for_host(
348
+ self,
349
+ host: "Host",
350
+ op_hash: str,
351
+ ) -> StateOperationHostData:
289
352
  return self.ops[host][op_hash]
290
353
 
291
- def set_op_data(self, host: "Host", op_hash: str, op_data):
354
+ def set_op_data_for_host(
355
+ self,
356
+ host: "Host",
357
+ op_hash: str,
358
+ op_data: StateOperationHostData,
359
+ ):
292
360
  self.ops[host][op_hash] = op_data
293
361
 
294
362
  def activate_host(self, host: "Host"):
@@ -330,13 +398,16 @@ class State:
330
398
 
331
399
  # No hosts left!
332
400
  if not active_hosts:
333
- raise NoMoreHostsError("No hosts remaining!")
401
+ raise PyinfraError("No hosts remaining!")
334
402
 
335
403
  if self.config.FAIL_PERCENT is not None:
336
404
  percent_failed = (1 - len(active_hosts) / activated_count) * 100
337
405
 
338
406
  if percent_failed > self.config.FAIL_PERCENT:
339
- raise NoMoreHostsError(
407
+ if self.should_raise_failed_hosts and self.should_raise_failed_hosts(self) is False:
408
+ return
409
+
410
+ raise PyinfraError(
340
411
  "Over {0}% of hosts failed ({1}%)".format(
341
412
  self.config.FAIL_PERCENT,
342
413
  int(round(percent_failed)),
@@ -353,16 +424,3 @@ class State:
353
424
  if not isinstance(limit_hosts, list):
354
425
  return True
355
426
  return host in limit_hosts
356
-
357
- def get_temp_filename(self, hash_key: Optional[str] = None, hash_filename: bool = True):
358
- """
359
- Generate a temporary filename for this deploy.
360
- """
361
-
362
- if not hash_key:
363
- hash_key = str(uuid4())
364
-
365
- if hash_filename:
366
- hash_key = sha1_hash(hash_key)
367
-
368
- return "{0}/pyinfra-{1}".format(self.config.TEMP_DIR, hash_key)
pyinfra/api/util.py CHANGED
@@ -1,21 +1,25 @@
1
+ from __future__ import annotations
2
+
1
3
  from functools import wraps
2
4
  from hashlib import sha1
3
- from inspect import getframeinfo, getfullargspec, stack
5
+ from inspect import getframeinfo, stack
4
6
  from io import BytesIO, StringIO
5
7
  from os import getcwd, path, stat
6
8
  from socket import error as socket_error, timeout as timeout_error
7
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
9
+ from typing import IO, TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, Union
8
10
 
9
11
  import click
10
12
  from jinja2 import Environment, FileSystemLoader, StrictUndefined
11
13
  from paramiko import SSHException
14
+ from typeguard import TypeCheckError, check_type
12
15
 
13
16
  import pyinfra
14
17
  from pyinfra import logger
15
18
 
16
19
  if TYPE_CHECKING:
17
20
  from pyinfra.api.host import Host
18
- from pyinfra.api.state import State
21
+ from pyinfra.api.state import State, StateOperationMeta
22
+ from pyinfra.connectors.util import CommandOutput
19
23
 
20
24
  # 64kb chunks
21
25
  BLOCKSIZE = 65536
@@ -24,7 +28,7 @@ BLOCKSIZE = 65536
24
28
  TEMPLATES: Dict[Any, Any] = {}
25
29
  FILE_SHAS: Dict[Any, Any] = {}
26
30
 
27
- PYINFRA_API_DIR = path.dirname(__file__)
31
+ PYINFRA_INSTALL_DIR = path.normpath(path.join(path.dirname(__file__), ".."))
28
32
 
29
33
 
30
34
  def get_file_path(state: "State", filename: str):
@@ -40,28 +44,6 @@ def get_file_path(state: "State", filename: str):
40
44
  return path.join(relative_to, filename)
41
45
 
42
46
 
43
- def get_args_kwargs_spec(func: Callable[..., Any]) -> Tuple[List, Dict]:
44
- args: List[Any] = []
45
- kwargs: Dict[Any, Any] = {}
46
-
47
- argspec = getfullargspec(func)
48
- if not argspec.args:
49
- return args, kwargs
50
-
51
- if argspec.defaults:
52
- kwargs = dict(
53
- zip(
54
- argspec.args[-len(argspec.defaults) :],
55
- argspec.defaults,
56
- ),
57
- )
58
- args = argspec.args[: -len(argspec.defaults)]
59
- else:
60
- args = argspec.args
61
-
62
- return args, kwargs
63
-
64
-
65
47
  def get_kwargs_str(kwargs: Dict[Any, Any]):
66
48
  if not kwargs:
67
49
  return ""
@@ -85,14 +67,14 @@ def memoize(func: Callable[..., Any]):
85
67
  @wraps(func)
86
68
  def wrapper(*args, **kwargs):
87
69
  key = "{0}{1}".format(args, kwargs)
88
- if key in wrapper.cache:
89
- return wrapper.cache[key]
70
+ if key in wrapper.cache: # type: ignore[attr-defined]
71
+ return wrapper.cache[key] # type: ignore[attr-defined]
90
72
 
91
73
  value = func(*args, **kwargs)
92
- wrapper.cache[key] = value
74
+ wrapper.cache[key] = value # type: ignore[attr-defined]
93
75
  return value
94
76
 
95
- wrapper.cache = {} # type: ignore
77
+ wrapper.cache = {} # type: ignore[attr-defined]
96
78
  return wrapper
97
79
 
98
80
 
@@ -147,7 +129,7 @@ def get_operation_order_from_stack(state: "State"):
147
129
  for stack_item in stack_items[i:]:
148
130
  frame = getframeinfo(stack_item[0])
149
131
 
150
- if frame.filename.startswith(PYINFRA_API_DIR):
132
+ if frame.filename.startswith(PYINFRA_INSTALL_DIR):
151
133
  continue
152
134
 
153
135
  line_numbers.append(frame.lineno)
@@ -184,7 +166,7 @@ def get_template(filename_or_io: str):
184
166
  return template
185
167
 
186
168
 
187
- def sha1_hash(string: str):
169
+ def sha1_hash(string: str) -> str:
188
170
  """
189
171
  Return the SHA1 of the input string.
190
172
  """
@@ -194,38 +176,30 @@ def sha1_hash(string: str):
194
176
  return hasher.hexdigest()
195
177
 
196
178
 
197
- def format_exception(e):
198
- return "{0}{1}".format(e.__class__.__name__, e.args)
179
+ def format_exception(e: Exception) -> str:
180
+ return f"{e.__class__.__name__}{e.args}"
199
181
 
200
182
 
201
- def print_host_combined_output(host: "Host", combined_output_lines):
202
- for type_, line in combined_output_lines:
203
- if type_ == "stderr":
204
- logger.error(
205
- "{0}{1}".format(
206
- host.print_prefix,
207
- click.style(line, "red"),
208
- ),
209
- )
183
+ def print_host_combined_output(host: "Host", output: "CommandOutput") -> None:
184
+ for line in output:
185
+ if line.buffer_name == "stderr":
186
+ logger.error(f"{host.print_prefix}{click.style(line.line, 'red')}")
210
187
  else:
211
- logger.error(
212
- "{0}{1}".format(
213
- host.print_prefix,
214
- line,
215
- ),
216
- )
188
+ logger.error(f"{host.print_prefix}{line.line}")
217
189
 
218
190
 
219
- def log_operation_start(op_meta: Dict, op_types: Optional[List] = None, prefix: str = "--> "):
191
+ def log_operation_start(
192
+ op_meta: "StateOperationMeta", op_types: Optional[List] = None, prefix: str = "--> "
193
+ ) -> None:
220
194
  op_types = op_types or []
221
- if op_meta["serial"]:
195
+ if op_meta.global_arguments["_serial"]:
222
196
  op_types.append("serial")
223
- if op_meta["run_once"]:
197
+ if op_meta.global_arguments["_run_once"]:
224
198
  op_types.append("run once")
225
199
 
226
200
  args = ""
227
- if op_meta["args"]:
228
- args = "({0})".format(", ".join(str(arg) for arg in op_meta["args"]))
201
+ if op_meta.args:
202
+ args = "({0})".format(", ".join(str(arg) for arg in op_meta.args))
229
203
 
230
204
  logger.info(
231
205
  "{0} {1} {2}".format(
@@ -236,7 +210,7 @@ def log_operation_start(op_meta: Dict, op_types: Optional[List] = None, prefix:
236
210
  ),
237
211
  "blue",
238
212
  ),
239
- click.style(", ".join(op_meta["names"]), bold=True),
213
+ click.style(", ".join(op_meta.names), bold=True),
240
214
  args,
241
215
  ),
242
216
  )
@@ -244,7 +218,7 @@ def log_operation_start(op_meta: Dict, op_types: Optional[List] = None, prefix:
244
218
 
245
219
  def log_error_or_warning(
246
220
  host: "Host", ignore_errors: bool, description: str = "", continue_on_error: bool = False
247
- ):
221
+ ) -> None:
248
222
  log_func = logger.error
249
223
  log_color = "red"
250
224
  log_text = "Error: " if description else "Error"
@@ -267,7 +241,7 @@ def log_error_or_warning(
267
241
  )
268
242
 
269
243
 
270
- def log_host_command_error(host: "Host", e, timeout: int = 0):
244
+ def log_host_command_error(host: "Host", e: Exception, timeout: int = 0) -> None:
271
245
  if isinstance(e, timeout_error):
272
246
  logger.error(
273
247
  "{0}{1}".format(
@@ -351,7 +325,11 @@ class get_file_io:
351
325
  will open and close filenames, and leave IO objects alone.
352
326
  """
353
327
 
354
- close = False
328
+ filename_or_io: Union[str, IO[Any]]
329
+ mode: str
330
+
331
+ _close: bool = False
332
+ _file_io: IO[Any]
355
333
 
356
334
  def __init__(self, filename_or_io, mode="rb"):
357
335
  if not (
@@ -378,25 +356,20 @@ class get_file_io:
378
356
  self.mode = mode
379
357
 
380
358
  def __enter__(self):
381
- # If we have a read attribute, just use the object as-is
382
- if hasattr(self.filename_or_io, "read"):
383
- file_io = self.filename_or_io
384
-
385
- # Otherwise, assume a filename and open it up
386
- else:
359
+ if isinstance(self.filename_or_io, str):
387
360
  file_io = open(self.filename_or_io, self.mode)
388
-
389
- # Attach to self for closing on __exit__
390
- self.file_io = file_io
391
- self.close = True
361
+ self._file_io = file_io
362
+ self._close = True
363
+ else:
364
+ file_io = self.filename_or_io
392
365
 
393
366
  # Ensure we're at the start of the file
394
367
  file_io.seek(0)
395
368
  return file_io
396
369
 
397
370
  def __exit__(self, type, value, traceback):
398
- if self.close:
399
- self.file_io.close()
371
+ if self._close:
372
+ self._file_io.close()
400
373
 
401
374
  @property
402
375
  def cache_key(self):
@@ -443,3 +416,15 @@ def get_path_permissions_mode(pathname: str):
443
416
 
444
417
  mode_octal = oct(stat(pathname).st_mode)
445
418
  return mode_octal[-3:]
419
+
420
+
421
+ def raise_if_bad_type(
422
+ value: Any,
423
+ type_: Type,
424
+ exception: type[Exception],
425
+ message_prefix: str,
426
+ ):
427
+ try:
428
+ check_type(value, type_)
429
+ except TypeCheckError as e:
430
+ raise exception(f"{message_prefix}: {e}")