pyinfra 3.0.dev0__py2.py3-none-any.whl → 3.0.1__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 (148) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +115 -97
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +5 -3
  5. pyinfra/api/config.py +139 -39
  6. pyinfra/api/connectors.py +5 -2
  7. pyinfra/api/deploy.py +19 -19
  8. pyinfra/api/exceptions.py +35 -4
  9. pyinfra/api/facts.py +62 -86
  10. pyinfra/api/host.py +102 -15
  11. pyinfra/api/inventory.py +4 -0
  12. pyinfra/api/operation.py +184 -118
  13. pyinfra/api/operations.py +66 -113
  14. pyinfra/api/state.py +53 -34
  15. pyinfra/api/util.py +64 -33
  16. pyinfra/connectors/base.py +65 -20
  17. pyinfra/connectors/chroot.py +15 -13
  18. pyinfra/connectors/docker.py +62 -72
  19. pyinfra/connectors/dockerssh.py +20 -19
  20. pyinfra/connectors/local.py +32 -22
  21. pyinfra/connectors/ssh.py +162 -86
  22. pyinfra/connectors/sshuserclient/client.py +1 -1
  23. pyinfra/connectors/terraform.py +57 -39
  24. pyinfra/connectors/util.py +26 -27
  25. pyinfra/connectors/vagrant.py +27 -26
  26. pyinfra/context.py +1 -0
  27. pyinfra/facts/apk.py +7 -2
  28. pyinfra/facts/apt.py +15 -7
  29. pyinfra/facts/brew.py +28 -13
  30. pyinfra/facts/bsdinit.py +9 -6
  31. pyinfra/facts/cargo.py +6 -3
  32. pyinfra/facts/choco.py +8 -4
  33. pyinfra/facts/deb.py +21 -9
  34. pyinfra/facts/dnf.py +11 -6
  35. pyinfra/facts/docker.py +30 -5
  36. pyinfra/facts/files.py +49 -33
  37. pyinfra/facts/gem.py +7 -2
  38. pyinfra/facts/git.py +14 -21
  39. pyinfra/facts/gpg.py +4 -1
  40. pyinfra/facts/hardware.py +186 -138
  41. pyinfra/facts/launchd.py +7 -2
  42. pyinfra/facts/lxd.py +8 -2
  43. pyinfra/facts/mysql.py +19 -12
  44. pyinfra/facts/npm.py +3 -1
  45. pyinfra/facts/openrc.py +8 -2
  46. pyinfra/facts/pacman.py +13 -5
  47. pyinfra/facts/pip.py +2 -0
  48. pyinfra/facts/pkg.py +5 -1
  49. pyinfra/facts/pkgin.py +7 -2
  50. pyinfra/facts/postgres.py +170 -0
  51. pyinfra/facts/postgresql.py +5 -162
  52. pyinfra/facts/rpm.py +21 -15
  53. pyinfra/facts/runit.py +70 -0
  54. pyinfra/facts/selinux.py +12 -4
  55. pyinfra/facts/server.py +240 -82
  56. pyinfra/facts/snap.py +8 -2
  57. pyinfra/facts/systemd.py +37 -13
  58. pyinfra/facts/sysvinit.py +7 -4
  59. pyinfra/facts/upstart.py +7 -2
  60. pyinfra/facts/util/packaging.py +3 -2
  61. pyinfra/facts/vzctl.py +8 -4
  62. pyinfra/facts/xbps.py +7 -2
  63. pyinfra/facts/yum.py +10 -5
  64. pyinfra/facts/zypper.py +9 -4
  65. pyinfra/operations/apk.py +5 -3
  66. pyinfra/operations/apt.py +28 -25
  67. pyinfra/operations/brew.py +60 -29
  68. pyinfra/operations/bsdinit.py +6 -4
  69. pyinfra/operations/cargo.py +3 -1
  70. pyinfra/operations/choco.py +3 -1
  71. pyinfra/operations/dnf.py +16 -20
  72. pyinfra/operations/docker.py +339 -0
  73. pyinfra/operations/files.py +187 -168
  74. pyinfra/operations/gem.py +3 -1
  75. pyinfra/operations/git.py +23 -25
  76. pyinfra/operations/iptables.py +33 -25
  77. pyinfra/operations/launchd.py +5 -6
  78. pyinfra/operations/lxd.py +7 -4
  79. pyinfra/operations/mysql.py +59 -55
  80. pyinfra/operations/npm.py +8 -1
  81. pyinfra/operations/openrc.py +5 -3
  82. pyinfra/operations/pacman.py +6 -7
  83. pyinfra/operations/pip.py +19 -12
  84. pyinfra/operations/pkg.py +3 -1
  85. pyinfra/operations/pkgin.py +5 -3
  86. pyinfra/operations/postgres.py +349 -0
  87. pyinfra/operations/postgresql.py +18 -335
  88. pyinfra/operations/puppet.py +3 -1
  89. pyinfra/operations/python.py +8 -19
  90. pyinfra/operations/runit.py +182 -0
  91. pyinfra/operations/selinux.py +47 -29
  92. pyinfra/operations/server.py +138 -67
  93. pyinfra/operations/snap.py +3 -1
  94. pyinfra/operations/ssh.py +18 -16
  95. pyinfra/operations/systemd.py +18 -12
  96. pyinfra/operations/sysvinit.py +7 -5
  97. pyinfra/operations/upstart.py +7 -5
  98. pyinfra/operations/util/__init__.py +12 -0
  99. pyinfra/operations/util/docker.py +177 -0
  100. pyinfra/operations/util/files.py +24 -16
  101. pyinfra/operations/util/packaging.py +54 -38
  102. pyinfra/operations/util/service.py +39 -47
  103. pyinfra/operations/vzctl.py +12 -10
  104. pyinfra/operations/xbps.py +5 -3
  105. pyinfra/operations/yum.py +15 -19
  106. pyinfra/operations/zypper.py +9 -10
  107. pyinfra/version.py +5 -2
  108. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/METADATA +51 -58
  109. pyinfra-3.0.1.dist-info/RECORD +168 -0
  110. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/WHEEL +1 -1
  111. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/entry_points.txt +0 -3
  112. pyinfra_cli/__main__.py +4 -3
  113. pyinfra_cli/commands.py +3 -2
  114. pyinfra_cli/exceptions.py +75 -43
  115. pyinfra_cli/inventory.py +52 -31
  116. pyinfra_cli/log.py +10 -2
  117. pyinfra_cli/main.py +88 -65
  118. pyinfra_cli/prints.py +37 -109
  119. pyinfra_cli/util.py +15 -10
  120. tests/test_api/test_api.py +2 -0
  121. tests/test_api/test_api_arguments.py +9 -9
  122. tests/test_api/test_api_deploys.py +15 -19
  123. tests/test_api/test_api_facts.py +4 -5
  124. tests/test_api/test_api_operations.py +18 -20
  125. tests/test_api/test_api_util.py +41 -2
  126. tests/test_cli/test_cli.py +14 -50
  127. tests/test_cli/test_cli_deploy.py +10 -12
  128. tests/test_cli/test_cli_exceptions.py +50 -19
  129. tests/test_cli/test_cli_inventory.py +66 -0
  130. tests/test_cli/util.py +1 -1
  131. tests/test_connectors/test_dockerssh.py +11 -8
  132. tests/test_connectors/test_ssh.py +88 -23
  133. tests/test_connectors/test_sshuserclient.py +1 -1
  134. tests/test_connectors/test_terraform.py +11 -8
  135. tests/test_connectors/test_vagrant.py +6 -6
  136. pyinfra/connectors/ansible.py +0 -175
  137. pyinfra/connectors/mech.py +0 -189
  138. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  139. pyinfra/connectors/winrm.py +0 -312
  140. pyinfra/facts/windows.py +0 -366
  141. pyinfra/facts/windows_files.py +0 -90
  142. pyinfra/operations/windows.py +0 -59
  143. pyinfra/operations/windows_files.py +0 -538
  144. pyinfra-3.0.dev0.dist-info/RECORD +0 -170
  145. tests/test_connectors/test_ansible.py +0 -64
  146. tests/test_connectors/test_mech.py +0 -126
  147. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/LICENSE.md +0 -0
  148. {pyinfra-3.0.dev0.dist-info → pyinfra-3.0.1.dist-info}/top_level.txt +0 -0
