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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. pyinfra/api/__init__.py +3 -0
  2. pyinfra/api/arguments.py +265 -253
  3. pyinfra/api/arguments_typed.py +80 -0
  4. pyinfra/api/command.py +68 -53
  5. pyinfra/api/config.py +139 -32
  6. pyinfra/api/connect.py +1 -1
  7. pyinfra/api/connectors.py +7 -26
  8. pyinfra/api/deploy.py +21 -52
  9. pyinfra/api/exceptions.py +33 -8
  10. pyinfra/api/facts.py +102 -137
  11. pyinfra/api/host.py +150 -82
  12. pyinfra/api/inventory.py +21 -25
  13. pyinfra/api/operation.py +240 -198
  14. pyinfra/api/operations.py +102 -148
  15. pyinfra/api/state.py +137 -79
  16. pyinfra/api/util.py +79 -86
  17. pyinfra/connectors/base.py +147 -0
  18. pyinfra/connectors/chroot.py +160 -169
  19. pyinfra/connectors/docker.py +220 -237
  20. pyinfra/connectors/dockerssh.py +231 -253
  21. pyinfra/connectors/local.py +196 -208
  22. pyinfra/connectors/ssh.py +530 -613
  23. pyinfra/connectors/ssh_util.py +114 -0
  24. pyinfra/connectors/sshuserclient/client.py +5 -3
  25. pyinfra/connectors/terraform.py +86 -65
  26. pyinfra/connectors/util.py +211 -137
  27. pyinfra/connectors/vagrant.py +60 -53
  28. pyinfra/context.py +4 -2
  29. pyinfra/facts/apk.py +2 -0
  30. pyinfra/facts/apt.py +2 -0
  31. pyinfra/facts/brew.py +2 -0
  32. pyinfra/facts/bsdinit.py +2 -0
  33. pyinfra/facts/cargo.py +2 -0
  34. pyinfra/facts/choco.py +2 -0
  35. pyinfra/facts/deb.py +7 -2
  36. pyinfra/facts/dnf.py +2 -0
  37. pyinfra/facts/docker.py +19 -0
  38. pyinfra/facts/files.py +47 -32
  39. pyinfra/facts/gem.py +2 -0
  40. pyinfra/facts/git.py +3 -1
  41. pyinfra/facts/gpg.py +3 -1
  42. pyinfra/facts/hardware.py +34 -24
  43. pyinfra/facts/iptables.py +5 -3
  44. pyinfra/facts/launchd.py +2 -0
  45. pyinfra/facts/lxd.py +2 -0
  46. pyinfra/facts/mysql.py +13 -6
  47. pyinfra/facts/npm.py +1 -0
  48. pyinfra/facts/openrc.py +2 -0
  49. pyinfra/facts/pacman.py +6 -2
  50. pyinfra/facts/pip.py +2 -0
  51. pyinfra/facts/pkg.py +2 -0
  52. pyinfra/facts/pkgin.py +2 -0
  53. pyinfra/facts/postgres.py +168 -0
  54. pyinfra/facts/postgresql.py +6 -160
  55. pyinfra/facts/rpm.py +12 -9
  56. pyinfra/facts/runit.py +68 -0
  57. pyinfra/facts/selinux.py +3 -1
  58. pyinfra/facts/server.py +80 -36
  59. pyinfra/facts/snap.py +2 -0
  60. pyinfra/facts/systemd.py +31 -12
  61. pyinfra/facts/sysvinit.py +10 -10
  62. pyinfra/facts/upstart.py +2 -0
  63. pyinfra/facts/util/packaging.py +7 -4
  64. pyinfra/facts/vzctl.py +2 -0
  65. pyinfra/facts/xbps.py +2 -0
  66. pyinfra/facts/yum.py +2 -0
  67. pyinfra/facts/zypper.py +2 -0
  68. pyinfra/local.py +4 -5
  69. pyinfra/operations/apk.py +6 -4
  70. pyinfra/operations/apt.py +46 -65
  71. pyinfra/operations/brew.py +17 -22
  72. pyinfra/operations/bsdinit.py +9 -7
  73. pyinfra/operations/cargo.py +4 -2
  74. pyinfra/operations/choco.py +4 -2
  75. pyinfra/operations/dnf.py +19 -23
  76. pyinfra/operations/docker.py +339 -0
  77. pyinfra/operations/files.py +188 -386
  78. pyinfra/operations/gem.py +4 -2
  79. pyinfra/operations/git.py +24 -53
  80. pyinfra/operations/iptables.py +29 -35
  81. pyinfra/operations/launchd.py +6 -7
  82. pyinfra/operations/lxd.py +8 -13
  83. pyinfra/operations/mysql.py +62 -81
  84. pyinfra/operations/npm.py +9 -2
  85. pyinfra/operations/openrc.py +6 -4
  86. pyinfra/operations/pacman.py +7 -8
  87. pyinfra/operations/pip.py +25 -24
  88. pyinfra/operations/pkg.py +4 -2
  89. pyinfra/operations/pkgin.py +6 -4
  90. pyinfra/operations/postgres.py +349 -0
  91. pyinfra/operations/postgresql.py +18 -379
  92. pyinfra/operations/puppet.py +3 -1
  93. pyinfra/operations/python.py +8 -19
  94. pyinfra/operations/runit.py +182 -0
  95. pyinfra/operations/selinux.py +47 -44
  96. pyinfra/operations/server.py +111 -127
  97. pyinfra/operations/snap.py +4 -4
  98. pyinfra/operations/ssh.py +20 -33
  99. pyinfra/operations/systemd.py +19 -15
  100. pyinfra/operations/sysvinit.py +9 -16
  101. pyinfra/operations/upstart.py +9 -7
  102. pyinfra/operations/util/__init__.py +12 -0
  103. pyinfra/operations/util/docker.py +177 -0
  104. pyinfra/operations/util/files.py +24 -16
  105. pyinfra/operations/util/packaging.py +55 -57
  106. pyinfra/operations/util/service.py +39 -51
  107. pyinfra/operations/vzctl.py +12 -10
  108. pyinfra/operations/xbps.py +6 -4
  109. pyinfra/operations/yum.py +18 -22
  110. pyinfra/operations/zypper.py +12 -13
  111. pyinfra/version.py +5 -2
  112. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/METADATA +40 -41
  113. pyinfra-3.0.dist-info/RECORD +167 -0
  114. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/WHEEL +1 -1
  115. pyinfra-3.0.dist-info/entry_points.txt +11 -0
  116. pyinfra_cli/__main__.py +4 -3
  117. pyinfra_cli/commands.py +7 -2
  118. pyinfra_cli/exceptions.py +78 -42
  119. pyinfra_cli/inventory.py +40 -6
  120. pyinfra_cli/log.py +17 -3
  121. pyinfra_cli/main.py +133 -90
  122. pyinfra_cli/prints.py +95 -127
  123. pyinfra_cli/util.py +62 -29
  124. tests/test_api/test_api.py +2 -0
  125. tests/test_api/test_api_arguments.py +13 -13
  126. tests/test_api/test_api_deploys.py +28 -29
  127. tests/test_api/test_api_facts.py +60 -98
  128. tests/test_api/test_api_operations.py +101 -201
  129. tests/test_cli/test_cli.py +18 -49
  130. tests/test_cli/test_cli_deploy.py +11 -37
  131. tests/test_cli/test_cli_exceptions.py +50 -19
  132. tests/test_cli/util.py +1 -1
  133. tests/test_connectors/test_chroot.py +6 -6
  134. tests/test_connectors/test_docker.py +4 -4
  135. tests/test_connectors/test_dockerssh.py +38 -50
  136. tests/test_connectors/test_local.py +11 -12
  137. tests/test_connectors/test_ssh.py +105 -93
  138. tests/test_connectors/test_terraform.py +9 -15
  139. tests/test_connectors/test_util.py +24 -46
  140. tests/test_connectors/test_vagrant.py +7 -7
  141. pyinfra/api/operation.pyi +0 -117
  142. pyinfra/connectors/ansible.py +0 -171
  143. pyinfra/connectors/mech.py +0 -186
  144. pyinfra/connectors/pyinfrawinrmsession/__init__.py +0 -28
  145. pyinfra/connectors/winrm.py +0 -320
  146. pyinfra/facts/windows.py +0 -366
  147. pyinfra/facts/windows_files.py +0 -90
  148. pyinfra/operations/windows.py +0 -59
  149. pyinfra/operations/windows_files.py +0 -551
  150. pyinfra-2.9.1.dist-info/RECORD +0 -170
  151. pyinfra-2.9.1.dist-info/entry_points.txt +0 -14
  152. tests/test_connectors/test_ansible.py +0 -64
  153. tests/test_connectors/test_mech.py +0 -126
  154. tests/test_connectors/test_winrm.py +0 -76
  155. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/LICENSE.md +0 -0
  156. {pyinfra-2.9.1.dist-info → pyinfra-3.0.dist-info}/top_level.txt +0 -0
