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/__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,261 +1,289 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import (
2
4
  TYPE_CHECKING,
3
5
  Any,
4
6
  Callable,
5
- Dict,
7
+ Generic,
6
8
  Iterable,
7
9
  List,
8
10
  Mapping,
9
11
  Optional,
10
- Set,
11
- Tuple,
12
+ TypeVar,
12
13
  Union,
14
+ cast,
15
+ get_type_hints,
13
16
  )
14
17
 
15
- import pyinfra
16
- from pyinfra import context, logger
17
- from pyinfra.api.state import State
18
+ from typing_extensions import TypedDict
18
19
 
19
- from .util import get_call_location, memoize
20
+ from pyinfra import context
21
+ from pyinfra.api.exceptions import ArgumentTypeError
22
+ from pyinfra.api.state import State
23
+ from pyinfra.api.util import raise_if_bad_type
20
24
 
21
25
  if TYPE_CHECKING:
22
26
  from pyinfra.api.config import Config
23
27
  from pyinfra.api.host import Host
24
28
 
25
- auth_kwargs = {
26
- "_sudo": {
27
- "description": "Execute/apply any changes with ``sudo``.",
28
- "default": lambda config: config.SUDO,
29
- "type": bool,
30
- },
31
- "_sudo_user": {
32
- "description": "Execute/apply any changes with ``sudo`` as a non-root user.",
33
- "default": lambda config: config.SUDO_USER,
34
- "type": str,
35
- },
36
- "_use_sudo_login": {
37
- "description": "Execute ``sudo`` with a login shell.",
38
- "default": lambda config: config.USE_SUDO_LOGIN,
39
- "type": bool,
40
- },
41
- "_use_sudo_password": {
42
- "description": "Whether to use a password with ``sudo`` (will ask).",
43
- "default": lambda config: config.USE_SUDO_PASSWORD,
44
- "type": bool,
45
- },
46
- "_preserve_sudo_env": {
47
- "description": "Preserve the shell environment when using ``sudo``.",
48
- "default": lambda config: config.PRESERVE_SUDO_ENV,
49
- "type": bool,
50
- },
51
- "_su_user": {
52
- "description": "Execute/apply any changes with this user using ``su``.",
53
- "default": lambda config: config.SU_USER,
54
- "type": str,
55
- },
56
- "_use_su_login": {
57
- "description": "Execute ``su`` with a login shell.",
58
- "default": lambda config: config.USE_SU_LOGIN,
59
- "type": bool,
60
- },
61
- "_preserve_su_env": {
62
- "description": "Preserve the shell environment when using ``su``.",
63
- "default": lambda config: config.PRESERVE_SU_ENV,
64
- "type": bool,
65
- },
66
- "_su_shell": {
67
- "description": (
68
- "Use this shell (instead of user login shell) when using ``su``). "
69
- "Only available under Linux, for use when using `su` with a user that "
70
- "has nologin/similar as their login shell."
71
- ),
72
- "default": lambda config: config.SU_SHELL,
73
- "type": str,
74
- },
75
- "_doas": {
76
- "description": "Execute/apply any changes with ``doas``.",
77
- "default": lambda config: config.DOAS,
78
- "type": bool,
79
- },
80
- "_doas_user": {
81
- "description": "Execute/apply any changes with ``doas`` as a non-root user.",
82
- "default": lambda config: config.DOAS_USER,
83
- "type": str,
84
- },
85
- }
29
+ T = TypeVar("T")
30
+ default_sentinel = object()
86
31
 
87
32
 
88
- def generate_env(config: "Config", value):
89
- env = config.ENV.copy()
33
+ class ArgumentMeta(Generic[T]):
34
+ description: str
35
+ default: Callable[["Config"], T]
36
+ handler: Optional[Callable[["Config", T], T]]
37
+
38
+ def __init__(self, description, default, handler=None) -> None:
39
+ self.description = description
40
+ self.default = default
41
+ self.handler = handler
42
+
43
+
44
+ # Connector arguments
45
+ # These are arguments passed to the various connectors that provide the underlying
46
+ # API to read/write external systems.
90
47
 