pyinfra/api/__init__.py CHANGED
@@ -11,6 +11,9 @@ from .config import Config # noqa: F401 # pragma: no cover
11
11
  from .deploy import deploy # noqa: F401 # pragma: no cover
12
12
  from .exceptions import DeployError # noqa: F401 # pragma: no cover
13
13
  from .exceptions import ( # noqa: F401
14
+ FactError,
15
+ FactTypeError,
16
+ FactValueError,
14
17
  InventoryError,
15
18
  OperationError,
16
19
  OperationTypeError,
pyinfra/api/arguments.py CHANGED
@@ -1,31 +1,42 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, Optional, Tuple, TypeVar, Union
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ Callable,
7
+ Generic,
8
+ Iterable,
9
+ List,
10
+ Mapping,
11
+ Optional,
12
+ Type,
13
+ TypeVar,
14
+ Union,
15
+ cast,
16
+ get_type_hints,
17
+ )
4
18
 
5
19
  from typing_extensions import TypedDict
6
20
 
7
21
  from pyinfra import context
22
+ from pyinfra.api.exceptions import ArgumentTypeError
8
23
  from pyinfra.api.state import State
9
-
10
- from .util import memoize
24
+ from pyinfra.api.util import raise_if_bad_type
11
25
 
12
26
  if TYPE_CHECKING:
