plain.pytest 0.0.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.
@@ -0,0 +1,58 @@
1
+ ## Plain is released under the BSD 3-Clause License
2
+
3
+ BSD 3-Clause License
4
+
5
+ Copyright (c) 2023, Dropseed, LLC
6
+
7
+ Redistribution and use in source and binary forms, with or without
8
+ modification, are permitted provided that the following conditions are met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright notice, this
11
+ list of conditions and the following disclaimer.
12
+
13
+ 2. Redistributions in binary form must reproduce the above copyright notice,
14
+ this list of conditions and the following disclaimer in the documentation
15
+ and/or other materials provided with the distribution.
16
+
17
+ 3. Neither the name of the copyright holder nor the names of its
18
+ contributors may be used to endorse or promote products derived from
19
+ this software without specific prior written permission.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+
32
+
33
+ ## This package contains code forked from github.com/pytest-dev/pytest-django
34
+
35
+ Copyright (c) 2015-2018, pytest-django authors (see AUTHORS file)
36
+ All rights reserved.
37
+
38
+ Redistribution and use in source and binary forms, with or without
39
+ modification, are permitted provided that the following conditions are met:
40
+
41
+ * Redistributions of source code must retain the above copyright notice, this
42
+ list of conditions and the following disclaimer.
43
+ * Redistributions in binary form must reproduce the above copyright notice,
44
+ this list of conditions and the following disclaimer in the documentation
45
+ and/or other materials provided with the distribution.
46
+ * The names of its contributors may not be used to endorse or promote products
47
+ derived from this software without specific prior written permission.
48
+
49
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
50
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
51
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
52
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
53
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
54
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
55
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
56
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
57
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
58
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.1
2
+ Name: plain.pytest
3
+ Version: 0.0.0
4
+ Summary: Testing for Plain
5
+ Home-page: https://www.plainpackages.com/
6
+ License: MIT
7
+ Keywords: django,saas,plain,framework
8
+ Author: Dave Gaeddert
9
+ Author-email: dave.gaeddert@dropseed.dev
10
+ Requires-Python: >=3.8,<4.0
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Framework :: Django
13
+ Classifier: Framework :: Django :: 4
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Requires-Dist: click (>=8.0.0)
24
+ Requires-Dist: pytest (>=7.0.0,<8.0.0)
25
+ Project-URL: Documentation, https://www.plainpackages.com/docs/
26
+ Project-URL: Repository, https://github.com/plainpackages/plain-pytest
27
+ Description-Content-Type: text/markdown
28
+
29
+ <!-- This file is compiled from plain-pytest/plain/pytest/README.md. Do not edit this file directly. -->
30
+
31
+ ## Testing - pytest
32
+
33
+ Write and run tests with pytest.
34
+
35
+ Django includes its own test runner and [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) classes.
36
+ But a lot of people (myself included) prefer [pytest](https://docs.pytest.org/en/latest/contents.html).
37
+
38
+ In Plain I've removed the Django test runner and a lot of the implications that come with it.
39
+ There are a few utilities that remain to make testing easier,
40
+ and `plain-test` is a wrapper around `pytest`.
41
+
@@ -0,0 +1,12 @@
1
+ <!-- This file is compiled from plain-pytest/plain/pytest/README.md. Do not edit this file directly. -->
2
+
3
+ ## Testing - pytest
4
+
5
+ Write and run tests with pytest.
6
+
7
+ Django includes its own test runner and [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) classes.
8
+ But a lot of people (myself included) prefer [pytest](https://docs.pytest.org/en/latest/contents.html).
9
+
10
+ In Plain I've removed the Django test runner and a lot of the implications that come with it.
11
+ There are a few utilities that remain to make testing easier,
12
+ and `plain-test` is a wrapper around `pytest`.
@@ -0,0 +1,10 @@
1
+ ## Testing - pytest
2
+
3
+ Write and run tests with pytest.
4
+
5
+ Django includes its own test runner and [unittest](https://docs.python.org/3/library/unittest.html#module-unittest) classes.
6
+ But a lot of people (myself included) prefer [pytest](https://docs.pytest.org/en/latest/contents.html).
7
+
8
+ In Plain I've removed the Django test runner and a lot of the implications that come with it.
9
+ There are a few utilities that remain to make testing easier,
10
+ and `plain-test` is a wrapper around `pytest`.
@@ -0,0 +1,3 @@
1
+ from .cli import cli
2
+
3
+ __all__ = ["cli"]
@@ -0,0 +1,48 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+
5
+ import click
6
+
7
+ from plain.runtime import settings
8
+
9
+
10
+ @click.command(
11
+ context_settings={
12
+ "ignore_unknown_options": True,
13
+ }
14
+ )
15
+ @click.argument("pytest_args", nargs=-1, type=click.UNPROCESSED)
16
+ def cli(pytest_args):
17
+ """Run tests with pytest"""
18
+
19
+ plain_tmp_dir = str(settings.PLAIN_TEMP_PATH)
20
+
21
+ if not os.path.exists(os.path.join(plain_tmp_dir, ".gitignore")):
22
+ os.makedirs(plain_tmp_dir, exist_ok=True)
23
+ with open(os.path.join(plain_tmp_dir, ".gitignore"), "w") as f:
24
+ f.write("*\n")
25
+
26
+ # Turn deprecation warnings into errors
27
+ # if "-W" not in pytest_args:
28
+ # pytest_args = list(pytest_args) # Make sure it's a list instead of tuple
29
+ # pytest_args.append("-W")
30
+ # pytest_args.append("error::DeprecationWarning")
31
+
32
+ os.environ.setdefault("PLAIN_ENV", "test")
33
+
34
+ click.secho(f"Running pytest with PLAIN_ENV={os.environ['PLAIN_ENV']}", bold=True)
35
+
36
+ result = subprocess.run(
37
+ [
38
+ "pytest",
39
+ *pytest_args,
40
+ ],
41
+ env={
42
+ **os.environ,
43
+ },
44
+ )
45
+
46
+ if result.returncode:
47
+ # Can be invoked by pre-commit, so only exit if it fails
48
+ sys.exit(result.returncode)
File without changes
@@ -0,0 +1,335 @@
1
+ """All pytest-plain fixtures"""
2
+ from collections.abc import Generator, Iterable
3
+ from contextlib import contextmanager
4
+ from functools import partial
5
+ from typing import Any, Optional, Union
6
+
7
+ import pytest
8
+
9
+ from ..testcases import TestCase, TransactionTestCase
10
+
11
+ TYPE_CHECKING = False
12
+ if TYPE_CHECKING:
13
+ from typing import Literal
14
+
15
+ import plain.runtime
16
+
17
+ _DjangoDbDatabases = Optional[Union["Literal['__all__']", Iterable[str]]]
18
+ # transaction, reset_sequences, databases, serialized_rollback
19
+ _DjangoDb = tuple[bool, bool, _DjangoDbDatabases, bool]
20
+
21
+
22
+ __all__ = [
23
+ "plain_db_setup",
24
+ "db",
25
+ "transactional_db",
26
+ "plain_db_reset_sequences",
27
+ "plain_db_serialized_rollback",
28
+ "client",
29
+ "rf",
30
+ "settings",
31
+ "plain_assert_num_queries",
32
+ "plain_assert_max_num_queries",
33
+ ]
34
+
35
+
36
+ @pytest.fixture(scope="session")
37
+ def plain_db_setup(
38
+ request,
39
+ plain_test_environment: None,
40
+ plain_db_blocker,
41
+ ) -> None:
42
+ """Top level fixture to ensure test databases are available"""
43
+ from plain.test.utils import setup_databases, teardown_databases
44
+
45
+ setup_databases_args = {}
46
+
47
+ if request.config.getvalue("reuse_db") and not request.config.getvalue("create_db"):
48
+ setup_databases_args["keepdb"] = True
49
+
50
+ with plain_db_blocker.unblock():
51
+ db_cfg = setup_databases(
52
+ verbosity=request.config.option.verbose,
53
+ interactive=False,
54
+ **setup_databases_args,
55
+ )
56
+
57
+ def teardown_database() -> None:
58
+ with plain_db_blocker.unblock():
59
+ try:
60
+ teardown_databases(db_cfg, verbosity=request.config.option.verbose)
61
+ except Exception as exc:
62
+ request.node.warn(
63
+ pytest.PytestWarning(
64
+ f"Error when trying to teardown test databases: {exc!r}"
65
+ )
66
+ )
67
+
68
+ if not request.config.getvalue("reuse_db"):
69
+ request.addfinalizer(teardown_database)
70
+
71
+
72
+ @pytest.fixture()
73
+ def _plain_db_helper(
74
+ request,
75
+ plain_db_setup: None,
76
+ plain_db_blocker,
77
+ ) -> None:
78
+ marker = request.node.get_closest_marker("plain_db")
79
+ if marker:
80
+ (
81
+ transactional,
82
+ reset_sequences,
83
+ databases,
84
+ serialized_rollback,
85
+ ) = validate_plain_db(marker)
86
+ else:
87
+ (
88
+ transactional,
89
+ reset_sequences,
90
+ databases,
91
+ serialized_rollback,
92
+ ) = (
93
+ False,
94
+ False,
95
+ None,
96
+ False,
97
+ )
98
+
99
+ transactional = (
100
+ transactional or reset_sequences or ("transactional_db" in request.fixturenames)
101
+ )
102
+ reset_sequences = reset_sequences or (
103
+ "plain_db_reset_sequences" in request.fixturenames
104
+ )
105
+ serialized_rollback = serialized_rollback or (
106
+ "plain_db_serialized_rollback" in request.fixturenames
107
+ )
108
+
109
+ plain_db_blocker.unblock()
110
+ request.addfinalizer(plain_db_blocker.restore)
111
+
112
+ if transactional:
113
+ test_case_class = TransactionTestCase
114
+ else:
115
+ test_case_class = TestCase
116
+
117
+ _reset_sequences = reset_sequences
118
+ _serialized_rollback = serialized_rollback
119
+ _databases = databases
120
+
121
+ class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type]
122
+ reset_sequences = _reset_sequences
123
+ serialized_rollback = _serialized_rollback
124
+ if _databases is not None:
125
+ databases = _databases
126
+
127
+ PytestDjangoTestCase.setUpClass()
128
+
129
+ request.addfinalizer(PytestDjangoTestCase.doClassCleanups)
130
+ request.addfinalizer(PytestDjangoTestCase.tearDownClass)
131
+
132
+ test_case = PytestDjangoTestCase(methodName="__init__")
133
+ test_case._pre_setup()
134
+ request.addfinalizer(test_case._post_teardown)
135
+
136
+
137
+ def validate_plain_db(marker) -> "_DjangoDb":
138
+ """Validate the plain_db marker.
139
+
140
+ It checks the signature and creates the ``transaction``,
141
+ ``reset_sequences``, ``databases`` and ``serialized_rollback`` attributes on
142
+ the marker which will have the correct values.
143
+
144
+ Sequence reset and serialized_rollback are only allowed when combined with
145
+ transaction.
146
+ """
147
+
148
+ def apifun(
149
+ transaction: bool = False,
150
+ reset_sequences: bool = False,
151
+ databases: "_DjangoDbDatabases" = None,
152
+ serialized_rollback: bool = False,
153
+ ) -> "_DjangoDb":
154
+ return transaction, reset_sequences, databases, serialized_rollback
155
+
156
+ return apifun(*marker.args, **marker.kwargs)
157
+
158
+
159
+ # ############### User visible fixtures ################
160
+
161
+
162
+ @pytest.fixture()
163
+ def db(_plain_db_helper: None) -> None:
164
+ """Require a plain test database.
165
+
166
+ This database will be setup with the default fixtures and will have
167
+ the transaction management disabled. At the end of the test the outer
168
+ transaction that wraps the test itself will be rolled back to undo any
169
+ changes to the database (in case the backend supports transactions).
170
+ This is more limited than the ``transactional_db`` fixture but
171
+ faster.
172
+
173
+ If both ``db`` and ``transactional_db`` are requested,
174
+ ``transactional_db`` takes precedence.
175
+ """
176
+ # The `_plain_db_helper` fixture checks if `db` is requested.
177
+
178
+
179
+ @pytest.fixture()
180
+ def transactional_db(_plain_db_helper: None) -> None:
181
+ """Require a plain test database with transaction support.
182
+
183
+ This will re-initialise the plain database for each test and is
184
+ thus slower than the normal ``db`` fixture.
185
+
186
+ If you want to use the database with transactions you must request
187
+ this resource.
188
+
189
+ If both ``db`` and ``transactional_db`` are requested,
190
+ ``transactional_db`` takes precedence.
191
+ """
192
+ # The `_plain_db_helper` fixture checks if `transactional_db` is requested.
193
+
194
+
195
+ @pytest.fixture()
196
+ def plain_db_reset_sequences(
197
+ _plain_db_helper: None,
198
+ transactional_db: None,
199
+ ) -> None:
200
+ """Require a transactional test database with sequence reset support.
201
+
202
+ This requests the ``transactional_db`` fixture, and additionally
203
+ enforces a reset of all auto increment sequences. If the enquiring
204
+ test relies on such values (e.g. ids as primary keys), you should
205
+ request this resource to ensure they are consistent across tests.
206
+ """
207
+ # The `_plain_db_helper` fixture checks if `plain_db_reset_sequences`
208
+ # is requested.
209
+
210
+
211
+ @pytest.fixture()
212
+ def plain_db_serialized_rollback(
213
+ _plain_db_helper: None,
214
+ db: None,
215
+ ) -> None:
216
+ """Require a test database with serialized rollbacks.
217
+
218
+ This requests the ``db`` fixture, and additionally performs rollback
219
+ emulation - serializes the database contents during setup and restores
220
+ it during teardown.
221
+
222
+ This fixture may be useful for transactional tests, so is usually combined
223
+ with ``transactional_db``, but can also be useful on databases which do not
224
+ support transactions.
225
+
226
+ Note that this will slow down that test suite by approximately 3x.
227
+ """
228
+ # The `_plain_db_helper` fixture checks if `plain_db_serialized_rollback`
229
+ # is requested.
230
+
231
+
232
+ @pytest.fixture()
233
+ def client() -> "plain.test.client.Client":
234
+ """A Plain test client instance."""
235
+ from plain.test.client import Client
236
+
237
+ return Client()
238
+
239
+
240
+ @pytest.fixture()
241
+ def rf() -> "plain.test.client.RequestFactory":
242
+ """RequestFactory instance"""
243
+ from plain.test.client import RequestFactory
244
+
245
+ return RequestFactory()
246
+
247
+
248
+ class SettingsWrapper:
249
+ _to_restore: list[Any] = []
250
+
251
+ def __delattr__(self, attr: str) -> None:
252
+ from plain.test import override_settings
253
+
254
+ override = override_settings()
255
+ override.enable()
256
+ from plain.runtime import settings
257
+
258
+ delattr(settings, attr)
259
+
260
+ self._to_restore.append(override)
261
+
262
+ def __setattr__(self, attr: str, value) -> None:
263
+ from plain.test import override_settings
264
+
265
+ override = override_settings(**{attr: value})
266
+ override.enable()
267
+ self._to_restore.append(override)
268
+
269
+ def __getattr__(self, attr: str):
270
+ from plain.runtime import settings
271
+
272
+ return getattr(settings, attr)
273
+
274
+ def finalize(self) -> None:
275
+ for override in reversed(self._to_restore):
276
+ override.disable()
277
+
278
+ del self._to_restore[:]
279
+
280
+
281
+ @pytest.fixture()
282
+ def settings():
283
+ """A Plain settings object which restores changes after the testrun"""
284
+ wrapper = SettingsWrapper()
285
+ yield wrapper
286
+ wrapper.finalize()
287
+
288
+
289
+ @contextmanager
290
+ def _assert_num_queries(
291
+ config,
292
+ num: int,
293
+ exact: bool = True,
294
+ connection=None,
295
+ info=None,
296
+ ) -> Generator["plain.test.utils.CaptureQueriesContext", None, None]:
297
+ from plain.test.utils import CaptureQueriesContext
298
+
299
+ if connection is None:
300
+ from plain.models import connection as conn
301
+ else:
302
+ conn = connection
303
+
304
+ verbose = config.getoption("verbose") > 0
305
+ with CaptureQueriesContext(conn) as context:
306
+ yield context
307
+ num_performed = len(context)
308
+ if exact:
309
+ failed = num != num_performed
310
+ else:
311
+ failed = num_performed > num
312
+ if failed:
313
+ msg = f"Expected to perform {num} queries "
314
+ if not exact:
315
+ msg += "or less "
316
+ verb = "was" if num_performed == 1 else "were"
317
+ msg += f"but {num_performed} {verb} done"
318
+ if info:
319
+ msg += f"\n{info}"
320
+ if verbose:
321
+ sqls = (q["sql"] for q in context.captured_queries)
322
+ msg += "\n\nQueries:\n========\n\n" + "\n\n".join(sqls)
323
+ else:
324
+ msg += " (add -v option to show queries)"
325
+ pytest.fail(msg)
326
+
327
+
328
+ @pytest.fixture()
329
+ def plain_assert_num_queries(pytestconfig):
330
+ return partial(_assert_num_queries, pytestconfig)
331
+
332
+
333
+ @pytest.fixture()
334
+ def plain_assert_max_num_queries(pytestconfig):
335
+ return partial(_assert_num_queries, pytestconfig, exact=False)
@@ -0,0 +1,339 @@
1
+ """A pytest plugin which helps testing Plain applications
2
+
3
+ This plugin handles creating and destroying the test environment and
4
+ test database and provides some useful text fixtures.
5
+ """
6
+
7
+
8
+ import pytest
9
+
10
+ from .fixtures import (
11
+ _plain_db_helper, # noqa
12
+ plain_assert_max_num_queries, # noqa
13
+ plain_assert_num_queries, # noqa
14
+ plain_db_reset_sequences, # noqa
15
+ plain_db_serialized_rollback, # noqa
16
+ plain_db_setup, # noqa
17
+ client, # noqa
18
+ db, # noqa
19
+ rf, # noqa
20
+ settings, # noqa
21
+ transactional_db, # noqa
22
+ validate_plain_db,
23
+ )
24
+
25
+ TYPE_CHECKING = False
26
+ if TYPE_CHECKING:
27
+ from typing import ContextManager, NoReturn
28
+
29
+ import plain.runtime
30
+
31
+
32
+ # ############### pytest hooks ################
33
+
34
+
35
+ @pytest.hookimpl()
36
+ def pytest_addoption(parser) -> None:
37
+ group = parser.getgroup("plain")
38
+ group.addoption(
39
+ "--reuse-db",
40
+ action="store_true",
41
+ dest="reuse_db",
42
+ default=False,
43
+ help="Re-use the testing database if it already exists, "
44
+ "and do not remove it when the test finishes.",
45
+ )
46
+ group.addoption(
47
+ "--create-db",
48
+ action="store_true",
49
+ dest="create_db",
50
+ default=False,
51
+ help="Re-create the database, even if it exists. This "
52
+ "option can be used to override --reuse-db.",
53
+ )
54
+
55
+ parser.addini(
56
+ "plain_debug_mode",
57
+ "How to set the Plain DEBUG setting (default `False`). "
58
+ "Use `keep` to not override.",
59
+ default="False",
60
+ )
61
+
62
+
63
+ def _setup_plain() -> None:
64
+ import plain.runtime
65
+
66
+ plain.runtime.setup()
67
+
68
+ _blocking_manager.block()
69
+
70
+
71
+ def _get_boolean_value(
72
+ x: None | bool | str,
73
+ name: str,
74
+ default: bool | None = None,
75
+ ) -> bool:
76
+ if x is None:
77
+ return bool(default)
78
+ if isinstance(x, bool):
79
+ return x
80
+ possible_values = {"true": True, "false": False, "1": True, "0": False}
81
+ try:
82
+ return possible_values[x.lower()]
83
+ except KeyError:
84
+ possible = ", ".join(possible_values)
85
+ raise ValueError(
86
+ f"{x} is not a valid value for {name}. It must be one of {possible}."
87
+ )
88
+
89
+
90
+ @pytest.hookimpl()
91
+ def pytest_load_initial_conftests(
92
+ early_config,
93
+ parser,
94
+ args: list[str],
95
+ ) -> None:
96
+ # Register the marks
97
+ early_config.addinivalue_line(
98
+ "markers",
99
+ "plain_db(transaction=False, reset_sequences=False, databases=None, "
100
+ "serialized_rollback=False): "
101
+ "Mark the test as using the Plain test database. "
102
+ "The *transaction* argument allows you to use real transactions "
103
+ "in the test like Plain's TransactionTestCase. "
104
+ "The *reset_sequences* argument resets database sequences before "
105
+ "the test. "
106
+ "The *databases* argument sets which database aliases the test "
107
+ "uses (by default, only 'default'). Use '__all__' for all databases. "
108
+ "The *serialized_rollback* argument enables rollback emulation for "
109
+ "the test.",
110
+ )
111
+ early_config.addinivalue_line(
112
+ "markers",
113
+ "urls(modstr): Use a different URLconf for this test, similar to "
114
+ "the `urls` attribute of Plain's `TestCase` objects. *modstr* is "
115
+ "a string specifying the module of a URL config, e.g. "
116
+ '"my_app.test_urls".',
117
+ )
118
+ early_config.addinivalue_line(
119
+ "markers",
120
+ "ignore_template_errors(): ignore errors from invalid template "
121
+ "variables (if --fail-on-template-vars is used).",
122
+ )
123
+
124
+ options = parser.parse_known_args(args)
125
+
126
+ if options.version or options.help:
127
+ return
128
+
129
+ _setup_plain()
130
+
131
+
132
+ @pytest.hookimpl(trylast=True)
133
+ def pytest_configure() -> None:
134
+ # Allow Plain settings to be configured in a user pytest_configure call,
135
+ # but make sure we call plain.setup()
136
+ _setup_plain()
137
+
138
+
139
+ @pytest.hookimpl(tryfirst=True)
140
+ def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
141
+ def get_order_number(test: pytest.Item) -> int:
142
+ marker_db = test.get_closest_marker("plain_db")
143
+ if marker_db:
144
+ (
145
+ transaction,
146
+ reset_sequences,
147
+ databases,
148
+ serialized_rollback,
149
+ ) = validate_plain_db(marker_db)
150
+ uses_db = True
151
+ transactional = transaction or reset_sequences
152
+ else:
153
+ uses_db = False
154
+ transactional = False
155
+ fixtures = getattr(test, "fixturenames", [])
156
+ transactional = transactional or "transactional_db" in fixtures
157
+ uses_db = uses_db or "db" in fixtures
158
+
159
+ if transactional:
160
+ return 1
161
+ elif uses_db:
162
+ return 0
163
+ else:
164
+ return 2
165
+
166
+ items.sort(key=get_order_number)
167
+
168
+
169
+ @pytest.fixture(autouse=True, scope="session")
170
+ def plain_test_environment(request) -> None:
171
+ """
172
+ Ensure that Plain is loaded and has its testing environment setup.
173
+
174
+ XXX It is a little dodgy that this is an autouse fixture. Perhaps
175
+ an email fixture should be requested in order to be able to
176
+ use the Plain email machinery just like you need to request a
177
+ db fixture for access to the Plain database, etc. But
178
+ without duplicating a lot more of Plain's test support code
179
+ we need to follow this model.
180
+ """
181
+ _setup_plain()
182
+ from plain.test.utils import (
183
+ setup_test_environment,
184
+ teardown_test_environment,
185
+ )
186
+
187
+ debug_ini = request.config.getini("plain_debug_mode")
188
+ if debug_ini == "keep":
189
+ debug = None
190
+ else:
191
+ debug = _get_boolean_value(debug_ini, "plain_debug_mode", False)
192
+
193
+ setup_test_environment(debug=debug)
194
+ request.addfinalizer(teardown_test_environment)
195
+
196
+
197
+ @pytest.fixture(scope="session")
198
+ def plain_db_blocker() -> "_DatabaseBlocker | None":
199
+ """Wrapper around Plain's database access.
200
+
201
+ This object can be used to re-enable database access. This fixture is used
202
+ internally in pytest-plain to build the other fixtures and can be used for
203
+ special database handling.
204
+
205
+ The object is a context manager and provides the methods
206
+ .unblock()/.block() and .restore() to temporarily enable database access.
207
+
208
+ This is an advanced feature that is meant to be used to implement database
209
+ fixtures.
210
+ """
211
+ return _blocking_manager
212
+
213
+
214
+ @pytest.fixture(autouse=True)
215
+ def _plain_db_marker(request) -> None:
216
+ """Implement the plain_db marker, internal to pytest-plain."""
217
+ marker = request.node.get_closest_marker("plain_db")
218
+ if marker:
219
+ request.getfixturevalue("_plain_db_helper")
220
+
221
+
222
+ @pytest.fixture(autouse=True)
223
+ def _dj_autoclear_mailbox() -> None:
224
+ try:
225
+ from plain import mail
226
+
227
+ del mail.outbox[:]
228
+ except ImportError:
229
+ pass
230
+
231
+
232
+ @pytest.fixture()
233
+ def mailoutbox(
234
+ plain_mail_patch_dns: None,
235
+ _dj_autoclear_mailbox: None,
236
+ ) -> "list[plain.mail.EmailMessage] | None":
237
+ try:
238
+ from plain import mail
239
+
240
+ return mail.outbox
241
+ except ImportError:
242
+ pass
243
+
244
+
245
+ @pytest.fixture()
246
+ def plain_mail_patch_dns(
247
+ monkeypatch,
248
+ plain_mail_dnsname: str,
249
+ ) -> None:
250
+ try:
251
+ from plain import mail
252
+
253
+ monkeypatch.setattr(mail.message, "DNS_NAME", plain_mail_dnsname)
254
+ except ImportError:
255
+ pass
256
+
257
+
258
+ @pytest.fixture()
259
+ def plain_mail_dnsname() -> str:
260
+ return "fake-tests.example.com"
261
+
262
+
263
+ # ############### Helper Functions ################
264
+
265
+
266
+ class _DatabaseBlockerContextManager:
267
+ def __init__(self, db_blocker) -> None:
268
+ self._db_blocker = db_blocker
269
+
270
+ def __enter__(self) -> None:
271
+ pass
272
+
273
+ def __exit__(self, exc_type, exc_value, traceback) -> None:
274
+ self._db_blocker.restore()
275
+
276
+
277
+ class _DatabaseBlocker:
278
+ """Manager for plain.models.backends.base.base.BaseDatabaseWrapper.
279
+
280
+ This is the object returned by plain_db_blocker.
281
+ """
282
+
283
+ def __init__(self):
284
+ self._history = []
285
+ self._real_ensure_connection = None
286
+
287
+ @property
288
+ def _dj_db_wrapper(self) -> "plain.models.backends.base.base.BaseDatabaseWrapper":
289
+ from plain.models.backends.base.base import BaseDatabaseWrapper
290
+
291
+ # The first time the _dj_db_wrapper is accessed, we will save a
292
+ # reference to the real implementation.
293
+ if self._real_ensure_connection is None:
294
+ self._real_ensure_connection = BaseDatabaseWrapper.ensure_connection
295
+
296
+ return BaseDatabaseWrapper
297
+
298
+ def _save_active_wrapper(self) -> None:
299
+ self._history.append(self._dj_db_wrapper.ensure_connection)
300
+
301
+ def _blocking_wrapper(*args, **kwargs) -> "NoReturn":
302
+ __tracebackhide__ = True
303
+ __tracebackhide__ # Silence pyflakes
304
+ raise RuntimeError(
305
+ "Database access not allowed, "
306
+ 'use the "plain_db" mark, or the '
307
+ '"db" or "transactional_db" fixtures to enable it.'
308
+ )
309
+
310
+ def unblock(self) -> "ContextManager[None]":
311
+ """Enable access to the Plain database."""
312
+ self._save_active_wrapper()
313
+ self._dj_db_wrapper.ensure_connection = self._real_ensure_connection
314
+ return _DatabaseBlockerContextManager(self)
315
+
316
+ def block(self) -> "ContextManager[None]":
317
+ """Disable access to the Plain database."""
318
+ self._save_active_wrapper()
319
+ self._dj_db_wrapper.ensure_connection = self._blocking_wrapper
320
+ return _DatabaseBlockerContextManager(self)
321
+
322
+ def restore(self) -> None:
323
+ self._dj_db_wrapper.ensure_connection = self._history.pop()
324
+
325
+
326
+ _blocking_manager = _DatabaseBlocker()
327
+
328
+
329
+ def validate_urls(marker) -> list[str]:
330
+ """Validate the urls marker.
331
+
332
+ It checks the signature and creates the `urls` attribute on the
333
+ marker which will have the correct value.
334
+ """
335
+
336
+ def apifun(urls: list[str]) -> list[str]:
337
+ return urls
338
+
339
+ return apifun(*marker.args, **marker.kwargs)
File without changes
@@ -0,0 +1,473 @@
1
+ import logging
2
+ import sys
3
+ import unittest
4
+ from contextlib import contextmanager
5
+ from difflib import get_close_matches
6
+ from unittest.suite import _DebugResult
7
+
8
+ from plain.exceptions import ImproperlyConfigured
9
+ from plain.internal.legacy.management import call_command
10
+ from plain.internal.legacy.management.color import no_style
11
+ from plain.internal.legacy.management.sql import emit_post_migrate_signal
12
+ from plain.models import DEFAULT_DB_ALIAS, connections, transaction
13
+ from plain.packages import packages
14
+ from plain.test.client import Client
15
+ from plain.test.utils import (
16
+ modify_settings,
17
+ override_settings,
18
+ )
19
+
20
+ logger = logging.getLogger("plain.test")
21
+
22
+ __all__ = (
23
+ "TestCase",
24
+ "TransactionTestCase",
25
+ )
26
+
27
+
28
+ class DatabaseOperationForbidden(AssertionError):
29
+ pass
30
+
31
+
32
+ class _DatabaseFailure:
33
+ def __init__(self, wrapped, message):
34
+ self.wrapped = wrapped
35
+ self.message = message
36
+
37
+ def __call__(self):
38
+ raise DatabaseOperationForbidden(self.message)
39
+
40
+
41
+ class TransactionTestCase(unittest.TestCase):
42
+ # The class we'll use for the test client self.client.
43
+ # Can be overridden in derived classes.
44
+ client_class = Client
45
+ _overridden_settings = None
46
+ _modified_settings = None
47
+
48
+ databases = set()
49
+ _disallowed_database_msg = (
50
+ "Database %(operation)s to %(alias)r are not allowed in SimpleTestCase "
51
+ "subclasses. Either subclass TestCase or TransactionTestCase to ensure "
52
+ "proper test isolation or add %(alias)r to %(test)s.databases to silence "
53
+ "this failure."
54
+ )
55
+ _disallowed_connection_methods = [
56
+ ("connect", "connections"),
57
+ ("temporary_connection", "connections"),
58
+ ("cursor", "queries"),
59
+ ("chunked_cursor", "queries"),
60
+ ]
61
+
62
+ # Subclasses can ask for resetting of auto increment sequence before each
63
+ # test case
64
+ reset_sequences = False
65
+
66
+ # Subclasses can enable only a subset of packages for faster tests
67
+ available_packages = None
68
+
69
+ # Subclasses can define fixtures which will be automatically installed.
70
+ fixtures = None
71
+
72
+ databases = {DEFAULT_DB_ALIAS}
73
+ _disallowed_database_msg = (
74
+ "Database %(operation)s to %(alias)r are not allowed in this test. "
75
+ "Add %(alias)r to %(test)s.databases to ensure proper test isolation "
76
+ "and silence this failure."
77
+ )
78
+
79
+ # If transactions aren't available, Plain will serialize the database
80
+ # contents into a fixture during setup and flush and reload them
81
+ # during teardown (as flush does not restore data from migrations).
82
+ # This can be slow; this flag allows enabling on a per-case basis.
83
+ serialized_rollback = False
84
+
85
+ @classmethod
86
+ def setUpClass(cls):
87
+ super().setUpClass()
88
+ if cls._overridden_settings:
89
+ cls._cls_overridden_context = override_settings(**cls._overridden_settings)
90
+ cls._cls_overridden_context.enable()
91
+ cls.addClassCleanup(cls._cls_overridden_context.disable)
92
+ if cls._modified_settings:
93
+ cls._cls_modified_context = modify_settings(cls._modified_settings)
94
+ cls._cls_modified_context.enable()
95
+ cls.addClassCleanup(cls._cls_modified_context.disable)
96
+ cls._add_databases_failures()
97
+ cls.addClassCleanup(cls._remove_databases_failures)
98
+
99
+ @classmethod
100
+ def _validate_databases(cls):
101
+ if cls.databases == "__all__":
102
+ return frozenset(connections)
103
+ for alias in cls.databases:
104
+ if alias not in connections:
105
+ message = (
106
+ "{}.{}.databases refers to {!r} which is not defined in "
107
+ "settings.DATABASES.".format(
108
+ cls.__module__,
109
+ cls.__qualname__,
110
+ alias,
111
+ )
112
+ )
113
+ close_matches = get_close_matches(alias, list(connections))
114
+ if close_matches:
115
+ message += " Did you mean %r?" % close_matches[0]
116
+ raise ImproperlyConfigured(message)
117
+ return frozenset(cls.databases)
118
+
119
+ @classmethod
120
+ def _add_databases_failures(cls):
121
+ cls.databases = cls._validate_databases()
122
+ for alias in connections:
123
+ if alias in cls.databases:
124
+ continue
125
+ connection = connections[alias]
126
+ for name, operation in cls._disallowed_connection_methods:
127
+ message = cls._disallowed_database_msg % {
128
+ "test": f"{cls.__module__}.{cls.__qualname__}",
129
+ "alias": alias,
130
+ "operation": operation,
131
+ }
132
+ method = getattr(connection, name)
133
+ setattr(connection, name, _DatabaseFailure(method, message))
134
+
135
+ @classmethod
136
+ def _remove_databases_failures(cls):
137
+ for alias in connections:
138
+ if alias in cls.databases:
139
+ continue
140
+ connection = connections[alias]
141
+ for name, _ in cls._disallowed_connection_methods:
142
+ method = getattr(connection, name)
143
+ setattr(connection, name, method.wrapped)
144
+
145
+ def __call__(self, result=None):
146
+ """
147
+ Wrapper around default __call__ method to perform common Plain test
148
+ set up. This means that user-defined Test Cases aren't required to
149
+ include a call to super().setUp().
150
+ """
151
+ self._setup_and_call(result)
152
+
153
+ def debug(self):
154
+ """Perform the same as __call__(), without catching the exception."""
155
+ debug_result = _DebugResult()
156
+ self._setup_and_call(debug_result, debug=True)
157
+
158
+ def _setup_and_call(self, result, debug=False):
159
+ """
160
+ Perform the following in order: pre-setup, run test, post-teardown,
161
+ skipping pre/post hooks if test is set to be skipped.
162
+
163
+ If debug=True, reraise any errors in setup and use super().debug()
164
+ instead of __call__() to run the test.
165
+ """
166
+ testMethod = getattr(self, self._testMethodName)
167
+ skipped = getattr(self.__class__, "__unittest_skip__", False) or getattr(
168
+ testMethod, "__unittest_skip__", False
169
+ )
170
+
171
+ if not skipped:
172
+ try:
173
+ self._pre_setup()
174
+ except Exception:
175
+ if debug:
176
+ raise
177
+ result.addError(self, sys.exc_info())
178
+ return
179
+ if debug:
180
+ super().debug()
181
+ else:
182
+ super().__call__(result)
183
+ if not skipped:
184
+ try:
185
+ self._post_teardown()
186
+ except Exception:
187
+ if debug:
188
+ raise
189
+ result.addError(self, sys.exc_info())
190
+ return
191
+
192
+ def _pre_setup(self):
193
+ """
194
+ Perform pre-test setup:
195
+ * Create a test client.
196
+ * Clear the mail test outbox.
197
+ """
198
+ self.client = self.client_class()
199
+
200
+ try:
201
+ from plain import mail
202
+
203
+ mail.outbox = []
204
+ except ImportError:
205
+ pass
206
+
207
+ if self.available_packages is not None:
208
+ packages.set_available_packages(self.available_packages)
209
+ for db_name in self._databases_names(include_mirrors=False):
210
+ emit_post_migrate_signal(verbosity=0, interactive=False, db=db_name)
211
+ try:
212
+ self._fixture_setup()
213
+ except Exception:
214
+ if self.available_packages is not None:
215
+ packages.unset_available_packages()
216
+ raise
217
+ # Clear the queries_log so that it's less likely to overflow (a single
218
+ # test probably won't execute 9K queries). If queries_log overflows,
219
+ # then assertNumQueries() doesn't work.
220
+ for db_name in self._databases_names(include_mirrors=False):
221
+ connections[db_name].queries_log.clear()
222
+
223
+ def _post_teardown(self):
224
+ """
225
+ Perform post-test things:
226
+ * Flush the contents of the database to leave a clean slate. If the
227
+ class has an 'available_packages' attribute, don't fire post_migrate.
228
+ * Force-close the connection so the next test gets a clean cursor.
229
+ """
230
+ try:
231
+ self._fixture_teardown()
232
+ if self._should_reload_connections():
233
+ # Some DB cursors include SQL statements as part of cursor
234
+ # creation. If you have a test that does a rollback, the effect
235
+ # of these statements is lost, which can affect the operation of
236
+ # tests (e.g., losing a timezone setting causing objects to be
237
+ # created with the wrong time). To make sure this doesn't
238
+ # happen, get a clean connection at the start of every test.
239
+ for conn in connections.all(initialized_only=True):
240
+ conn.close()
241
+ finally:
242
+ if self.available_packages is not None:
243
+ packages.unset_available_packages()
244
+
245
+ def settings(self, **kwargs):
246
+ """
247
+ A context manager that temporarily sets a setting and reverts to the
248
+ original value when exiting the context.
249
+ """
250
+ return override_settings(**kwargs)
251
+
252
+ def modify_settings(self, **kwargs):
253
+ """
254
+ A context manager that temporarily applies changes a list setting and
255
+ reverts back to the original value when exiting the context.
256
+ """
257
+ return modify_settings(**kwargs)
258
+
259
+ @classmethod
260
+ def _databases_names(cls, include_mirrors=True):
261
+ # Only consider allowed database aliases, including mirrors or not.
262
+ return [
263
+ alias
264
+ for alias in connections
265
+ if alias in cls.databases
266
+ and (
267
+ include_mirrors
268
+ or not connections[alias].settings_dict["TEST"]["MIRROR"]
269
+ )
270
+ ]
271
+
272
+ def _reset_sequences(self, db_name):
273
+ conn = connections[db_name]
274
+ if conn.features.supports_sequence_reset:
275
+ sql_list = conn.ops.sequence_reset_by_name_sql(
276
+ no_style(), conn.introspection.sequence_list()
277
+ )
278
+ if sql_list:
279
+ with transaction.atomic(using=db_name):
280
+ with conn.cursor() as cursor:
281
+ for sql in sql_list:
282
+ cursor.execute(sql)
283
+
284
+ def _fixture_setup(self):
285
+ for db_name in self._databases_names(include_mirrors=False):
286
+ # Reset sequences
287
+ if self.reset_sequences:
288
+ self._reset_sequences(db_name)
289
+
290
+ # Provide replica initial data from migrated packages, if needed.
291
+ # if self.serialized_rollback and hasattr(
292
+ # connections[db_name], "_test_serialized_contents"
293
+ # ):
294
+ # if self.available_packages is not None:
295
+ # packages.unset_available_packages()
296
+ # connections[db_name].creation.deserialize_db_from_string(
297
+ # connections[db_name]._test_serialized_contents
298
+ # )
299
+ # if self.available_packages is not None:
300
+ # packages.set_available_packages(self.available_packages)
301
+
302
+ if self.fixtures:
303
+ # We have to use this slightly awkward syntax due to the fact
304
+ # that we're using *args and **kwargs together.
305
+ call_command(
306
+ "loaddata", *self.fixtures, **{"verbosity": 0, "database": db_name}
307
+ )
308
+
309
+ def _should_reload_connections(self):
310
+ return True
311
+
312
+ def _fixture_teardown(self):
313
+ # Allow TRUNCATE ... CASCADE and don't emit the post_migrate signal
314
+ # when flushing only a subset of the packages
315
+ for db_name in self._databases_names(include_mirrors=False):
316
+ # Flush the database
317
+ inhibit_post_migrate = (
318
+ self.available_packages is not None
319
+ or ( # Inhibit the post_migrate signal when using serialized
320
+ # rollback to avoid trying to recreate the serialized data.
321
+ self.serialized_rollback
322
+ # and hasattr(connections[db_name], "_test_serialized_contents")
323
+ )
324
+ )
325
+ call_command(
326
+ "flush",
327
+ verbosity=0,
328
+ interactive=False,
329
+ database=db_name,
330
+ reset_sequences=False,
331
+ allow_cascade=self.available_packages is not None,
332
+ inhibit_post_migrate=inhibit_post_migrate,
333
+ )
334
+
335
+
336
+ def connections_support_transactions(aliases=None):
337
+ """
338
+ Return whether or not all (or specified) connections support
339
+ transactions.
340
+ """
341
+ conns = (
342
+ connections.all()
343
+ if aliases is None
344
+ else (connections[alias] for alias in aliases)
345
+ )
346
+ return all(conn.features.supports_transactions for conn in conns)
347
+
348
+
349
+ class TestCase(TransactionTestCase):
350
+ """
351
+ Similar to TransactionTestCase, but use `transaction.atomic()` to achieve
352
+ test isolation.
353
+
354
+ In most situations, TestCase should be preferred to TransactionTestCase as
355
+ it allows faster execution. However, there are some situations where using
356
+ TransactionTestCase might be necessary (e.g. testing some transactional
357
+ behavior).
358
+
359
+ On database backends with no transaction support, TestCase behaves as
360
+ TransactionTestCase.
361
+ """
362
+
363
+ @classmethod
364
+ def _enter_atomics(cls):
365
+ """Open atomic blocks for multiple databases."""
366
+ atomics = {}
367
+ for db_name in cls._databases_names():
368
+ atomic = transaction.atomic(using=db_name)
369
+ atomic._from_testcase = True
370
+ atomic.__enter__()
371
+ atomics[db_name] = atomic
372
+ return atomics
373
+
374
+ @classmethod
375
+ def _rollback_atomics(cls, atomics):
376
+ """Rollback atomic blocks opened by the previous method."""
377
+ for db_name in reversed(cls._databases_names()):
378
+ transaction.set_rollback(True, using=db_name)
379
+ atomics[db_name].__exit__(None, None, None)
380
+
381
+ @classmethod
382
+ def _databases_support_transactions(cls):
383
+ return connections_support_transactions(cls.databases)
384
+
385
+ @classmethod
386
+ def setUpClass(cls):
387
+ super().setUpClass()
388
+ if not cls._databases_support_transactions():
389
+ return
390
+ cls.cls_atomics = cls._enter_atomics()
391
+
392
+ if cls.fixtures:
393
+ for db_name in cls._databases_names(include_mirrors=False):
394
+ try:
395
+ call_command(
396
+ "loaddata",
397
+ *cls.fixtures,
398
+ **{"verbosity": 0, "database": db_name},
399
+ )
400
+ except Exception:
401
+ cls._rollback_atomics(cls.cls_atomics)
402
+ raise
403
+
404
+ @classmethod
405
+ def tearDownClass(cls):
406
+ if cls._databases_support_transactions():
407
+ cls._rollback_atomics(cls.cls_atomics)
408
+ for conn in connections.all(initialized_only=True):
409
+ conn.close()
410
+ super().tearDownClass()
411
+
412
+ def _should_reload_connections(self):
413
+ if self._databases_support_transactions():
414
+ return False
415
+ return super()._should_reload_connections()
416
+
417
+ def _fixture_setup(self):
418
+ if not self._databases_support_transactions():
419
+ return super()._fixture_setup()
420
+
421
+ if self.reset_sequences:
422
+ raise TypeError("reset_sequences cannot be used on TestCase instances")
423
+ self.atomics = self._enter_atomics()
424
+
425
+ def _fixture_teardown(self):
426
+ if not self._databases_support_transactions():
427
+ return super()._fixture_teardown()
428
+ try:
429
+ for db_name in reversed(self._databases_names()):
430
+ if self._should_check_constraints(connections[db_name]):
431
+ connections[db_name].check_constraints()
432
+ finally:
433
+ self._rollback_atomics(self.atomics)
434
+
435
+ def _should_check_constraints(self, connection):
436
+ return (
437
+ connection.features.can_defer_constraint_checks
438
+ and not connection.needs_rollback
439
+ and connection.is_usable()
440
+ )
441
+
442
+ @classmethod
443
+ @contextmanager
444
+ def captureOnCommitCallbacks(cls, *, using=DEFAULT_DB_ALIAS, execute=False):
445
+ """Context manager to capture transaction.on_commit() callbacks."""
446
+ callbacks = []
447
+ start_count = len(connections[using].run_on_commit)
448
+ try:
449
+ yield callbacks
450
+ finally:
451
+ while True:
452
+ callback_count = len(connections[using].run_on_commit)
453
+ for _, callback, robust in connections[using].run_on_commit[
454
+ start_count:
455
+ ]:
456
+ callbacks.append(callback)
457
+ if execute:
458
+ if robust:
459
+ try:
460
+ callback()
461
+ except Exception as e:
462
+ logger.error(
463
+ f"Error calling {callback.__qualname__} in "
464
+ f"on_commit() (%s).",
465
+ e,
466
+ exc_info=True,
467
+ )
468
+ else:
469
+ callback()
470
+
471
+ if callback_count == len(connections[using].run_on_commit):
472
+ break
473
+ start_count = callback_count
@@ -0,0 +1,44 @@
1
+ [tool.poetry]
2
+
3
+ name = "plain.pytest"
4
+ packages = [
5
+ { include = "plain" },
6
+ ]
7
+
8
+ version = "0.0.0"
9
+ description = "Testing for Plain"
10
+ authors = ["Dave Gaeddert <dave.gaeddert@dropseed.dev>"]
11
+ license = "MIT"
12
+ readme = "README.md"
13
+ homepage = "https://www.plainpackages.com/"
14
+ documentation = "https://www.plainpackages.com/docs/"
15
+ repository = "https://github.com/plainpackages/plain-pytest"
16
+ keywords = ["django", "saas", "plain", "framework"]
17
+ classifiers = [
18
+ "Environment :: Web Environment",
19
+ "Framework :: Django",
20
+ "Framework :: Django :: 4",
21
+ "Intended Audience :: Developers",
22
+ "Operating System :: OS Independent",
23
+ ]
24
+
25
+ # Make the CLI available without adding to INSTALLED_APPS
26
+ [tool.poetry.plugins."plain.cli"]
27
+ "test" = "plain.pytest:cli"
28
+
29
+ # Automatically sets this up with pytest
30
+ [tool.poetry.plugins."pytest11"]
31
+ "plain" = "plain.pytest.pytest.plugin"
32
+
33
+
34
+ [tool.poetry.dependencies]
35
+ python = "^3.8"
36
+ click = ">=8.0.0"
37
+ pytest = "^7.0.0"
38
+
39
+ [tool.poetry.dev-dependencies]
40
+ ipdb = "^0.13.9"
41
+
42
+ [build-system]
43
+ requires = ["poetry-core>=1.0.0"]
44
+ build-backend = "poetry.core.masonry.api"