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/command.py CHANGED
@@ -1,20 +1,23 @@
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
6
+ from typing import IO, TYPE_CHECKING, Callable, Union
5
7
 
6
8
  import gevent
9
+ from typing_extensions import Unpack
7
10
 
8
11
  from pyinfra.context import ctx_config, ctx_host
9
12
 
10
- from .arguments import get_executor_kwarg_keys
13
+ from .arguments import ConnectorArguments
11
14
 
12
15
  if TYPE_CHECKING:
13
16
  from pyinfra.api.host import Host
14
17
  from pyinfra.api.state import State
15
18
 
16
19
 
17
- def make_formatted_string_command(string: str, *args, **kwargs):
20
+ def make_formatted_string_command(string: str, *args, **kwargs) -> "StringCommand":
18
21
  """
19
22
  Helper function that takes a shell command or script as a string, splits it
20
23
  using ``shlex.split`` and then formats each bit, returning a ``StringCommand``
@@ -50,39 +53,46 @@ class MaskString(str):
50
53
 
51
54
 
52
55
  class QuoteString:
53
- def __init__(self, obj):
54
- self.object = obj
56
+ obj: Union[str, "StringCommand"]
55
57
 
56
- def __repr__(self):
57
- return "QuoteString({0})".format(self.object)
58
+ def __init__(self, obj: Union[str, "StringCommand"]):
59
+ self.obj = obj
60
+
61
+ def __repr__(self) -> str:
62
+ return f"QuoteString({self.obj})"
58
63
 
59
64
 
60
65
  class PyinfraCommand:
61
- def __init__(self, *args, **kwargs):
62
- self.executor_kwargs = {
63
- key: kwargs[key] for key in get_executor_kwarg_keys() if key in kwargs
64
- }
66
+ connector_arguments: ConnectorArguments
65
67
 
66
- def __eq__(self, other):
68
+ def __init__(self, **arguments: Unpack[ConnectorArguments]):
69
+ self.connector_arguments = arguments
70
+
71
+ def __eq__(self, other) -> bool:
67
72
  if isinstance(other, self.__class__) and repr(self) == repr(other):
68
73
  return True
69
74
  return False
70
75
 
71
- def execute(self, state: "State", host: "Host", executor_kwargs):
76
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
72
77
  raise NotImplementedError
73
78
 
74
79
 
75
80
  class StringCommand(PyinfraCommand):
76
- def __init__(self, *bits, **kwargs):
77
- super().__init__(**kwargs)
81
+ def __init__(
82
+ self,
83
+ *bits,
84
+ _separator=" ",
85
+ **arguments: Unpack[ConnectorArguments],
86
+ ):
87
+ super().__init__(**arguments)
78
88
  self.bits = bits
79
- self.separator = kwargs.pop("_separator", " ")
89
+ self.separator = _separator
80
90
 
81
- def __str__(self):
91
+ def __str__(self) -> str:
82
92
  return self.get_masked_value()
83
93
 
84
- def __repr__(self):
85
- return "StringCommand({0})".format(self.get_masked_value())
94
+ def __repr__(self) -> str:
95
+ return f"StringCommand({self.get_masked_value()})"
86
96
 
87
97
  def _get_all_bits(self, bit_accessor):
88
98
  all_bits = []
@@ -91,7 +101,7 @@ class StringCommand(PyinfraCommand):
91
101
  quote = False
92
102
  if isinstance(bit, QuoteString):
93
103
  quote = True
94
- bit = bit.object
104
+ bit = bit.obj
95
105
 
96
106
  if isinstance(bit, StringCommand):
97
107
  bit = bit_accessor(bit)
@@ -106,14 +116,14 @@ class StringCommand(PyinfraCommand):
106
116
 
107
117
  return all_bits
108
118
 
109
- def get_raw_value(self):
119
+ def get_raw_value(self) -> str:
110
120
  return self.separator.join(
111
121
  self._get_all_bits(
112
122
  lambda bit: bit.get_raw_value(),
113
123
  ),
114
124
  )
115
125
 
116
- def get_masked_value(self):
126
+ def get_masked_value(self) -> str:
117
127
  return self.separator.join(
118
128
  [
119
129
  "***" if isinstance(bit, MaskString) else bit
@@ -121,20 +131,25 @@ class StringCommand(PyinfraCommand):
121
131
  ],
122
132
  )
123
133
 
124
- def execute(self, state: "State", host: "Host", executor_kwargs):
125
- executor_kwargs.update(self.executor_kwargs)
134
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
135
+ connector_arguments.update(self.connector_arguments)
126
136
 
127
137
  return host.run_shell_command(
128
138
  self,
129
139
  print_output=state.print_output,
130
140
  print_input=state.print_input,
131
- return_combined_output=True,
132
- **executor_kwargs,
141
+ **connector_arguments,
133
142
  )
134
143
 
135
144
 
136
145
  class FileUploadCommand(PyinfraCommand):
137
- def __init__(self, src: str, dest: str, remote_temp_filename=None, **kwargs):
146
+ def __init__(
147
+ self,
148
+ src: str | IO,
149
+ dest: str,
150
+ remote_temp_filename=None,
151
+ **kwargs: Unpack[ConnectorArguments],
152
+ ):
138
153
  super().__init__(**kwargs)
139
154
  self.src = src
140
155
  self.dest = dest
@@ -143,8 +158,8 @@ class FileUploadCommand(PyinfraCommand):
143
158
  def __repr__(self):
144
159
  return "FileUploadCommand({0}, {1})".format(self.src, self.dest)
145
160
 
146
- def execute(self, state: "State", host: "Host", executor_kwargs):
147
- executor_kwargs.update(self.executor_kwargs)
161
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
162
+ connector_arguments.update(self.connector_arguments)
148
163
 
149
164
  return host.put_file(
150
165
  self.src,
@@ -152,12 +167,18 @@ class FileUploadCommand(PyinfraCommand):
152
167
  remote_temp_filename=self.remote_temp_filename,
153
168
  print_output=state.print_output,
154
169
  print_input=state.print_input,
155
- **executor_kwargs,
170
+ **connector_arguments,
156
171
  )
157
172
 
158
173
 
159
174
  class FileDownloadCommand(PyinfraCommand):
160
- def __init__(self, src: str, dest: str, remote_temp_filename=None, **kwargs):
175
+ def __init__(
176
+ self,
177
+ src: str,
178
+ dest: str | IO,
179
+ remote_temp_filename=None,
180
+ **kwargs: Unpack[ConnectorArguments],
181
+ ):
161
182
  super().__init__(**kwargs)
162
183
  self.src = src
163
184
  self.dest = dest
@@ -166,8 +187,8 @@ class FileDownloadCommand(PyinfraCommand):
166
187
  def __repr__(self):
167
188
  return "FileDownloadCommand({0}, {1})".format(self.src, self.dest)
168
189
 
169
- def execute(self, state: "State", host: "Host", executor_kwargs):
170
- executor_kwargs.update(self.executor_kwargs)
190
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
191
+ connector_arguments.update(self.connector_arguments)
171
192
 
172
193
  return host.get_file(
173
194
  self.src,
@@ -175,12 +196,18 @@ class FileDownloadCommand(PyinfraCommand):
175
196
  remote_temp_filename=self.remote_temp_filename,
176
197
  print_output=state.print_output,
177
198
  print_input=state.print_input,
178
- **executor_kwargs,
199
+ **connector_arguments,
179
200
  )
180
201
 
181
202
 
182
203
  class FunctionCommand(PyinfraCommand):
183
- def __init__(self, function, args, func_kwargs, **kwargs):
204
+ def __init__(
205
+ self,
206
+ function: Callable,
207
+ args,
208
+ func_kwargs,
209
+ **kwargs: Unpack[ConnectorArguments],
210
+ ):
184
211
  super().__init__(**kwargs)
185
212
  self.function = function
186
213
  self.args = args
@@ -193,34 +220,22 @@ class FunctionCommand(PyinfraCommand):
193
220
  self.kwargs,
194
221
  )
195
222
 
196
- def execute(self, state: "State", host: "Host", executor_kwargs):
223
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
197
224
  argspec = getfullargspec(self.function)
198
225
  if "state" in argspec.args and "host" in argspec.args:
199
226
  return self.function(state, host, *self.args, **self.kwargs)
200
227
 
201
- # Note: we use try/except here to catch any exception inside the greenlet
202
- # and bubble that back out as a return value before re-raising outside.
203
- # This is because gevent dumps stack traces when raised within a greenlet
204
- # and we want to handle that ourselves.
205
228
  def execute_function():
206
229
  with ctx_config.use(state.config.copy()):
207
230
  with ctx_host.use(host):
208
- try:
209
- self.function(*self.args, **self.kwargs)
210
- except Exception as e:
211
- return e
212
- else:
213
- return True
231
+ self.function(*self.args, **self.kwargs)
214
232
 
215
233
  greenlet = gevent.spawn(execute_function)
216
- res = greenlet.get()
217
- if isinstance(res, Exception):
218
- raise res
219
- return res
234
+ return greenlet.get()
220
235
 
221
236
 
222
237
  class RsyncCommand(PyinfraCommand):
223
- def __init__(self, src: str, dest: str, flags, **kwargs):
238
+ def __init__(self, src: str, dest: str, flags, **kwargs: Unpack[ConnectorArguments]):
224
239
  super().__init__(**kwargs)
225
240
  self.src = src
226
241
  self.dest = dest
@@ -229,12 +244,12 @@ class RsyncCommand(PyinfraCommand):
229
244
  def __repr__(self):
230
245
  return "RsyncCommand({0}, {1}, {2})".format(self.src, self.dest, self.flags)
231
246
 
232
- def execute(self, state: "State", host: "Host", executor_kwargs):
247
+ def execute(self, state: "State", host: "Host", connector_arguments: ConnectorArguments):
233
248
  return host.rsync(
234
249
  self.src,
235
250
  self.dest,
236
251
  self.flags,
237
252
  print_output=state.print_output,
238
253
  print_input=state.print_input,
239
- **executor_kwargs,
254
+ **connector_arguments,
240
255
  )
pyinfra/api/config.py CHANGED
@@ -1,6 +1,14 @@
1
+ try:
2
+ import importlib_metadata
3
+ except ImportError:
4
+ import importlib.metadata as importlib_metadata # type: ignore[no-redef]
1
5
  from os import path
6
+ from typing import Iterable, Optional, Set
2
7
 
3
- from pkg_resources import Requirement, ResolutionError, parse_version, require
8
+ from packaging.markers import Marker
9
+ from packaging.requirements import Requirement
10
+ from packaging.specifiers import SpecifierSet
11
+ from packaging.version import Version
4
12
 
5
13
  from pyinfra import __version__, state
6
14
 
@@ -9,37 +17,40 @@ from .exceptions import PyinfraError
9
17
 
10
18
  class ConfigDefaults:
11
19
  # % of hosts which have to fail for all operations to stop
12
- FAIL_PERCENT = None
20
+ FAIL_PERCENT: Optional[int] = None
13
21
  # Seconds to timeout SSH connections
14
- CONNECT_TIMEOUT = 10
15
- # Temporary directory (on the remote side) to use for caching any files/downloads
16
- TEMP_DIR = "/tmp"
22
+ CONNECT_TIMEOUT: int = 10
23
+ # Temporary directory (on the remote side) to use for caching any files/downloads, the default
24
+ # None value first tries to load the hosts' temporary directory configured via "TMPDIR" env
25
+ # variable, falling back to DEFAULT_TEMP_DIR if not set.
26
+ TEMP_DIR: Optional[str] = None
27
+ DEFAULT_TEMP_DIR: str = "/tmp"
17
28
  # Gevent pool size (defaults to #of target hosts)
18
- PARALLEL = 0
29
+ PARALLEL: int = 0
19
30
  # Specify the required pyinfra version (using PEP 440 setuptools specifier)
20
- REQUIRE_PYINFRA_VERSION = None
31
+ REQUIRE_PYINFRA_VERSION: Optional[str] = None
21
32
  # Specify any required packages (either using PEP 440 or a requirements file)
22
33
  # Note: this can also include pyinfra potentially replacing REQUIRE_PYINFRA_VERSION
23
- REQUIRE_PACKAGES = None
34
+ REQUIRE_PACKAGES: Optional[str] = None
24
35
  # All these can be overridden inside individual operation calls:
25
36
  # Switch to this user (from ssh_user) using su before executing operations
26
- SU_USER = None
27
- USE_SU_LOGIN = False
28
- SU_SHELL = None
29
- PRESERVE_SU_ENV = False
37
+ SU_USER: Optional[str] = None
38
+ USE_SU_LOGIN: bool = False
39
+ SU_SHELL: bool = False
40
+ PRESERVE_SU_ENV: bool = False
30
41
  # Use sudo and optional user
31
- SUDO = False
32
- SUDO_USER = None
33
- PRESERVE_SUDO_ENV = False
34
- USE_SUDO_LOGIN = False
35
- USE_SUDO_PASSWORD = False
42
+ SUDO: bool = False
43
+ SUDO_USER: Optional[str] = None
44
+ PRESERVE_SUDO_ENV: bool = False
45
+ USE_SUDO_LOGIN: bool = False
46
+ SUDO_PASSWORD: Optional[str] = None
36
47
  # Use doas and optional user
37
- DOAS = False
38
- DOAS_USER = None
48
+ DOAS: bool = False
49
+ DOAS_USER: Optional[str] = None
39
50
  # Only show errors but don't count as failure
40
- IGNORE_ERRORS = False
51
+ IGNORE_ERRORS: bool = False
41
52
  # Shell to use to execute commands
42
- SHELL = "sh"
53
+ SHELL: str = "sh"
43
54
 
44
55
 
45
56
  config_defaults = {key: value for key, value in ConfigDefaults.__dict__.items() if key.isupper()}
@@ -48,15 +59,113 @@ config_defaults = {key: value for key, value in ConfigDefaults.__dict__.items()
48
59
  def check_pyinfra_version(version: str):
49
60
  if not version:
50
61
  return
51
- running_version = parse_version(__version__)
52
- required_versions = Requirement.parse("pyinfra{0}".format(version))
62
+ running_version = Version(__version__)
63
+ required_versions = SpecifierSet(version)
53
64
 
54
- if not required_versions.specifier.contains(running_version):
65
+ if running_version not in required_versions:
55
66
  raise PyinfraError(
56
67
  f"pyinfra version requirement not met (requires {version}, running {__version__})"
57
68
  )
58
69
 
59
70
 
71
+ def _check_requirements(requirements: Iterable[str]) -> Set[Requirement]:
72
+ """
73
+ Check whether each of the given requirements and all their dependencies are
74
+ installed.
75
+
76
+ Or more precisely, this checks that each of the given *requirements* is
77
+ satisfied by some installed *distribution package*, and so on recursively
78
+ for each of the dependencies of those distribution packages. The terminology
79
+ here is as follows:
80
+
81
+ * A *distribution package* is essentially a thing that can be installed with
82
+ ``pip``, from an sdist or wheel or Git repo or so on.
83
+ * A *requirement* is the expectation that a distribution package satisfying
84
+ some constraint is installed.
85
+ * A *dependency* is a requirement specified by a distribution package (as
86
+ opposed to the requirements passed in to this function).
87
+
88
+ So what this function does is start from the given requirements, for each
89
+ one check that it is satisfied by some installed distribution package, and
90
+ if so recursively perform the same check on all the dependencies of that
91
+ distribution package. In short, it's traversing the graph of package
92
+ requirements. It stops whenever it finds a requirement that is not satisfied
93
+ (i.e. a required package that is not installed), or when it runs out of
94
+ requirements to check.
95
+
96
+ .. note::
97
+ This is basically equivalent to ``pkg_resources.require()`` except that
98
+ when ``require()`` succeeds, it will return the list of distribution
99
+ packages that satisfy the given requirements and their dependencies, and
100
+ when it fails, it will raise an exception. This function just returns
101
+ the requirements which were not satisfied instead.
102
+
103
+ :param requirements: The requirements to check for in the set of installed
104
+ packages (along with their dependencies).
105
+ :return: The set of requirements that were not satisfied, which will be
106
+ an empty set if all requirements (recursively) were satisfied.
107
+ """
108
+
109
+ # Based on pkg_resources.require() from setuptools. The implementation of
110
+ # hbutils.system.check_reqs() from the hbutils package was also helpful in
111
+ # clarifying what this is supposed to do.
112
+
113
+ reqs_to_check: Set[Requirement] = set(Requirement(r) for r in requirements)
114
+ reqs_satisfied: Set[Requirement] = set()
115
+ reqs_not_satisfied: Set[Requirement] = set()
116
+
117
+ while reqs_to_check:
118
+ req = reqs_to_check.pop()
119
+ assert req not in reqs_satisfied and req not in reqs_not_satisfied
120
+
121
+ # Check for an installed distribution package with the right name and version
122
+ try:
123
+ dist = importlib_metadata.distribution(req.name)
124
+ except importlib_metadata.PackageNotFoundError:
125
+ # No installed package with the right name
126
+ # This would raise a DistributionNotFound error from pkg_resources.require()
127
+ reqs_not_satisfied.add(req)
128
+ continue
129
+
130
+ if dist.version not in req.specifier:
131
+ # There is a distribution with the right name but wrong version
132
+ # This would raise a VersionConflict error from pkg_resources.require()
133
+ reqs_not_satisfied.add(req)
134
+ continue
135
+
136
+ reqs_satisfied.add(req)
137
+
138
+ # If the distribution package has dependencies of its own, go through
139
+ # those dependencies and for each one add it to the set to be checked if
140
+ # - it's unconditional (no marker)
141
+ # - or it's conditional and the condition is satisfied (the marker
142
+ # evaluates to true) in the current environment
143
+ # Markers can check things like the Python version and system version
144
+ # etc., and/or they can check which extras of the distribution package
145
+ # were required. To facilitate checking extras we have to pass the extra
146
+ # in the environment when calling Marker.evaluate().
147
+ if dist.requires:
148
+ if req.extras:
149
+ extras_envs = [{"extra": extra} for extra in req.extras]
150
+
151
+ def evaluate_marker(marker: Marker) -> bool:
152
+ return any(map(marker.evaluate, extras_envs))
153
+
154
+ else:
155
+
156
+ def evaluate_marker(marker: Marker) -> bool:
157
+ return marker.evaluate()
158
+
159
+ for dist_req_str in dist.requires:
160
+ dist_req = Requirement(dist_req_str)
161
+ if dist_req in reqs_satisfied or dist_req in reqs_not_satisfied:
162
+ continue
163
+ if (not dist_req.marker) or evaluate_marker(dist_req.marker):
164
+ reqs_to_check.add(dist_req)
165
+
166
+ return reqs_not_satisfied
167
+
168
+
60
169
  def check_require_packages(requirements_config):
61
170
  if not requirements_config:
62
171
  return
@@ -64,17 +173,15 @@ def check_require_packages(requirements_config):
64
173
  if isinstance(requirements_config, (list, tuple)):
65
174
  requirements = requirements_config
66
175
  else:
67
- with open(path.join(state.cwd, requirements_config), encoding="utf-8") as f:
176
+ with open(path.join(state.cwd or "", requirements_config), encoding="utf-8") as f:
68
177
  requirements = [line.split("#egg=")[-1] for line in f.read().splitlines()]
69
178
 
70
- try:
71
- require(requirements)
72
- except ResolutionError as e:
179
+ requirements_not_met = _check_requirements(requirements)
180
+ if requirements_not_met:
73
181
  raise PyinfraError(
74
- "Deploy requirements ({0}) not met: {1}".format(
75
- requirements_config,
76
- e,
77
- ),
182
+ "Deploy requirements ({0}) not met: missing {1}".format(
183
+ requirements_config, ", ".join(str(r) for r in requirements_not_met)
184
+ )
78
185
  )
79
186
 
80
187
 
pyinfra/api/connect.py CHANGED
@@ -36,7 +36,7 @@ def connect_all(state: "State"):
36
36
  # Raise any unexpected exception
37
37
  greenlet.get()
38
38
 
39
- if host.connection:
39
+ if host.connected:
40
40
  state.activate_host(host)
41
41
  else:
42
42
  failed_hosts.add(host)
pyinfra/api/connectors.py CHANGED
@@ -1,36 +1,17 @@
1
- import pkg_resources
2
-
3
-
4
- class BaseConnectorMeta:
5
- handles_execution = False
6
- keys_prefix = ""
7
-
8
- class DataKeys:
9
- pass
10
-
11
- @classmethod
12
- def keys(cls):
13
- class Keys:
14
- pass
15
-
16
- for key in cls.DataKeys.__dict__:
17
- if not key.startswith("_"):
18
- setattr(Keys, key, f"{cls.keys_prefix}_{key}")
19
-
20
- return Keys
1
+ try:
2
+ from importlib_metadata import entry_points
3
+ except ImportError:
4
+ from importlib.metadata import entry_points # type: ignore[assignment]
21
5
 
22
6
 
23
7
  def _load_connector(entrypoint):
24
- connector = entrypoint.load()
25
- if not getattr(connector, "Meta", None):
26
- connector.Meta = BaseConnectorMeta
27
- return connector
8
+ return entrypoint.load()
28
9
 
29
10
 
30
11
  def get_all_connectors():
31
12
  return {
32
13
  entrypoint.name: _load_connector(entrypoint)
33
- for entrypoint in pkg_resources.iter_entry_points("pyinfra.connectors")
14
+ for entrypoint in entry_points(group="pyinfra.connectors")
34
15
  }
35
16
 
36
17
 
@@ -38,7 +19,7 @@ def get_execution_connectors():
38
19
  return {
39
20
  connector: connector_mod
40
21
  for connector, connector_mod in get_all_connectors().items()
41
- if connector_mod.Meta.handles_execution
22
+ if connector_mod.handles_execution
42
23
  }
43
24
 
44
25
 
pyinfra/api/deploy.py CHANGED
@@ -5,31 +5,24 @@ creation (eg pyinfra-openstack).
5
5
  """