pyinfra/api/__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,295 @@
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
+ Type,
13
+ TypeVar,
12
14
  Union,
15
+ cast,
16
+ get_type_hints,
13
17
  )
14
18
 
15
- import pyinfra
16
- from pyinfra import context, logger
17
- from pyinfra.api.state import State
19
+ from typing_extensions import TypedDict
18
20
 
19
- from .util import get_call_location, memoize
21
+ from pyinfra import context
22
+ from pyinfra.api.exceptions import ArgumentTypeError
23
+ from pyinfra.api.state import State
24
+ from pyinfra.api.util import raise_if_bad_type
20
25
 
21
26
  if TYPE_CHECKING:
22
27
  from pyinfra.api.config import Config
23
28
  from pyinfra.api.host import Host
24
29
 
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
- }
30
+ T = TypeVar("T")
31
+ default_sentinel = object()
86
32
 
87
33
 
88
- def generate_env(config: "Config", value):
89
- env = config.ENV.copy()
34
+ class ArgumentMeta(Generic[T]):
35
+ description: str
36
+ default: Callable[["Config"], T]
37
+ handler: Optional[Callable[["Config", T], T]]
38
+
39
+ def __init__(self, description, default, handler=None) -> None:
40
+ self.description = description
41
+ self.default = default
42
+ self.handler = handler
43
+
44
+
45
+ # Connector arguments
46
+ # These are arguments passed to the various connectors that provide the underlying
47
+ # API to read/write external systems.
90
48
 
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
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.
52
+ class ConnectorArguments(TypedDict, total=False):
53
+ # Auth arguments
54
+ _sudo: bool
55
+ _sudo_user: str
56
+ _use_sudo_login: bool
57
+ _sudo_password: str
58
+ _preserve_sudo_env: bool
59
+ _su_user: str
60
+ _use_su_login: bool
61
+ _preserve_su_env: bool
62
+ _su_shell: str
63
+ _doas: bool
64
+ _doas_user: str
65
+
66
+ # Shell arguments
67
+ _shell_executable: str
68
+ _chdir: str
69
+ _env: Mapping[str, str]
70
+
71
+ # Connector control (outside of command generation)
72
+ _success_exit_codes: Iterable[int]
73
+ _timeout: int
74
+ _get_pty: bool
75
+ _stdin: Union[str, Iterable[str]]
76
+
77
+
78
+ def generate_env(config: "Config", value: dict) -> dict:
79
+ env = config.ENV.copy()
80
+ env.update(value)
98
81
  return env