13
27
  from pyinfra.api.config import Config
14
28
  from pyinfra.api.host import Host
15
29
 
16
-
17
30
  T = TypeVar("T")
18
-
19
-
20
31
  default_sentinel = object()
21
32
 
22
33
 
23
- class ArgumentMeta:
34
+ class ArgumentMeta(Generic[T]):
24
35
  description: str
25
36
  default: Callable[["Config"], T]
26
- handler: Callable[["Config", T], T]
37
+ handler: Optional[Callable[["Config", T], T]]
27
38
 
28
- def __init__(self, description, default, handler=default_sentinel) -> None:
39
+ def __init__(self, description, default, handler=None) -> None:
29
40
  self.description = description
30
41
  self.default = default
31
42
  self.handler = handler
@@ -36,12 +47,14 @@ class ArgumentMeta:
36
47
  # API to read/write external systems.
37
48
 
38
49
 
50
+ # Note: ConnectorArguments is specifically not total as it's used to type many
51
+ # functions via Unpack and we don't want to specify every kwarg.
39
52
  class ConnectorArguments(TypedDict, total=False):
40
53
  # Auth arguments
41
54
  _sudo: bool
42
55
  _sudo_user: str
43
56
  _use_sudo_login: bool
44
- _use_sudo_password: bool
57
+ _sudo_password: str
45
58
  _preserve_sudo_env: bool
46
59
  _su_user: str
47
60
  _use_su_login: bool
@@ -59,7 +72,7 @@ class ConnectorArguments(TypedDict, total=False):
59
72
  _success_exit_codes: Iterable[int]
60
73
  _timeout: int
61
74
  _get_pty: bool
62
- _stdin: Union[str, list, tuple]
75
+ _stdin: Union[str, Iterable[str]]
63
76
 
64
77
 
65
78
  def generate_env(config: "Config", value: dict) -> dict:
@@ -70,82 +83,82 @@ def generate_env(config: "Config", value: dict) -> dict:
70
83
 