6
6
 
7
7
  from functools import wraps
8
- from typing import TYPE_CHECKING, Any, Callable, Union
8
+ from typing import TYPE_CHECKING, Any, Callable, Optional, cast
9
+
10
+ from typing_extensions import ParamSpec
9
11
 
10
12
  import pyinfra
11
- from pyinfra import context, logger
13
+ from pyinfra import context
12
14
  from pyinfra.context import ctx_host, ctx_state
13
15
 
14
16
  from .arguments import pop_global_arguments
17
+ from .arguments_typed import PyinfraOperation
15
18
  from .exceptions import PyinfraError
16
19
  from .host import Host
17
- from .util import get_args_kwargs_spec, get_call_location, memoize
20
+ from .util import get_call_location
18
21
 
19
22
  if TYPE_CHECKING:
20
23
  from pyinfra.api.state import State
21
24
 
22
25
 
23
- @memoize
24
- def show_state_host_arguments_warning(call_location):
25
- logger.warning(
26
- (
27
- "{0}:\n\tLegacy deploy function detected! Deploys should no longer define "
28
- "`state` and `host` arguments."
29
- ).format(call_location),
30
- )
31
-
32
-
33
26
  def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs):
34
27
  """
35
28
  Prepare & add an deploy to pyinfra.state by executing it on all hosts.
@@ -58,61 +51,37 @@ def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs)
58
51
  deploy_func(*args, **kwargs)
59
52
 
60
53
 
61
- def deploy(func_or_name: Union[Callable[..., Any], str], data_defaults=None, _call_location=None):
54
+ P = ParamSpec("P")
55
+
56
+
57
+ def deploy(name: Optional[str] = None, data_defaults=None):
62
58
  """