99
82
 
100
83
 
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
- },
84
+ auth_argument_meta: dict[str, ArgumentMeta] = {
85
+ "_sudo": ArgumentMeta(
86
+ "Execute/apply any changes with sudo.",
87
+ default=lambda config: config.SUDO,
88
+ ),
89
+ "_sudo_user": ArgumentMeta(
90
+ "Execute/apply any changes with sudo as a non-root user.",
91
+ default=lambda config: config.SUDO_USER,
92
+ ),
93
+ "_use_sudo_login": ArgumentMeta(
94
+ "Execute sudo with a login shell.",
95
+ default=lambda config: config.USE_SUDO_LOGIN,
96
+ ),
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,
100
+ ),
101
+ "_preserve_sudo_env": ArgumentMeta(
102
+ "Preserve the shell environment of the connecting user when using sudo.",
103
+ default=lambda config: config.PRESERVE_SUDO_ENV,
104
+ ),
105
+ "_su_user": ArgumentMeta(
106
+ "Execute/apply any changes with this user using su.",
107
+ default=lambda config: config.SU_USER,
108
+ ),
109
+ "_use_su_login": ArgumentMeta(
110
+ "Execute su with a login shell.",
111
+ default=lambda config: config.USE_SU_LOGIN,
112
+ ),
113
+ "_preserve_su_env": ArgumentMeta(
114
+ "Preserve the shell environment of the connecting user when using su.",
115
+ default=lambda config: config.PRESERVE_SU_ENV,
116
+ ),
117
+ "_su_shell": ArgumentMeta(
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.",
121
+ default=lambda config: config.SU_SHELL,
122
+ ),
123
+ "_doas": ArgumentMeta(
124
+ "Execute/apply any changes with doas.",
125
+ default=lambda config: config.DOAS,
126
+ ),
127
+ "_doas_user": ArgumentMeta(
128
+ "Execute/apply any changes with doas as a non-root user.",
129
+ default=lambda config: config.DOAS_USER,
130
+ ),
131
+ }
132
+
133
+ shell_argument_meta: dict[str, ArgumentMeta] = {
134
+ "_shell_executable": ArgumentMeta(
135
+ "The shell executable to use for executing commands.",
136
+ default=lambda config: config.SHELL,
137
+ ),
138
+ "_chdir": ArgumentMeta(
139
+ "Directory to switch to before executing the command.",
140
+ default=lambda _: "",
141
+ ),
142
+ "_env": ArgumentMeta(
143
+ "Dictionary of environment variables to set.",
144
+ default=lambda _: {},
145
+ handler=generate_env,
146
+ ),
147
+ "_success_exit_codes": ArgumentMeta(
148
+ "List of exit codes to consider a success.",
149
+ default=lambda _: [0],
150
+ ),
151
+ "_timeout": ArgumentMeta(
152
+ "Timeout for *each* command executed during the operation.",
153
+ default=lambda _: None,
154
+ ),
155
+ "_get_pty": ArgumentMeta(
156
+ "Whether to get a pseudoTTY when executing any commands.",
157
+ default=lambda _: False,
158
+ ),
159
+ "_stdin": ArgumentMeta(
160
+ "String or buffer to send to the stdin of any commands.",
161
+ default=lambda _: None,
162
+ ),
133
163
  }
