pytest-postgresql 8.0.0__tar.gz → 8.1.0__tar.gz

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 (44) hide show
  1. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/CHANGES.rst +31 -0
  2. {pytest_postgresql-8.0.0/pytest_postgresql.egg-info → pytest_postgresql-8.1.0}/PKG-INFO +2 -2
  3. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pyproject.toml +22 -16
  4. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/__init__.py +1 -1
  5. pytest_postgresql-8.1.0/pytest_postgresql/executor.py +388 -0
  6. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/factories/process.py +1 -1
  7. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0/pytest_postgresql.egg-info}/PKG-INFO +2 -2
  8. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/SOURCES.txt +2 -1
  9. pytest_postgresql-8.1.0/tests/test_executor.py +409 -0
  10. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_postgres_options_plugin.py +4 -2
  11. pytest_postgresql-8.1.0/tests/test_windows_compatibility.py +898 -0
  12. pytest_postgresql-8.0.0/pytest_postgresql/executor.py +0 -240
  13. pytest_postgresql-8.0.0/tests/test_executor.py +0 -174
  14. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/AUTHORS.rst +0 -0
  15. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/CONTRIBUTING.rst +0 -0
  16. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/COPYING +0 -0
  17. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/COPYING.lesser +0 -0
  18. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/MANIFEST.in +0 -0
  19. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/README.rst +0 -0
  20. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/config.py +0 -0
  21. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/exceptions.py +0 -0
  22. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/executor_noop.py +0 -0
  23. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/factories/__init__.py +0 -0
  24. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/factories/client.py +0 -0
  25. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/factories/noprocess.py +0 -0
  26. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/janitor.py +0 -0
  27. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/loader.py +0 -0
  28. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/plugin.py +0 -0
  29. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/py.typed +0 -0
  30. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/retry.py +0 -0
  31. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/dependency_links.txt +0 -0
  32. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/entry_points.txt +0 -0
  33. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/requires.txt +0 -0
  34. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/top_level.txt +0 -0
  35. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/zip-safe +0 -0
  36. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/setup.cfg +0 -0
  37. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_chaining.py +0 -0
  38. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_config.py +0 -0
  39. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_janitor.py +0 -0
  40. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_loader.py +0 -0
  41. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_noopexecutor.py +0 -0
  42. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_postgresql.py +0 -0
  43. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_template_database.py +0 -0
  44. {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_version.py +0 -0
@@ -3,6 +3,37 @@ CHANGELOG
3
3
 
4
4
  .. towncrier release notes start
5
5
 
6
+ pytest-postgresql 8.1.0 (2026-05-15)
7
+ ====================================
8
+
9
+ Features
10
+ --------
11
+
12
+ - Add Windows support for ``PostgreSQLExecutor``, including platform-specific start/stop handling. (`#1182 <https://github.com/dbfixtures/pytest-postgresql/issues/1182>`__)
13
+
14
+
15
+ Documentation
16
+ -------------
17
+
18
+ - Make sure diagrams are using unified terminology.
19
+
20
+
21
+ Miscellaneus
22
+ ------------
23
+
24
+ - Fix typos (`#1267 <https://github.com/dbfixtures/pytest-postgresql/issues/1267>`__)
25
+ - Add ``pytest-postgresql`` as an editable self-install in the Pipfile; remove the manual ``from pytest_postgresql.plugin import *`` import from ``tests/conftest.py`` and the ``makeconftest`` call in ``tests/test_postgres_options_plugin.py`` that re-registered the plugin and caused duplicate-plugin errors under editable install.
26
+ - Add coderabbitai configuration with instructions to keep an eye for newsfragments.
27
+ - Attempt to overcome random failure on CI on MacOS on xdist workers where test_executor_init_with_password is receiving
28
+ `FATAL: the database system is starting up` error message from psycopg.
29
+
30
+ That's the only test that doesn't have guards against attempting to start on the same port as other xdist worker.
31
+ - Fix codecov pipeline configuration.
32
+ - Fixes for diagram workflow.
33
+ - Improve reliability of Coverage reporting on CI
34
+ - Migrate package publishing step to trusted publishing.
35
+
36
+
6
37
  pytest-postgresql 8.0.0 (2026-01-23)
7
38
  ====================================
8
39
 
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-postgresql
3
- Version: 8.0.0
3
+ Version: 8.1.0
4
4
  Summary: Postgresql fixtures and fixture factories for Pytest.
5
5
  Author-email: Grzegorz Śliwiński <fizyk+pypi@fizyk.dev>
6
6
  Project-URL: Source, https://github.com/dbfixtures/pytest-postgresql
7
7
  Project-URL: Bug Tracker, https://github.com/dbfixtures/pytest-postgresql/issues
8
- Project-URL: Changelog, https://github.com/dbfixtures/pytest-postgresql/blob/v8.0.0/CHANGES.rst
8
+ Project-URL: Changelog, https://github.com/dbfixtures/pytest-postgresql/blob/v8.1.0/CHANGES.rst
9
9
  Keywords: tests,pytest,fixture,postgresql
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Web Environment
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pytest-postgresql"
3
- version = "8.0.0"
3
+ version = "8.1.0"
4
4
  description = "Postgresql fixtures and fixture factories for Pytest."
5
5
  readme = "README.rst"
6
6
  keywords = ["tests", "pytest", "fixture", "postgresql"]
@@ -39,7 +39,7 @@ requires-python = ">= 3.10"
39
39
  [project.urls]
40
40
  "Source" = "https://github.com/dbfixtures/pytest-postgresql"
41
41
  "Bug Tracker" = "https://github.com/dbfixtures/pytest-postgresql/issues"
42
- "Changelog" = "https://github.com/dbfixtures/pytest-postgresql/blob/v8.0.0/CHANGES.rst"
42
+ "Changelog" = "https://github.com/dbfixtures/pytest-postgresql/blob/v8.1.0/CHANGES.rst"
43
43
 
44
44
  [project.entry-points."pytest11"]
45
45
  pytest_postgresql = "pytest_postgresql.plugin"
@@ -58,7 +58,7 @@ namespaces = false
58
58
 
59
59
  [tool.pytest]
60
60
  strict_xfail=true
61
- addopts = ["--showlocals", "--verbose", "--cov"]
61
+ addopts = ["--showlocals", "--verbose"]
62
62
  testpaths = ["tests"]
63
63
  pytester_example_dir = "tests/examples"
64
64
  norecursedirs = ["examples"]
@@ -105,32 +105,38 @@ single_file=true
105
105
  filename="CHANGES.rst"
106
106
  issue_format="`#{issue} <https://github.com/dbfixtures/pytest-postgresql/issues/{issue}>`__"
107
107
 
108
- [tool.towncrier.fragment.feature]
109
- name = "Features"
108
+ [[tool.towncrier.type]]
109
+ directory = "break"
110
+ name = "Breaking changes"
110
111
  showcontent = true
111
112
 
112
- [tool.towncrier.fragment.bugfix]
113
- name = "Bugfixes"
113
+ [[tool.towncrier.type]]
114
+ directory = "depr"
115
+ name = "Deprecations"
114
116
  showcontent = true
115
117
 
116
- [tool.towncrier.fragment.docs]
117
- name = "Documentation"
118
+ [[tool.towncrier.type]]
119
+ directory = "feature"
120
+ name = "Features"
118
121
  showcontent = true
119
122
 
120
- [tool.towncrier.fragment.deprecate]
121
- name = "Deprecations"
123
+ [[tool.towncrier.type]]
124
+ directory = "bugfix"
125
+ name = "Bugfixes"
122
126
  showcontent = true
123
127
 
124
- [tool.towncrier.fragment.break]
125
- name = "Breaking changes"
128
+ [[tool.towncrier.type]]
129
+ directory = "docs"
130
+ name = "Documentation"
126
131
  showcontent = true
127
132
 
128
- [tool.towncrier.fragment.misc]
133
+ [[tool.towncrier.type]]
134
+ directory = "misc"
129
135
  name = "Miscellaneus"
130
- showcontent = false
136
+ showcontent = true
131
137
 
132
138
  [tool.tbump.version]
133
- current = "8.0.0"
139
+ current = "8.1.0"
134
140
 
135
141
  # Example of a semver regexp.
136
142
  # Make sure this matches current_version before
@@ -18,4 +18,4 @@
18
18
  # along with pytest-postgresql. If not, see <http://www.gnu.org/licenses/>.
19
19
  """Main module for pytest-postgresql."""
20
20
 
21
- __version__ = "8.0.0"
21
+ __version__ = "8.1.0"
@@ -0,0 +1,388 @@
1
+ # Copyright (C) 2016-2020 by Clearcode <http://clearcode.cc>
2
+ # and associates (see AUTHORS).
3
+
4
+ # This file is part of pytest-postgresql.
5
+
6
+ # pytest-postgresql is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Lesser General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+
11
+ # pytest-postgresql is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Lesser General Public License for more details.
15
+
16
+ # You should have received a copy of the GNU Lesser General Public License
17
+ # along with pytest-postgresql. If not, see <http://www.gnu.org/licenses/>.
18
+ """PostgreSQL executor crafter around pg_ctl."""
19
+
20
+ import logging
21
+ import os
22
+ import os.path
23
+ import platform
24
+ import re
25
+ import shlex
26
+ import shutil
27
+ import subprocess
28
+ import tempfile
29
+ import time
30
+ from typing import Any, Optional, TypeVar
31
+
32
+ from mirakuru import TCPExecutor
33
+ from mirakuru.exceptions import ProcessFinishedWithError, TimeoutExpired
34
+ from packaging.version import parse
35
+
36
+ from pytest_postgresql.exceptions import ExecutableMissingException, PostgreSQLUnsupported
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ _LOCALE = "C.UTF-8"
41
+
42
+ if platform.system() == "Darwin":
43
+ # Darwin does not have C.UTF-8, but en_US.UTF-8 is always available
44
+ _LOCALE = "en_US.UTF-8"
45
+ elif platform.system() == "Windows":
46
+ # Windows doesn't support C.UTF-8 or en_US.UTF-8, use plain "C" locale
47
+ _LOCALE = "C"
48
+
49
+
50
+ T = TypeVar("T", bound="PostgreSQLExecutor")
51
+
52
+
53
+ class PostgreSQLExecutor(TCPExecutor):
54
+ """PostgreSQL executor running on pg_ctl.
55
+
56
+ Based over an `pg_ctl program
57
+ <http://www.postgresql.org/docs/current/static/app-pg-ctl.html>`_
58
+ """
59
+
60
+ # Unix command template - uses single quotes for PostgreSQL config value quoting
61
+ # which protects paths with spaces in unix_socket_directories.
62
+ # On Unix, mirakuru uses shlex.split() with shell=False, so single quotes
63
+ # inside double-quoted strings are preserved and passed to PostgreSQL's config parser.
64
+ UNIX_PROC_START_COMMAND = (
65
+ '"{executable}" start -D "{datadir}" '
66
+ "-o \"-F -p {port} -c log_destination='stderr' "
67
+ "-c logging_collector=off "
68
+ "-c unix_socket_directories='{unixsocketdir}'{postgres_options}\" "
69
+ '-l "{logfile}" {startparams}'
70
+ )
71
+
72
+ # Windows command template - no single quotes (cmd.exe treats them as literals,
73
+ # not delimiters) and unix_socket_directories is omitted entirely since PostgreSQL
74
+ # ignores it on Windows. The -o payload is produced by _windows_pg_options() so
75
+ # both __init__ and start() always use identical arguments.
76
+ WINDOWS_PROC_START_COMMAND = '"{executable}" start -D "{datadir}" -o "{pg_options}" -l "{logfile}" {startparams}'
77
+
78
+ VERSION_RE = re.compile(r".* (?P<version>\d+(?:\.\d+)?)")
79
+ MIN_SUPPORTED_VERSION = parse("14")
80
+
81
+ @staticmethod
82
+ def _windows_pg_options(port: int, postgres_options: str) -> str:
83
+ """Return the -o argument value for the Windows pg_ctl start invocation.
84
+
85
+ Centralises the construction so __init__ (command string) and start()
86
+ (argv list) always produce identical payloads without risk of drift.
87
+ """
88
+ extra = f" {postgres_options}" if postgres_options else ""
89
+ return f"-F -p {port} -c log_destination=stderr -c logging_collector=off{extra}"
90
+
91
+ def __init__(
92
+ self,
93
+ executable: str,
94
+ host: str,
95
+ port: int,
96
+ datadir: str,
97
+ unixsocketdir: str,
98
+ logfile: str,
99
+ startparams: str,
100
+ dbname: str,
101
+ shell: bool = False,
102
+ timeout: Optional[int] = 60,
103
+ sleep: float = 0.1,
104
+ user: str = "postgres",
105
+ password: str = "",
106
+ options: str = "",
107
+ postgres_options: str = "",
108
+ ):
109
+ """Initialize PostgreSQLExecutor executor.
110
+
111
+ :param executable: pg_ctl location
112
+ :param host: host under which process is accessible
113
+ :param port: port under which process is accessible
114
+ :param datadir: path to postgresql datadir
115
+ :param unixsocketdir: path to socket directory
116
+ :param logfile: path to logfile for postgresql
117
+ :param startparams: additional start parameters
118
+ :param shell: see `subprocess.Popen`
119
+ :param timeout: time to wait for process to start or stop.
120
+ if None, wait indefinitely.
121
+ :param sleep: how often to check for start/stop condition
122
+ :param user: postgresql's username used to manage
123
+ and access PostgreSQL
124
+ :param password: optional password for the user
125
+ :param dbname: database name (might not yet exist)
126
+ :param options:
127
+ :param postgres_options: extra arguments to `postgres start`
128
+ """
129
+ self._directory_initialised = False
130
+ self.executable = executable
131
+ self.user = user
132
+ self.password = password
133
+ self.dbname = dbname
134
+ self.options = options
135
+ self.datadir = datadir
136
+ self.unixsocketdir = unixsocketdir
137
+ self.logfile = logfile
138
+ self.startparams = startparams
139
+ self.postgres_options = postgres_options
140
+ if platform.system() == "Windows":
141
+ command = self.WINDOWS_PROC_START_COMMAND.format(
142
+ executable=self.executable,
143
+ datadir=self.datadir,
144
+ pg_options=PostgreSQLExecutor._windows_pg_options(port, self.postgres_options),
145
+ logfile=self.logfile,
146
+ startparams=self.startparams,
147
+ )
148
+ else:
149
+ # PostgreSQL GUC single-quoted strings double single-quotes to escape them
150
+ # (e.g. /tmp/o'hare → /tmp/o''hare). Apply this before interpolation so
151
+ # the generated unix_socket_directories value is always syntactically valid.
152
+ escaped_unixsocketdir = self.unixsocketdir.replace("'", "''")
153
+ command = self.UNIX_PROC_START_COMMAND.format(
154
+ executable=self.executable,
155
+ datadir=self.datadir,
156
+ port=port,
157
+ unixsocketdir=escaped_unixsocketdir,
158
+ logfile=self.logfile,
159
+ startparams=self.startparams,
160
+ postgres_options=f" {self.postgres_options}" if self.postgres_options else "",
161
+ )
162
+ super().__init__(
163
+ command,
164
+ host,
165
+ port,
166
+ shell=shell,
167
+ timeout=timeout,
168
+ sleep=sleep,
169
+ envvars={
170
+ "LC_ALL": _LOCALE,
171
+ "LC_CTYPE": _LOCALE,
172
+ "LANG": _LOCALE,
173
+ },
174
+ )
175
+
176
+ @property
177
+ def template_dbname(self) -> str:
178
+ """Return the template database name."""
179
+ return f"{self.dbname}_tmpl"
180
+
181
+ def start(self: T) -> T:
182
+ """Add check for postgresql version before starting process."""
183
+ if self.version < self.MIN_SUPPORTED_VERSION:
184
+ raise PostgreSQLUnsupported(
185
+ f"Your version of PostgreSQL is not supported. "
186
+ f"Consider updating to PostgreSQL {self.MIN_SUPPORTED_VERSION} at least. "
187
+ f"The currently installed version of PostgreSQL: {self.version}."
188
+ )
189
+ self.init_directory()
190
+ if platform.system() == "Windows":
191
+ # On Windows, pg_ctl start -w exits as soon as the server is
192
+ # accepting connections. mirakuru's polling loop calls
193
+ # check_subprocess() (our pg_ctl-status override) repeatedly
194
+ # while waiting for the launcher subprocess to indicate readiness,
195
+ # but by the time polling begins the launcher has already exited
196
+ # so the loop never sees a live process and times out.
197
+ # Run pg_ctl synchronously as an argv list (no shell) so quoting
198
+ # is handled safely by subprocess, mirroring the stop() approach.
199
+ pg_options = self._windows_pg_options(self.port, self.postgres_options)
200
+ args = [
201
+ self.executable,
202
+ "start",
203
+ "-D",
204
+ self.datadir,
205
+ "-o",
206
+ pg_options,
207
+ "-l",
208
+ self.logfile,
209
+ *shlex.split(self.startparams, posix=False),
210
+ ]
211
+ merged_env = os.environ.copy()
212
+ merged_env.update(self.envvars)
213
+ try:
214
+ result = subprocess.run(args, check=False, env=merged_env, timeout=self._timeout)
215
+ except subprocess.TimeoutExpired as exc:
216
+ # subprocess.run already killed the stuck pg_ctl process before
217
+ # re-raising, so no additional cleanup is required here.
218
+ raise TimeoutExpired(self, self._timeout) from exc
219
+ if result.returncode != 0:
220
+ raise ProcessFinishedWithError(self, result.returncode)
221
+ # pg_ctl start without -w returns before the server accepts connections.
222
+ # wait_for_postgres() polls self.running() when -w is absent; when -w is
223
+ # present pg_ctl has already waited so it returns immediately.
224
+ self.wait_for_postgres()
225
+ return self
226
+ return super().start()
227
+
228
+ def clean_directory(self) -> None:
229
+ """Remove directory created for postgresql run."""
230
+ if os.path.isdir(self.datadir):
231
+ shutil.rmtree(self.datadir)
232
+ self._directory_initialised = False
233
+
234
+ def init_directory(self) -> None:
235
+ """Initialize postgresql data directory.
236
+
237
+ See `Initialize postgresql data directory
238
+ <www.postgresql.org/docs/9.5/static/app-initdb.html>`_
239
+ """
240
+ # only make sure it's removed if it's handled by this exact process
241
+ if self._directory_initialised:
242
+ return
243
+ # remove old one if exists first.
244
+ self.clean_directory()
245
+ init_directory = [self.executable, "initdb", "--pgdata", self.datadir]
246
+ options = ["--username=%s" % self.user]
247
+
248
+ if self.password:
249
+ with tempfile.NamedTemporaryFile() as password_file:
250
+ options += ["--auth=password", "--pwfile=%s" % password_file.name]
251
+ if hasattr(self.password, "encode"):
252
+ password = self.password.encode("utf-8")
253
+ else:
254
+ password = self.password # type: ignore[assignment]
255
+ password_file.write(password)
256
+ password_file.flush()
257
+ init_directory += ["-o", " ".join(options)]
258
+ # Passing envvars to command to avoid weird MacOs error.
259
+ subprocess.check_output(init_directory, env=self.envvars)
260
+ else:
261
+ options += ["--auth=trust"]
262
+ init_directory += ["-o", " ".join(options)]
263
+ # Passing envvars to command to avoid weird MacOs error.
264
+ subprocess.check_output(init_directory, env=self.envvars)
265
+
266
+ self._directory_initialised = True
267
+
268
+ def wait_for_postgres(self) -> None:
269
+ """Wait for postgresql being started."""
270
+ if "-w" not in self.startparams:
271
+ return
272
+ # wait until server is running
273
+ while 1:
274
+ if self.running():
275
+ break
276
+ time.sleep(1)
277
+
278
+ @property
279
+ def version(self) -> Any:
280
+ """Detect postgresql version."""
281
+ try:
282
+ version_string = subprocess.check_output([self.executable, "--version"]).decode("utf-8")
283
+ except FileNotFoundError as ex:
284
+ raise ExecutableMissingException(
285
+ f"Could not find {self.executable}. Is PostgreSQL server installed? "
286
+ f"Alternatively pg_config installed might be from different "
287
+ f"version that postgresql-server."
288
+ ) from ex
289
+ matches = self.VERSION_RE.search(version_string)
290
+ assert matches is not None
291
+ return parse(matches.groupdict()["version"])
292
+
293
+ def running(self) -> bool:
294
+ """Check if server is running."""
295
+ if not os.path.exists(self.datadir):
296
+ return False
297
+ result = subprocess.run(
298
+ [self.executable, "status", "-D", self.datadir],
299
+ check=False,
300
+ capture_output=True,
301
+ )
302
+ return result.returncode == 0
303
+
304
+ def check_subprocess(self) -> bool:
305
+ """Check whether the PostgreSQL server is ready.
306
+
307
+ On Windows the launcher (pg_ctl start -w) is invoked synchronously
308
+ in start(), so mirakuru's polling loop is never reached and this
309
+ method is called only from running()-style checks. Returning
310
+ self.running() (pg_ctl status) is appropriate there.
311
+
312
+ On non-Windows, mirakuru's TCPExecutor.check_subprocess() is the
313
+ correct check: it verifies both that the launcher subprocess is still
314
+ alive (enabling fast ProcessFinishedWithError on pg_ctl failures) and
315
+ that the TCP port is accepting connections. Delegating to super()
316
+ preserves that failure-detection behaviour.
317
+ """
318
+ if platform.system() == "Windows":
319
+ return self.running()
320
+ return super().check_subprocess()
321
+
322
+ def _windows_terminate_process(self, _sig: Optional[int] = None) -> None:
323
+ """Terminate process on Windows.
324
+
325
+ :param _sig: Signal parameter (unused on Windows but included for consistency)
326
+ """
327
+ if self.process is None:
328
+ return
329
+
330
+ try:
331
+ # On Windows, try to terminate gracefully first
332
+ self.process.terminate()
333
+ # Give it a chance to terminate gracefully
334
+ try:
335
+ self.process.wait(timeout=5)
336
+ except subprocess.TimeoutExpired:
337
+ # If it doesn't terminate gracefully, force kill
338
+ self.process.kill()
339
+ try:
340
+ self.process.wait(timeout=10)
341
+ except subprocess.TimeoutExpired:
342
+ logger.warning(
343
+ "Process %s could not be cleaned up after kill() and may be a zombie process",
344
+ self.process.pid if self.process else "unknown",
345
+ )
346
+ except (OSError, AttributeError) as e:
347
+ # Process might already be dead or other issues
348
+ logger.debug(
349
+ "Exception during Windows process termination: %s: %s",
350
+ type(e).__name__,
351
+ e,
352
+ )
353
+
354
+ def stop(self: T, sig: Optional[int] = None, exp_sig: Optional[int] = None) -> T:
355
+ """Issue a stop request to executable."""
356
+ try:
357
+ subprocess.check_output(
358
+ [self.executable, "stop", "-D", self.datadir, "-m", "f"],
359
+ timeout=self._timeout,
360
+ )
361
+ except subprocess.TimeoutExpired as exc:
362
+ raise TimeoutExpired(self, self._timeout) from exc
363
+ try:
364
+ if platform.system() == "Windows":
365
+ self._windows_terminate_process(sig)
366
+ # Clean up mirakuru's internal process reference so that
367
+ # the stopped() context manager can call start() again
368
+ self._process = None
369
+ else:
370
+ super().stop(sig, exp_sig)
371
+ except ProcessFinishedWithError:
372
+ # Finished, leftovers ought to be cleaned afterwards anyway
373
+ pass
374
+ except AttributeError:
375
+ # Fallback for edge cases where os.killpg doesn't exist (e.g., Windows)
376
+ if not hasattr(os, "killpg"):
377
+ self._windows_terminate_process(sig)
378
+ self._process = None
379
+ else:
380
+ raise
381
+ return self
382
+
383
+ def __del__(self) -> None:
384
+ """Make sure the directories are properly removed at the end."""
385
+ try:
386
+ super().__del__()
387
+ finally:
388
+ self.clean_directory()
@@ -45,7 +45,7 @@ def _pg_exe(executable: str | None, config: PostgreSQLConfig) -> str:
45
45
  try:
46
46
  pg_bindir = subprocess.check_output(["pg_config", "--bindir"], universal_newlines=True).strip()
47
47
  except FileNotFoundError as ex:
48
- raise ExecutableMissingException("Could not found pg_config executable. Is it in systenm $PATH?") from ex
48
+ raise ExecutableMissingException("Could not find pg_config executable. Is it in system $PATH?") from ex
49
49
  postgresql_ctl = os.path.join(pg_bindir, "pg_ctl")
50
50
  return postgresql_ctl
51
51
 
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-postgresql
3
- Version: 8.0.0
3
+ Version: 8.1.0
4
4
  Summary: Postgresql fixtures and fixture factories for Pytest.
5
5
  Author-email: Grzegorz Śliwiński <fizyk+pypi@fizyk.dev>
6
6
  Project-URL: Source, https://github.com/dbfixtures/pytest-postgresql
7
7
  Project-URL: Bug Tracker, https://github.com/dbfixtures/pytest-postgresql/issues
8
- Project-URL: Changelog, https://github.com/dbfixtures/pytest-postgresql/blob/v8.0.0/CHANGES.rst
8
+ Project-URL: Changelog, https://github.com/dbfixtures/pytest-postgresql/blob/v8.1.0/CHANGES.rst
9
9
  Keywords: tests,pytest,fixture,postgresql
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Web Environment
@@ -36,4 +36,5 @@ tests/test_noopexecutor.py
36
36
  tests/test_postgres_options_plugin.py
37
37
  tests/test_postgresql.py
38
38
  tests/test_template_database.py
39
- tests/test_version.py
39
+ tests/test_version.py
40
+ tests/test_windows_compatibility.py