63
59
  Decorator that takes a deploy function (normally from a pyinfra_* package)
64
60
  and wraps any operations called inside with any deploy-wide kwargs/data.
65
61
  """
66
62
 
67
- # If not decorating, return function with config attached
68
- if isinstance(func_or_name, str):
69
- name = func_or_name
70
-
71
- def decorator(f):
72
- f.deploy_name = name
73
- if data_defaults:
74
- f.deploy_data = data_defaults
75
- return deploy(f, _call_location=get_call_location())
76
-
77
- return decorator
63
+ def decorator(func: Callable[P, Any]) -> PyinfraOperation[P]:
64
+ func.deploy_name = name or func.__name__ # type: ignore[attr-defined]
65
+ if data_defaults:
66
+ func.deploy_data = data_defaults # type: ignore[attr-defined]
67
+ return _wrap_deploy(func)
78
68
 
79
- # Actually decorate!
80
- func = func_or_name
69
+ return decorator
81
70
 
82
- # Check whether an operation is "legacy" - ie contains state=None, host=None kwargs
83
- # TODO: remove this in v3
84
- is_legacy = False
85
- args, kwargs = get_args_kwargs_spec(func)
86
- if all(key in kwargs and kwargs[key] is None for key in ("state", "host")):
87
- show_state_host_arguments_warning(_call_location or get_call_location())
88
- is_legacy = True
89
- func.is_legacy = is_legacy # type: ignore
90
71
 
72
+ def _wrap_deploy(func: Callable[P, Any]) -> PyinfraOperation[P]:
91
73
  @wraps(func)
92
- def decorated_func(*args, **kwargs):
74
+ def decorated_func(*args: P.args, **kwargs: P.kwargs) -> Any:
93
75
  deploy_kwargs, _ = pop_global_arguments(kwargs)
94
76
 
95
- # If this is a legacy operation function (ie - state & host arg kwargs), ensure that state
96
- # and host are included as kwargs.
97
- if func.is_legacy:
98
- if "state" not in kwargs:
99
- kwargs["state"] = context.state
100
- if "host" not in kwargs:
101
- kwargs["host"] = context.host
102
- # If not legacy, pop off any state/host kwargs that may come from legacy @deploy functions
103
- else:
104
- kwargs.pop("state", None)
105
- kwargs.pop("host", None)
106
-
107
- # Name the deploy
108
- deploy_name = getattr(func, "deploy_name", func.__name__)
109
77
  deploy_data = getattr(func, "deploy_data", None)
110
78
 
111
79
  with context.host.deploy(
112
- name=deploy_name,
80
+ name=func.deploy_name, # type: ignore[attr-defined]
113
81
  kwargs=deploy_kwargs,
114
82
  data=deploy_data,
115
83
  ):
116
84
  return func(*args, **kwargs)
117
85
 
118
- return decorated_func
86
+ decorated_func._inner = func # type: ignore[attr-defined]
87
+ return cast(PyinfraOperation[P], decorated_func)