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.
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/CHANGES.rst +31 -0
- {pytest_postgresql-8.0.0/pytest_postgresql.egg-info → pytest_postgresql-8.1.0}/PKG-INFO +2 -2
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pyproject.toml +22 -16
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/__init__.py +1 -1
- pytest_postgresql-8.1.0/pytest_postgresql/executor.py +388 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/factories/process.py +1 -1
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0/pytest_postgresql.egg-info}/PKG-INFO +2 -2
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/SOURCES.txt +2 -1
- pytest_postgresql-8.1.0/tests/test_executor.py +409 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_postgres_options_plugin.py +4 -2
- pytest_postgresql-8.1.0/tests/test_windows_compatibility.py +898 -0
- pytest_postgresql-8.0.0/pytest_postgresql/executor.py +0 -240
- pytest_postgresql-8.0.0/tests/test_executor.py +0 -174
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/AUTHORS.rst +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/CONTRIBUTING.rst +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/COPYING +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/COPYING.lesser +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/MANIFEST.in +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/README.rst +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/config.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/exceptions.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/executor_noop.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/factories/__init__.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/factories/client.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/factories/noprocess.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/janitor.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/loader.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/plugin.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/py.typed +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql/retry.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/dependency_links.txt +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/entry_points.txt +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/requires.txt +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/top_level.txt +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/pytest_postgresql.egg-info/zip-safe +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/setup.cfg +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_chaining.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_config.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_janitor.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_loader.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_noopexecutor.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_postgresql.py +0 -0
- {pytest_postgresql-8.0.0 → pytest_postgresql-8.1.0}/tests/test_template_database.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|
|
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.
|
|
109
|
-
|
|
108
|
+
[[tool.towncrier.type]]
|
|
109
|
+
directory = "break"
|
|
110
|
+
name = "Breaking changes"
|
|
110
111
|
showcontent = true
|
|
111
112
|
|
|
112
|
-
[tool.towncrier.
|
|
113
|
-
|
|
113
|
+
[[tool.towncrier.type]]
|
|
114
|
+
directory = "depr"
|
|
115
|
+
name = "Deprecations"
|
|
114
116
|
showcontent = true
|
|
115
117
|
|
|
116
|
-
[tool.towncrier.
|
|
117
|
-
|
|
118
|
+
[[tool.towncrier.type]]
|
|
119
|
+
directory = "feature"
|
|
120
|
+
name = "Features"
|
|
118
121
|
showcontent = true
|
|
119
122
|
|
|
120
|
-
[tool.towncrier.
|
|
121
|
-
|
|
123
|
+
[[tool.towncrier.type]]
|
|
124
|
+
directory = "bugfix"
|
|
125
|
+
name = "Bugfixes"
|
|
122
126
|
showcontent = true
|
|
123
127
|
|
|
124
|
-
[tool.towncrier.
|
|
125
|
-
|
|
128
|
+
[[tool.towncrier.type]]
|
|
129
|
+
directory = "docs"
|
|
130
|
+
name = "Documentation"
|
|
126
131
|
showcontent = true
|
|
127
132
|
|
|
128
|
-
[tool.towncrier.
|
|
133
|
+
[[tool.towncrier.type]]
|
|
134
|
+
directory = "misc"
|
|
129
135
|
name = "Miscellaneus"
|
|
130
|
-
showcontent =
|
|
136
|
+
showcontent = true
|
|
131
137
|
|
|
132
138
|
[tool.tbump.version]
|
|
133
|
-
current = "8.
|
|
139
|
+
current = "8.1.0"
|
|
134
140
|
|
|
135
141
|
# Example of a semver regexp.
|
|
136
142
|
# Make sure this matches current_version before
|
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|