91
- # TODO: this is to protect against host.data.env being a string or similar,
92
- # the introduction of using host.data.X for operation kwargs combined with
93
- # `env` being a commonly defined data variable causes issues.
94
- # The real fix here is the prefixed `_env` argument.
95
- if value and isinstance(value, dict):
96
- env.update(value)
97
48
 
49
+ # Note: ConnectorArguments is specifically not total as it's used to type many
50
+ # functions via Unpack and we don't want to specify every kwarg.
51
+ class ConnectorArguments(TypedDict, total=False):
52
+ # Auth arguments
53
+ _sudo: bool
54
+ _sudo_user: str
55
+ _use_sudo_login: bool
56
+ _sudo_password: str
57
+ _preserve_sudo_env: bool
58
+ _su_user: str
59
+ _use_su_login: bool
60
+ _preserve_su_env: bool
61
+ _su_shell: str
62
+ _doas: bool
63
+ _doas_user: str
64
+
65
+ # Shell arguments
66
+ _shell_executable: str
67
+ _chdir: str
68
+ _env: Mapping[str, str]
69
+
70
+ # Connector control (outside of command generation)
71
+ _success_exit_codes: Iterable[int]
72
+ _timeout: int
73
+ _get_pty: bool
74
+ _stdin: Union[str, Iterable[str]]
75
+
76
+
77
+ def generate_env(config: "Config", value: dict) -> dict:
78
+ env = config.ENV.copy()
79
+ env.update(value)
98
80
  return env
99
81
 
100
82
 