134
164
 
135
- meta_kwargs = {
165
+
166
+ # Meta arguments
167
+ # These provide/extend additional operation metadata
168
+
169
+
170
+ class MetaArguments(TypedDict):
171
+ name: str
172
+ _ignore_errors: bool
173
+ _continue_on_error: bool
174
+ _if: Union[List[Callable[[], bool]], Callable[[], bool], None]
175
+
176
+
177
+ meta_argument_meta: dict[str, ArgumentMeta] = {
136
178
  # 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": (
179
+ "name": ArgumentMeta(
180
+ "Name of the operation.",
181
+ default=lambda _: None,
182
+ ),
183
+ "_ignore_errors": ArgumentMeta(
184
+ "Ignore errors when executing the operation.",
185
+ default=lambda config: config.IGNORE_ERRORS,
186
+ ),
187
+ "_continue_on_error": ArgumentMeta(
188
+ (
148
189
  "Continue executing operation commands after error. "
149
190
  "Only applies when ``_ignore_errors`` is true."
150
191
  ),
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
- },
171
- }
172
-
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
-
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,
192
+ default=lambda _: False,
193
+ ),
194
+ "_if": ArgumentMeta(
195
+ "Only run this operation if these functions return True",
196
+ default=lambda _: [],
197
+ ),
198
198
  }
199
199
 
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
200
 
201
+ # Execution arguments
202
+ # These alter how pyinfra is to execute an operation. Notably these must all have the same value
203
+ # over every target host for the same operation.
211
204
 