71
84
  auth_argument_meta: dict[str, ArgumentMeta] = {
72
85
  "_sudo": ArgumentMeta(
73
- "Execute/apply any changes with ``sudo``.",
86
+ "Execute/apply any changes with sudo.",
74
87
  default=lambda config: config.SUDO,
75
88
  ),
76
89
  "_sudo_user": ArgumentMeta(
77
- "Execute/apply any changes with ``sudo`` as a non-root user.",
90
+ "Execute/apply any changes with sudo as a non-root user.",
78
91
  default=lambda config: config.SUDO_USER,
79
92
  ),
80
93
  "_use_sudo_login": ArgumentMeta(
81
- "Execute ``sudo`` with a login shell.",
94
+ "Execute sudo with a login shell.",
82
95
  default=lambda config: config.USE_SUDO_LOGIN,
83
96
  ),
84
- "_use_sudo_password": ArgumentMeta(
85
- "Whether to use a password with ``sudo`` (will ask).",
86
- default=lambda config: config.USE_SUDO_PASSWORD,
97
+ "_sudo_password": ArgumentMeta(
98
+ "Password to sudo with. If needed and not specified pyinfra will prompt for it.",
99
+ default=lambda config: config.SUDO_PASSWORD,
87
100
  ),
88
101
  "_preserve_sudo_env": ArgumentMeta(
89
- "Preserve the shell environment when using ``sudo``.",
102
+ "Preserve the shell environment of the connecting user when using sudo.",
90
103
  default=lambda config: config.PRESERVE_SUDO_ENV,
91
104
  ),
92
105
  "_su_user": ArgumentMeta(
93
- "Execute/apply any changes with this user using ``su``.",
106
+ "Execute/apply any changes with this user using su.",
94
107
  default=lambda config: config.SU_USER,
95
108
  ),
96
109
  "_use_su_login": ArgumentMeta(
97
- "Execute ``su`` with a login shell.",
110
+ "Execute su with a login shell.",
98
111
  default=lambda config: config.USE_SU_LOGIN,
99
112
  ),
100
113
  "_preserve_su_env": ArgumentMeta(
101
- "Preserve the shell environment when using ``su``.",
114
+ "Preserve the shell environment of the connecting user when using su.",
102
115
  default=lambda config: config.PRESERVE_SU_ENV,
103
116
  ),
104
117
  "_su_shell": ArgumentMeta(
105
- "Use this shell (instead of user login shell) when using ``su``). "
106
- "Only available under Linux, for use when using `su` with a user that "
107
- "has nologin/similar as their login shell.",
118
+ "Use this shell (instead of user login shell) when using ``_su``). "
119
+ + "Only available under Linux, for use when using `su` with a user that "
120
+ + "has nologin/similar as their login shell.",
108
121
  default=lambda config: config.SU_SHELL,
109
122
  ),
110
123
  "_doas": ArgumentMeta(
111
- "Execute/apply any changes with ``doas``.",
124
+ "Execute/apply any changes with doas.",
112
125
  default=lambda config: config.DOAS,
113
126
  ),
114
127
  "_doas_user": ArgumentMeta(
115
- "Execute/apply any changes with ``doas`` as a non-root user.",
128
+ "Execute/apply any changes with doas as a non-root user.",
116
129
  default=lambda config: config.DOAS_USER,
117
130
  ),
118
131
  }
119
132
 
120
133
  shell_argument_meta: dict[str, ArgumentMeta] = {
121
134
  "_shell_executable": ArgumentMeta(
122
- "The shell to use. Defaults to ``sh`` (Unix) or ``cmd`` (Windows).",
135
+ "The shell executable to use for executing commands.",
123
136
  default=lambda config: config.SHELL,
124
137
  ),
125
138
  "_chdir": ArgumentMeta(
126
139
  "Directory to switch to before executing the command.",
127
- default=lambda config: "",
140
+ default=lambda _: "",
128
141
  ),
129
142
  "_env": ArgumentMeta(
130
143
  "Dictionary of environment variables to set.",
131
- default=lambda config: {},
144
+ default=lambda _: {},
132
145
  handler=generate_env,
133
146
  ),
134
147
  "_success_exit_codes": ArgumentMeta(
135
148
  "List of exit codes to consider a success.",
136
- default=lambda config: [0],
149
+ default=lambda _: [0],
137
150
  ),
138
151
  "_timeout": ArgumentMeta(
139
152
  "Timeout for *each* command executed during the operation.",
140
- default=lambda config: None,
153
+ default=lambda _: None,
141
154
  ),
142
155
  "_get_pty": ArgumentMeta(
143
156
  "Whether to get a pseudoTTY when executing any commands.",
144
- default=lambda config: False,
157
+ default=lambda _: False,
145
158
  ),
146
159
  "_stdin": ArgumentMeta(
147
160
  "String or buffer to send to the stdin of any commands.",
148
- default=lambda config: None,
161
+ default=lambda _: None,
149
162
  ),
150
163
  }
151
164
 
@@ -154,21 +167,18 @@ shell_argument_meta: dict[str, ArgumentMeta] = {
154
167
  # These provide/extend additional operation metadata
155
168
 
156
169
 
157
- class MetaArguments(TypedDict, total=False):
170
+ class MetaArguments(TypedDict):
158
171
  name: str
159
172
  _ignore_errors: bool
160
173
  _continue_on_error: bool
161
- _precondition: str
162
- _postcondition: str
163
- _on_success: Callable[[State, "Host", str], None]
164
- _on_error: Callable[[State, "Host", str], None]
174
+ _if: Union[List[Callable[[], bool]], Callable[[], bool], None]
165
175
 
166
176
 
167
177
  meta_argument_meta: dict[str, ArgumentMeta] = {
168
178
  # NOTE: name is the only non-_-prefixed argument
169
179
  "name": ArgumentMeta(
170
180
  "Name of the operation.",
171
- default=lambda config: None,
181
+ default=lambda _: None,
172
182
  ),
173
183
  "_ignore_errors": ArgumentMeta(
174
184
  "Ignore errors when executing the operation.",
@@ -179,24 +189,11 @@ meta_argument_meta: dict[str, ArgumentMeta] = {
179
189
  "Continue executing operation commands after error. "
180
190
  "Only applies when ``_ignore_errors`` is true."
181
191
  ),
182
- default=lambda config: False,
183
- ),
184
- "_precondition": ArgumentMeta(
185
- "Command to execute & check before the operation commands begin.",
186
- default=lambda config: None,
192
+ default=lambda _: False,
187
193
  ),
188
- "_postcondition": ArgumentMeta(
189
- "Command to execute & check after the operation commands complete.",
190
- default=lambda config: None,
191
- ),
192
- # Lambda on the next two are to workaround a circular import
193
- "_on_success": ArgumentMeta(
194
- "Callback function to execute on success.",
195
- default=lambda config: None,
196
- ),
197
- "_on_error": ArgumentMeta(
198
- "Callback function to execute on error.",
199
- default=lambda config: None,
194
+ "_if": ArgumentMeta(
195
+ "Only run this operation if these functions return True",
196
+ default=lambda _: [],
200
197
  ),
201
198
  }
202
199
 
@@ -206,7 +203,7 @@ meta_argument_meta: dict[str, ArgumentMeta] = {
206
203
  # over every target host for the same operation.
207
204
 
208
205
 
209
- class ExecutionArguments(TypedDict, total=False):
206
+ class ExecutionArguments(TypedDict):
210
207
  _parallel: int
211
208
  _run_once: bool
212
209
  _serial: bool
@@ -219,11 +216,11 @@ execution_argument_meta: dict[str, ArgumentMeta] = {
219
216
  ),
220
217
  "_run_once": ArgumentMeta(
221
218
  "Only execute this operation once, on the first host to see it.",
222
- default=lambda config: False,
219
+ default=lambda _: False,
223
220
  ),
224
221
  "_serial": ArgumentMeta(
225
222
  "Run this operation host by host, rather than in parallel.",
226
- default=lambda config: False,
223
+ default=lambda _: False,
227
224
  ),
228
225
  }
229
226
 
@@ -232,6 +229,11 @@ class AllArguments(ConnectorArguments, MetaArguments, ExecutionArguments):
232
229
  pass
233
230
 
234
231
 
232
+ def all_global_arguments() -> List[tuple[str, Type]]:
233
+ """Return all global arguments and their types."""
234
+ return list(get_type_hints(AllArguments).items())
235
+
236
+
235
237
  all_argument_meta: dict[str, ArgumentMeta] = {
236
238
  **auth_argument_meta,
237
239
  **shell_argument_meta,
@@ -239,29 +241,47 @@ all_argument_meta: dict[str, ArgumentMeta] = {
239
241
  **execution_argument_meta,
240
242
  }
241
243
 
242
-
243
- OPERATION_KWARG_DOC: list[tuple[str, Optional[str], type, dict[str, ArgumentMeta]]] = [
244
- ("Privilege & user escalation", None, ConnectorArguments, auth_argument_meta),
245
- ("Shell control & features", None, ConnectorArguments, shell_argument_meta),
246
- ("Operation meta & callbacks", "Not available in facts.", MetaArguments, meta_argument_meta),
247
- (
248
- "Execution strategy",
249
- "Not available in facts, value must be the same for all hosts.",
250
- ExecutionArguments,
251
- execution_argument_meta,
244
+ EXECUTION_KWARG_KEYS = list(ExecutionArguments.__annotations__.keys())
245
+ CONNECTOR_ARGUMENT_KEYS = list(ConnectorArguments.__annotations__.keys())
246
+
247
+ __argument_docs__ = {
248
+ "Privilege & user escalation": (
249
+ auth_argument_meta,
250
+ """
251
+ .. code:: python
252
+
253
+ # Execute a command with sudo
254
+ server.user(
255
+ name="Create pyinfra user using sudo",
256
+ user="pyinfra",
257
+ _sudo=True,
258
+ )
259
+
260
+ # Execute a command with a specific sudo password
261
+ server.user(
262
+ name="Create pyinfra user using sudo",
263
+ user="pyinfra",
264
+ _sudo=True,
265
+ _sudo_password="my-secret-password",
266
+ )
267
+ """,
252
268
  ),
253
- ]
254
-
255
-
256
- # Called ONCE to dedupe args that must be same for all calls of an op
257
- @memoize
258
- def get_execution_kwarg_keys() -> list[str]:
259
- return list(ExecutionArguments.__annotations__.keys())
260
-
261
-
262
- @memoize
263
- def get_connector_argument_keys() -> list[str]:
264
- return list(ConnectorArguments.__annotations__.keys())
269
+ "Shell control & features": (
270
+ shell_argument_meta,
271
+ """
272
+ .. code:: python
273
+
274
+ # Execute from a specific directory
275
+ server.shell(
276
+ name="Bootstrap nginx params",
277
+ commands=["openssl dhparam -out dhparam.pem 4096"],
278
+ _chdir="/etc/ssl/certs",
279
+ )
280
+ """,
281
+ ),
282
+ "Operation meta & callbacks": (meta_argument_meta, ""),
283
+ "Execution strategy": (execution_argument_meta, ""),
284
+ }
265
285
 
266
286
 
267
287
  def pop_global_arguments(
@@ -269,7 +289,7 @@ def pop_global_arguments(
269
289
  state: Optional["State"] = None,
270
290
  host: Optional["Host"] = None,
271
291
  keys_to_check=None,
272
- ) -> Tuple[AllArguments, list[str]]:
292
+ ) -> tuple[AllArguments, list[str]]:
273
293
  """
274
294
  Pop and return operation global keyword arguments, in preferred order:
275
295
 
@@ -277,16 +297,6 @@ def pop_global_arguments(
277
297
  + From any current @deploy context (deploy kwargs)
278
298
  + From the host data variables
279
299
  + From the config variables
280
-
281
- Note this function is only called directly in the @operation & @deploy decorator
282
- wrappers which the user should pass global arguments prefixed "_". This is to
283
- avoid any clashes with operation and deploy functions both internal and third
284
- party.
285
-
286
- This is a bit strange because internally pyinfra uses non-_-prefixed arguments,
287
- and this function is responsible for the translation between the two.
288
-
289
- TODO: is this weird-ness acceptable? Is it worth updating internal use to _prefix?
290
300
  """
291
301
 
292
302
  state = state or context.state
@@ -296,12 +306,12 @@ def pop_global_arguments(
296
306
  if context.ctx_config.isset():
297
307
  config = context.config
298
308
 
299
- meta_kwargs = host.current_deploy_kwargs or {}
309
+ meta_kwargs: dict[str, Any] = host.current_deploy_kwargs or {} # type: ignore[assignment]
300
310
 
301
- arguments: AllArguments = {}
311
+ arguments: dict[str, Any] = {}
302
312
  found_keys: list[str] = []
303
313
 
304
- for key, type_ in AllArguments.__annotations__.items():
314
+ for key, type_ in all_global_arguments():
305
315
  if keys_to_check and key not in keys_to_check:
306
316
  continue
307
317
 
@@ -319,9 +329,17 @@ def pop_global_arguments(
319
329
  else:
320
330
  value = meta_kwargs.get(key, default)
321
331
 
322
- if handler is not default_sentinel:
332
+ if handler:
323
333
  value = handler(config, value)
324
334
 
335
+ if value != default:
336
+ raise_if_bad_type(
337
+ value,
338
+ type_,
339
+ ArgumentTypeError,
340
+ f"Invalid argument `{key}`:",
341
+ )
342
+
325
343
  # TODO: why is type failing here?
326
344
  arguments[key] = value # type: ignore
327
- return arguments, found_keys
345
+ return cast(AllArguments, arguments), found_keys
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Callable,
6
+ Generator,
7
+ Generic,
8
+ Iterable,
9
+ List,
10
+ Mapping,
11
+ Optional,
12
+ Union,
13
+ )
14
+
15
+ from typing_extensions import ParamSpec, Protocol
16
+
17
+ if TYPE_CHECKING:
18
+ from pyinfra.api.operation import OperationMeta
19
+
20
+ P = ParamSpec("P")
21
+
22
+
23
+ # Unfortunately we have to re-type out all of the global arguments here because
24
+ # Python typing doesn't (yet) support merging kwargs. This acts as the operation
25
+ # decorator output function which merges actual operation args/kwargs in paramspec
26
+ # with the global arguments available in all operations.
27
+ # The nature of "global arguments" is somewhat opposed to static typing as it's
28
+ # indirect and somewhat magic, but we are where we are.
29
+ class PyinfraOperation(Generic[P], Protocol):
30
+ _inner: Callable[P, Generator]
31
+
32
+ def __call__(
33
+ self,
34
+ #
35
+ # op args
36
+ # needs to be first
37
+ #
38
+ *args: P.args,
39
+ #
40
+ # ConnectorArguments
41
+ #
42
+ # Auth
43
+ _sudo: bool = False,
44
+ _sudo_user: Optional[str] = None,
45
+ _use_sudo_login: bool = False,
46
+ _sudo_password: Optional[str] = None,
47
+ _preserve_sudo_env: bool = False,
48
+ _su_user: Optional[str] = None,
49
+ _use_su_login: bool = False,
50
+ _preserve_su_env: bool = False,
51
+ _su_shell: Optional[str] = None,
52
+ _doas: bool = False,
53
+ _doas_user: Optional[str] = None,
54
+ # Shell arguments
55
+ _shell_executable: Optional[str] = None,
56
+ _chdir: Optional[str] = None,
57
+ _env: Optional[Mapping[str, str]] = None,
58
+ # Connector control
59
+ _success_exit_codes: Iterable[int] = (0,),
60
+ _timeout: Optional[int] = None,
61
+ _get_pty: bool = False,
62
+ _stdin: Union[None, str, list[str], tuple[str, ...]] = None,
63
+ #
64
+ # MetaArguments
65
+ #
66
+ name: Optional[str] = None,
67
+ _ignore_errors: bool = False,
68
+ _continue_on_error: bool = False,
69
+ _if: Union[List[Callable[[], bool]], Callable[[], bool], None] = None,
70
+ #
71
+ # ExecutionArguments
72
+ #
73
+ _parallel: Optional[int] = None,
74
+ _run_once: bool = False,
75
+ _serial: bool = False,
76
+ #
77
+ # op kwargs
78
+ #
79
+ **kwargs: P.kwargs,
80
+ ) -> "OperationMeta": ...
pyinfra/api/command.py CHANGED
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import shlex
2
4
  from inspect import getfullargspec
3
5
  from string import Formatter
4
- from typing import TYPE_CHECKING, Callable, Union
6
+ from typing import IO, TYPE_CHECKING, Callable, Union
5
7
 
6
8
  import gevent
7
9
  from typing_extensions import Unpack
@@ -143,7 +145,7 @@ class StringCommand(PyinfraCommand):
143
145
  class FileUploadCommand(PyinfraCommand):
144
146
  def __init__(
145
147
  self,
146
- src: str,
148
+ src: str | IO,
147
149
  dest: str,
148
150
  remote_temp_filename=None,
149
151
  **kwargs: Unpack[ConnectorArguments],
@@ -173,7 +175,7 @@ class FileDownloadCommand(PyinfraCommand):
173
175
  def __init__(
174
176
  self,
175
177
  src: str,
176
- dest: str,
178
+ dest: str | IO,
177
179
  remote_temp_filename=None,
178
180
  **kwargs: Unpack[ConnectorArguments],
179
181
  ):