101
- shell_kwargs = {
102
- "_shell_executable": {
103
- "description": "The shell to use. Defaults to ``sh`` (Unix) or ``cmd`` (Windows).",
104
- "default": lambda config: config.SHELL,
105
- "type": str,
106
- },
107
- "_chdir": {
108
- "description": "Directory to switch to before executing the command.",
109
- "type": str,
110
- },
111
- "_env": {
112
- "description": "Dictionary of environment variables to set.",
113
- "handler": generate_env,
114
- "type": Mapping[str, str],
115
- },
116
- "_success_exit_codes": {
117
- "description": "List of exit codes to consider a success.",
118
- "default": lambda config: [0],
119
- "type": Iterable[int],
120
- },
121
- "_timeout": {
122
- "description": "Timeout for *each* command executed during the operation.",
123
- "type": int,
124
- },
125
- "_get_pty": {
126
- "description": "Whether to get a pseudoTTY when executing any commands.",
127
- "type": bool,
128
- },
129
- "_stdin": {
130
- "description": "String or buffer to send to the stdin of any commands.",
131
- "type": Union[str, list, tuple],
132
- },
83
+ auth_argument_meta: dict[str, ArgumentMeta] = {
84
+ "_sudo": ArgumentMeta(
85
+ "Execute/apply any changes with sudo.",
86
+ default=lambda config: config.SUDO,
87
+ ),
88
+ "_sudo_user": ArgumentMeta(
89
+ "Execute/apply any changes with sudo as a non-root user.",
90
+ default=lambda config: config.SUDO_USER,
91
+ ),
92
+ "_use_sudo_login": ArgumentMeta(
93
+ "Execute sudo with a login shell.",
94
+ default=lambda config: config.USE_SUDO_LOGIN,
95
+ ),
96
+ "_sudo_password": ArgumentMeta(
97
+ "Password to sudo with. If needed and not specified pyinfra will prompt for it.",
98
+ default=lambda config: config.SUDO_PASSWORD,
99
+ ),
100
+ "_preserve_sudo_env": ArgumentMeta(
101
+ "Preserve the shell environment of the connecting user when using sudo.",
102
+ default=lambda config: config.PRESERVE_SUDO_ENV,
103
+ ),
104
+ "_su_user": ArgumentMeta(
105
+ "Execute/apply any changes with this user using su.",
106
+ default=lambda config: config.SU_USER,
107
+ ),
108
+ "_use_su_login": ArgumentMeta(
109
+ "Execute su with a login shell.",
110
+ default=lambda config: config.USE_SU_LOGIN,
111
+ ),
112
+ "_preserve_su_env": ArgumentMeta(
113
+ "Preserve the shell environment of the connecting user when using su.",
114
+ default=lambda config: config.PRESERVE_SU_ENV,
115
+ ),
116
+ "_su_shell": ArgumentMeta(
117
+ "Use this shell (instead of user login shell) when using ``_su``). "
118
+ + "Only available under Linux, for use when using `su` with a user that "
119
+ + "has nologin/similar as their login shell.",
120
+ default=lambda config: config.SU_SHELL,
121
+ ),
122
+ "_doas": ArgumentMeta(
123
+ "Execute/apply any changes with doas.",
124
+ default=lambda config: config.DOAS,
125
+ ),
126
+ "_doas_user": ArgumentMeta(
127
+ "Execute/apply any changes with doas as a non-root user.",
128
+ default=lambda config: config.DOAS_USER,
129
+ ),
133
130
  }
134
131
 
135
- meta_kwargs = {
136
- # NOTE: name is the only non-_-prefixed argument
137
- "name": {
138
- "description": "Name of the operation.",
139
- "type": str,
140
- },
141
- "_ignore_errors": {
142
- "description": "Ignore errors when executing the operation.",
143
- "default": lambda config: config.IGNORE_ERRORS,
144
- "type": bool,
145
- },
146
- "_continue_on_error": {
147
- "description": (
148
- "Continue executing operation commands after error. "
149
- "Only applies when ``_ignore_errors`` is true."
150
- ),
151
- "default": False,
152
- "type": bool,
153
- },
154
- "_precondition": {
155
- "description": "Command to execute & check before the operation commands begin.",
156
- "type": str,
157
- },
158
- "_postcondition": {
159
- "description": "Command to execute & check after the operation commands complete.",
160
- "type": str,
161
- },
162
- # Lambda on the next two are to workaround a circular import
163
- "_on_success": {
164
- "description": "Callback function to execute on success.",
165
- "type": lambda: Callable[[State, pyinfra.api.Host, str], None],
166
- },
167
- "_on_error": {
168
- "description": "Callback function to execute on error.",
169
- "type": lambda: Callable[[State, pyinfra.api.Host, str], None],
170
- },
132
+ shell_argument_meta: dict[str, ArgumentMeta] = {
133
+ "_shell_executable": ArgumentMeta(
134
+ "The shell executable to use for executing commands.",
135
+ default=lambda config: config.SHELL,
136
+ ),
137
+ "_chdir": ArgumentMeta(
138
+ "Directory to switch to before executing the command.",
139
+ default=lambda _: "",
140
+ ),
141
+ "_env": ArgumentMeta(
142
+ "Dictionary of environment variables to set.",
143
+ default=lambda _: {},
144
+ handler=generate_env,
145
+ ),
146
+ "_success_exit_codes": ArgumentMeta(
147
+ "List of exit codes to consider a success.",
148
+ default=lambda _: [0],
149
+ ),
150
+ "_timeout": ArgumentMeta(
151
+ "Timeout for *each* command executed during the operation.",
152
+ default=lambda _: None,
153
+ ),
154
+ "_get_pty": ArgumentMeta(
155
+ "Whether to get a pseudoTTY when executing any commands.",
156
+ default=lambda _: False,
157
+ ),
158
+ "_stdin": ArgumentMeta(
159
+ "String or buffer to send to the stdin of any commands.",
160
+ default=lambda _: None,
161
+ ),
171
162
  }
172
163
 
173
- # Execution kwargs are global - ie must be identical for every host
174
- execution_kwargs = {
175
- "_parallel": {
176
- "description": "Run this operation in batches of hosts.",
177
- "default": lambda config: config.PARALLEL,
178
- "type": int,
179
- },
180
- "_run_once": {
181
- "description": "Only execute this operation once, on the first host to see it.",
182
- "default": lambda config: False,
183
- "type": bool,
184
- },
185
- "_serial": {
186
- "description": "Run this operation host by host, rather than in parallel.",
187
- "default": lambda config: False,
188
- "type": bool,
189
- },
190
- }
191
164
 
192
- # TODO: refactor these into classes so they can be typed properly, remove Any
193
- ALL_ARGUMENTS: Dict[str, Dict[str, Any]] = {
194
- **auth_kwargs,
195
- **shell_kwargs,
196
- **meta_kwargs,
197
- **execution_kwargs,
198
- }
165
+ # Meta arguments
166
+ # These provide/extend additional operation metadata
199
167
 
200
- OPERATION_KWARG_DOC: List[Tuple[str, Optional[str], Dict[str, Dict[str, Any]]]] = [
201
- ("Privilege & user escalation", None, auth_kwargs),
202
- ("Shell control & features", None, shell_kwargs),
203
- ("Operation meta & callbacks", "Not available in facts.", meta_kwargs),
204
- (
205
- "Execution strategy",
206
- "Not available in facts, value must be the same for all hosts.",
207
- execution_kwargs,
208
- ),
209
- ]
210
168
 
169
+ class MetaArguments(TypedDict):
170
+ name: str
171
+ _ignore_errors: bool
172
+ _continue_on_error: bool
173
+ _if: List[Callable[[], bool]]
211
174
 
212
- def _get_internal_key(key: str) -> str:
213
- if key.startswith("_"):
214
- return key[1:]
215
- return key
216
175
 
176
+ meta_argument_meta: dict[str, ArgumentMeta] = {
177
+ # NOTE: name is the only non-_-prefixed argument
178
+ "name": ArgumentMeta(
179
+ "Name of the operation.",
180
+ default=lambda _: None,
181
+ ),
182
+ "_ignore_errors": ArgumentMeta(
183
+ "Ignore errors when executing the operation.",
184
+ default=lambda config: config.IGNORE_ERRORS,
185
+ ),
186
+ "_continue_on_error": ArgumentMeta(
187
+ (
188
+ "Continue executing operation commands after error. "
189
+ "Only applies when ``_ignore_errors`` is true."
190
+ ),
191
+ default=lambda _: False,
192
+ ),
193
+ "_if": ArgumentMeta(
194
+ "Only run this operation if these functions returns True",
195
+ default=lambda _: [],
196
+ ),
197
+ }
217
198
 
218
- @memoize
219
- def get_execution_kwarg_keys() -> List[Any]:
220
- return [_get_internal_key(key) for key in execution_kwargs.keys()]
221
199
 
200
+ # Execution arguments
201
+ # These alter how pyinfra is to execute an operation. Notably these must all have the same value
202
+ # over every target host for the same operation.
222
203
 
223
- @memoize
224
- def get_executor_kwarg_keys() -> List[Any]:
225
- keys: Set[str] = set()
226
- keys.update(auth_kwargs.keys(), shell_kwargs.keys())
227
- return [_get_internal_key(key) for key in keys]
228
204
 
205
+ class ExecutionArguments(TypedDict):
206
+ _parallel: int
207
+ _run_once: bool
208
+ _serial: bool
229
209
 
230
- @memoize
231
- def show_legacy_argument_warning(key, call_location):
232
- logger.warning(
233
- (
234
- '{0}:\n\tGlobal arguments should be prefixed "_", '
235
- "please us the `{1}` keyword argument in place of `{2}`."
236
- ).format(call_location, "_{0}".format(key), key),
237
- )
238
210
 
211
+ execution_argument_meta: dict[str, ArgumentMeta] = {
212
+ "_parallel": ArgumentMeta(
213
+ "Run this operation in batches of hosts.",
214
+ default=lambda config: config.PARALLEL,
215
+ ),
216
+ "_run_once": ArgumentMeta(
217
+ "Only execute this operation once, on the first host to see it.",
218
+ default=lambda _: False,
219
+ ),
220
+ "_serial": ArgumentMeta(
221
+ "Run this operation host by host, rather than in parallel.",
222
+ default=lambda _: False,
223
+ ),
224
+ }
239
225
 
240
- @memoize
241
- def show_legacy_argument_host_data_warning(key):
242
- logger.warning(
243
- (
244
- 'Global arguments should be prefixed "_", '
245
- "please us the `host.data._{0}` keyword argument in place of `host.data.{0}`."
246
- ).format(key),
247
- )
248
226
 
227
+ class AllArguments(ConnectorArguments, MetaArguments, ExecutionArguments):
228
+ pass
249
229
 
250
- sentinel = object()
230
+
231
+ all_argument_meta: dict[str, ArgumentMeta] = {
232
+ **auth_argument_meta,
233
+ **shell_argument_meta,
234
+ **meta_argument_meta,
235
+ **execution_argument_meta,
236
+ }
237
+
238
+ EXECUTION_KWARG_KEYS = list(ExecutionArguments.__annotations__.keys())
239
+ CONNECTOR_ARGUMENT_KEYS = list(ConnectorArguments.__annotations__.keys())
240
+
241
+ __argument_docs__ = {
242
+ "Privilege & user escalation": (
243
+ auth_argument_meta,
244
+ """
245
+ .. code:: python
246
+
247
+ # Execute a command with sudo
248
+ server.user(
249
+ name="Create pyinfra user using sudo",
250
+ user="pyinfra",
251
+ _sudo=True,
252
+ )
253
+
254
+ # Execute a command with a specific sudo password
255
+ server.user(
256
+ name="Create pyinfra user using sudo",
257
+ user="pyinfra",
258
+ _sudo=True,
259
+ _sudo_password="my-secret-password",
260
+ )
261
+ """,
262
+ ),
263
+ "Shell control & features": (
264
+ shell_argument_meta,
265
+ """
266
+ .. code:: python
267
+
268
+ # Execute from a specific directory
269
+ server.shell(
270
+ name="Bootstrap nginx params",
271
+ commands=["openssl dhparam -out dhparam.pem 4096"],
272
+ _chdir="/etc/ssl/certs",
273
+ )
274
+ """,
275
+ ),
276
+ "Operation meta & callbacks": (meta_argument_meta, ""),
277
+ "Execution strategy": (execution_argument_meta, ""),
278
+ }
251
279
 
252
280
 
253
281
  def pop_global_arguments(
254
- kwargs: Dict[Any, Any],
282
+ kwargs: dict[str, Any],
255
283
  state: Optional["State"] = None,
256
284
  host: Optional["Host"] = None,
257
285
  keys_to_check=None,
258
- ):
286
+ ) -> tuple[AllArguments, list[str]]:
259
287
  """
260
288
  Pop and return operation global keyword arguments, in preferred order:
261
289
 
@@ -263,16 +291,6 @@ def pop_global_arguments(
263
291
  + From any current @deploy context (deploy kwargs)
264
292
  + From the host data variables
265
293
  + From the config variables
266
-
267
- Note this function is only called directly in the @operation & @deploy decorator
268
- wrappers which the user should pass global arguments prefixed "_". This is to
269
- avoid any clashes with operation and deploy functions both internal and third
270
- party.
271
-
272
- This is a bit strange because internally pyinfra uses non-_-prefixed arguments,
273
- and this function is responsible for the translation between the two.
274
-
275
- TODO: is this weird-ness acceptable? Is it worth updating internal use to _prefix?
276
294
  """
277
295
 
278
296
  state = state or context.state
@@ -282,52 +300,40 @@ def pop_global_arguments(
282
300
  if context.ctx_config.isset():
283
301
  config = context.config
284
302
 
285
- meta_kwargs = host.current_deploy_kwargs or {}
303
+ meta_kwargs: dict[str, Any] = host.current_deploy_kwargs or {} # type: ignore[assignment]
286
304
 
287
- global_kwargs = {}
288
- found_keys = []
305
+ arguments: dict[str, Any] = {}
306
+ found_keys: list[str] = []
289
307
 
290
- for key, argument in ALL_ARGUMENTS.items():
291
- internal_key = _get_internal_key(key)
292
-
293
- if keys_to_check and internal_key not in keys_to_check:
308
+ for key, type_ in get_type_hints(AllArguments).items():
309
+ if keys_to_check and key not in keys_to_check:
294
310
  continue
295
311
 
296
- handler: Optional[Callable] = None
297
- default: Optional[Callable] = None
298
-
299
- if isinstance(argument, dict):
300
- handler = argument.get("handler")
301
- default = argument.get("default")
302
- if default:
303
- default = default(config)
312
+ argument_meta = all_argument_meta[key]
313
+ handler = argument_meta.handler
314
+ default: Any = argument_meta.default(config)
304
315
 
305
- host_default: Any = getattr(host.data, key, sentinel)
306
-
307
- # TODO: remove this additional check in v3
308
- if host_default is sentinel and internal_key != key:
309
- host_default = getattr(host.data, internal_key, sentinel)
310
- if host_default is not sentinel:
311
- show_legacy_argument_host_data_warning(internal_key)
312
-
313
- if host_default is not sentinel:
316
+ host_default = getattr(host.data, key, default_sentinel)
317
+ if host_default is not default_sentinel:
314
318
  default = host_default
315
319
 
316
320
  if key in kwargs:
317
- found_keys.append(internal_key)
321
+ found_keys.append(key)
318
322
  value = kwargs.pop(key)
319
-
320
- # TODO: remove this additional check in v3
321
- elif internal_key in kwargs:
322
- show_legacy_argument_warning(internal_key, get_call_location(frame_offset=2))
323
- found_keys.append(internal_key)
324
- value = kwargs.pop(internal_key)
325
-
326
323
  else:
327
- value = meta_kwargs.get(internal_key, default)
324
+ value = meta_kwargs.get(key, default)
328
325
 
329
326
  if handler:
330
327
  value = handler(config, value)
331
328
 
332
- global_kwargs[internal_key] = value
333
- return global_kwargs, found_keys
329
+ if value != default:
330
+ raise_if_bad_type(
331
+ value,
332
+ type_,
333
+ ArgumentTypeError,
334
+ f"Invalid argument `{key}`:",
335
+ )
336
+
337
+ # TODO: why is type failing here?
338
+ arguments[key] = value # type: ignore
339
+ return cast(AllArguments, arguments), found_keys
@@ -0,0 +1,77 @@
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
+ # ConnectorArguments
36
+ #
37
+ # Auth
38
+ _sudo: bool = False,
39
+ _sudo_user: Optional[str] = None,
40
+ _use_sudo_login: bool = False,
41
+ _sudo_password: Optional[str] = None,
42
+ _preserve_sudo_env: bool = False,
43
+ _su_user: Optional[str] = None,
44
+ _use_su_login: bool = False,
45
+ _preserve_su_env: bool = False,
46
+ _su_shell: Optional[str] = None,
47
+ _doas: bool = False,
48
+ _doas_user: Optional[str] = None,
49
+ # Shell arguments
50
+ _shell_executable: Optional[str] = None,
51
+ _chdir: Optional[str] = None,
52
+ _env: Optional[Mapping[str, str]] = None,
53
+ # Connector control
54
+ _success_exit_codes: Iterable[int] = (0,),
55
+ _timeout: Optional[int] = None,
56
+ _get_pty: bool = False,
57
+ _stdin: Union[None, str, list[str], tuple[str, ...]] = None,
58
+ #
59
+ # MetaArguments
60
+ #
61
+ name: Optional[str] = None,
62
+ _ignore_errors: bool = False,
63
+ _continue_on_error: bool = False,
64
+ _if: Optional[List[Callable[[], bool]]] = None,
65
+ #
66
+ # ExecutionArguments
67
+ #
68
+ _parallel: Optional[int] = None,
69
+ _run_once: bool = False,
70
+ _serial: bool = False,
71
+ #
72
+ # The op itself
73
+ #
74
+ *args: P.args,
75
+ **kwargs: P.kwargs,
76
+ ) -> "OperationMeta":
77
+ ...