212
- def _get_internal_key(key: str) -> str:
213
- if key.startswith("_"):
214
- return key[1:]
215
- return key
216
205
 
206
+ class ExecutionArguments(TypedDict):
207
+ _parallel: int
208
+ _run_once: bool
209
+ _serial: bool
217
210
 
218
- @memoize
219
- def get_execution_kwarg_keys() -> List[Any]:
220
- return [_get_internal_key(key) for key in execution_kwargs.keys()]
221
211
 
212
+ execution_argument_meta: dict[str, ArgumentMeta] = {
213
+ "_parallel": ArgumentMeta(
214
+ "Run this operation in batches of hosts.",
215
+ default=lambda config: config.PARALLEL,
216
+ ),
217
+ "_run_once": ArgumentMeta(
218
+ "Only execute this operation once, on the first host to see it.",
219
+ default=lambda _: False,
220
+ ),
221
+ "_serial": ArgumentMeta(
222
+ "Run this operation host by host, rather than in parallel.",
223
+ default=lambda _: False,
224
+ ),
225
+ }
222
226
 
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
227
 
228
+ class AllArguments(ConnectorArguments, MetaArguments, ExecutionArguments):
229
+ pass
229
230
 
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
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())
239
235
 
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
236
 
237
+ all_argument_meta: dict[str, ArgumentMeta] = {
238
+ **auth_argument_meta,
239
+ **shell_argument_meta,
240
+ **meta_argument_meta,
241
+ **execution_argument_meta,
242
+ }
249
243
 
250
- sentinel = object()
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
+ """,
268
+ ),
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
+ }
251
285
 
252
286
 
253
287
  def pop_global_arguments(
254
- kwargs: Dict[Any, Any],
288
+ kwargs: dict[str, Any],
255
289
  state: Optional["State"] = None,
256
290
  host: Optional["Host"] = None,
257
291
  keys_to_check=None,
258
- ):
292
+ ) -> tuple[AllArguments, list[str]]:
259
293
  """
260
294
  Pop and return operation global keyword arguments, in preferred order:
261
295
 
@@ -263,16 +297,6 @@ def pop_global_arguments(
263
297
  + From any current @deploy context (deploy kwargs)
264
298
  + From the host data variables
265
299
  + 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
300
  """
277
301
 
278
302
  state = state or context.state
@@ -282,52 +306,40 @@ def pop_global_arguments(
282
306
  if context.ctx_config.isset():
283
307
  config = context.config
284
308
 
285
- meta_kwargs = host.current_deploy_kwargs or {}
309
+ meta_kwargs: dict[str, Any] = host.current_deploy_kwargs or {} # type: ignore[assignment]
286
310
 
287
- global_kwargs = {}
288
- found_keys = []
311
+ arguments: dict[str, Any] = {}
312
+ found_keys: list[str] = []
289
313
 
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:
314
+ for key, type_ in all_global_arguments():
315
+ if keys_to_check and key not in keys_to_check:
294
316
  continue
295
317
 
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)
318
+ argument_meta = all_argument_meta[key]
319
+ handler = argument_meta.handler
320
+ default: Any = argument_meta.default(config)
304
321
 
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:
322
+ host_default = getattr(host.data, key, default_sentinel)
323
+ if host_default is not default_sentinel:
314
324
  default = host_default
315
325
 
316
326
  if key in kwargs:
317
- found_keys.append(internal_key)
327
+ found_keys.append(key)
318
328
  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
329
  else:
327
- value = meta_kwargs.get(internal_key, default)
330
+ value = meta_kwargs.get(key, default)
328
331
 
329
332
  if handler:
330
333
  value = handler(config, value)
331
334
 
332
- global_kwargs[internal_key] = value
333
- return global_kwargs, found_keys
335
+ if value != default:
336
+ raise_if_bad_type(
337
+ value,
338
+ type_,
339
+ ArgumentTypeError,
340
+ f"Invalid argument `{key}`:",
341
+ )
342
+
343
+ # TODO: why is type failing here?
344
+ arguments[key] = value # type: ignore
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": ...