pytest-neon 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pytest_neon/__init__.py CHANGED
@@ -9,7 +9,7 @@ from pytest_neon.plugin import (
9
9
  neon_engine,
10
10
  )
11
11
 
12
- __version__ = "0.3.0"
12
+ __version__ = "0.5.0"
13
13
  __all__ = [
14
14
  "NeonBranch",
15
15
  "neon_branch",
pytest_neon/plugin.py CHANGED
@@ -30,9 +30,10 @@ class NeonBranch:
30
30
 
31
31
 
32
32
  def pytest_addoption(parser: pytest.Parser) -> None:
33
- """Add Neon-specific command line options."""
33
+ """Add Neon-specific command line options and ini settings."""
34
34
  group = parser.getgroup("neon", "Neon database branching")
35
35
 
36
+ # CLI options
36
37
  group.addoption(
37
38
  "--neon-api-key",
38
39
  dest="neon_api_key",
@@ -51,13 +52,11 @@ def pytest_addoption(parser: pytest.Parser) -> None:
51
52
  group.addoption(
52
53
  "--neon-database",
53
54
  dest="neon_database",
54
- default="neondb",
55
55
  help="Database name (default: neondb)",
56
56
  )
57
57
  group.addoption(
58
58
  "--neon-role",
59
59
  dest="neon_role",
60
- default="neondb_owner",
61
60
  help="Database role (default: neondb_owner)",
62
61
  )
63
62
  group.addoption(
@@ -70,7 +69,6 @@ def pytest_addoption(parser: pytest.Parser) -> None:
70
69
  "--neon-branch-expiry",
71
70
  dest="neon_branch_expiry",
72
71
  type=int,
73
- default=DEFAULT_BRANCH_EXPIRY_SECONDS,
74
72
  help=(
75
73
  f"Branch auto-expiry in seconds "
76
74
  f"(default: {DEFAULT_BRANCH_EXPIRY_SECONDS}). Set to 0 to disable."
@@ -79,45 +77,114 @@ def pytest_addoption(parser: pytest.Parser) -> None:
79
77
  group.addoption(
80
78
  "--neon-env-var",
81
79
  dest="neon_env_var",
82
- default="DATABASE_URL",
83
80
  help="Environment variable to set with connection string (default: DATABASE_URL)", # noqa: E501
84
81
  )
85
82
 
83
+ # INI file settings (pytest.ini, pyproject.toml, etc.)
84
+ parser.addini("neon_api_key", "Neon API key", default=None)
85
+ parser.addini("neon_project_id", "Neon project ID", default=None)
86
+ parser.addini("neon_parent_branch", "Parent branch ID", default=None)
87
+ parser.addini("neon_database", "Database name", default="neondb")
88
+ parser.addini("neon_role", "Database role", default="neondb_owner")
89
+ parser.addini(
90
+ "neon_keep_branches",
91
+ "Don't delete branches after tests",
92
+ type="bool",
93
+ default=False,
94
+ )
95
+ parser.addini(
96
+ "neon_branch_expiry",
97
+ "Branch auto-expiry in seconds",
98
+ default=str(DEFAULT_BRANCH_EXPIRY_SECONDS),
99
+ )
100
+ parser.addini(
101
+ "neon_env_var",
102
+ "Environment variable for connection string",
103
+ default="DATABASE_URL",
104
+ )
105
+
86
106
 
87
107
  def _get_config_value(
88
- config: pytest.Config, option: str, env_var: str, default: str | None = None
108
+ config: pytest.Config,
109
+ option: str,
110
+ env_var: str,
111
+ ini_name: str | None = None,
112
+ default: str | None = None,
89
113
  ) -> str | None:
90
- """Get config value from CLI option, env var, or default."""
114
+ """Get config value from CLI option, env var, ini setting, or default.
115
+
116
+ Priority order: CLI option > environment variable > ini setting > default
117
+ """
118
+ # 1. CLI option (highest priority)
91
119
  value = config.getoption(option, default=None)
92
120
  if value is not None:
93
121
  return value
94
- return os.environ.get(env_var, default)
122
+
123
+ # 2. Environment variable
124
+ env_value = os.environ.get(env_var)
125
+ if env_value is not None:
126
+ return env_value
127
+
128
+ # 3. INI setting (pytest.ini, pyproject.toml, etc.)
129
+ if ini_name is not None:
130
+ ini_value = config.getini(ini_name)
131
+ if ini_value:
132
+ return ini_value
133
+
134
+ # 4. Default
135
+ return default
95
136
 
96
137
 
97
138
  def _create_neon_branch(
98
139
  request: pytest.FixtureRequest,
140
+ parent_branch_id_override: str | None = None,
141
+ branch_expiry_override: int | None = None,
142
+ branch_name_suffix: str = "",
99
143
  ) -> Generator[NeonBranch, None, None]:
100
144
  """
101
145
  Internal helper that creates and manages a Neon branch lifecycle.
102
146
 
103
147
  This is the core implementation used by branch fixtures.
148
+
149
+ Args:
150
+ request: Pytest fixture request
151
+ parent_branch_id_override: If provided, use this as parent instead of config
152
+ branch_expiry_override: If provided, use this expiry instead of config
153
+ branch_name_suffix: Optional suffix for branch name (e.g., "-migrated", "-test")
104
154
  """
105
155
  config = request.config
106
156
 
107
- api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY")
108
- project_id = _get_config_value(config, "neon_project_id", "NEON_PROJECT_ID")
109
- parent_branch_id = _get_config_value(
110
- config, "neon_parent_branch", "NEON_PARENT_BRANCH_ID"
157
+ api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY", "neon_api_key")
158
+ project_id = _get_config_value(
159
+ config, "neon_project_id", "NEON_PROJECT_ID", "neon_project_id"
160
+ )
161
+ # Use override if provided, otherwise read from config
162
+ parent_branch_id = parent_branch_id_override or _get_config_value(
163
+ config, "neon_parent_branch", "NEON_PARENT_BRANCH_ID", "neon_parent_branch"
111
164
  )
112
165
  database_name = _get_config_value(
113
- config, "neon_database", "NEON_DATABASE", "neondb"
166
+ config, "neon_database", "NEON_DATABASE", "neon_database", "neondb"
114
167
  )
115
- role_name = _get_config_value(config, "neon_role", "NEON_ROLE", "neondb_owner")
116
- keep_branches = config.getoption("neon_keep_branches", default=False)
117
- branch_expiry = config.getoption(
118
- "neon_branch_expiry", default=DEFAULT_BRANCH_EXPIRY_SECONDS
168
+ role_name = _get_config_value(
169
+ config, "neon_role", "NEON_ROLE", "neon_role", "neondb_owner"
170
+ )
171
+
172
+ # For boolean/int options, check CLI first, then ini
173
+ keep_branches = config.getoption("neon_keep_branches", default=None)
174
+ if keep_branches is None:
175
+ keep_branches = config.getini("neon_keep_branches")
176
+
177
+ # Use override if provided, otherwise read from config
178
+ if branch_expiry_override is not None:
179
+ branch_expiry = branch_expiry_override
180
+ else:
181
+ branch_expiry = config.getoption("neon_branch_expiry", default=None)
182
+ if branch_expiry is None:
183
+ branch_expiry = int(config.getini("neon_branch_expiry"))
184
+
185
+ env_var_name = _get_config_value(
186
+ config, "neon_env_var", "", "neon_env_var", "DATABASE_URL"
119
187
  )
120
- env_var_name = config.getoption("neon_env_var", default="DATABASE_URL")
121
188
 
122
189
  if not api_key:
123
190
  pytest.skip(
@@ -132,7 +199,7 @@ def _create_neon_branch(
132
199
  neon = NeonAPI(api_key=api_key)
133
200
 
134
201
  # Generate unique branch name
135
- branch_name = f"pytest-{os.urandom(4).hex()}"
202
+ branch_name = f"pytest-{os.urandom(4).hex()}{branch_name_suffix}"
136
203
 
137
204
  # Build branch creation payload
138
205
  branch_config: dict[str, Any] = {"name": branch_name}
@@ -258,12 +325,86 @@ def _reset_branch_to_parent(branch: NeonBranch, api_key: str) -> None:
258
325
  response.raise_for_status()
259
326
 
260
327
 
328
+ @pytest.fixture(scope="session")
329
+ def _neon_migration_branch(
330
+ request: pytest.FixtureRequest,
331
+ ) -> Generator[NeonBranch, None, None]:
332
+ """
333
+ Session-scoped branch where migrations are applied.
334
+
335
+ This branch is created from the configured parent and serves as
336
+ the parent for all test branches. Migrations run once per session
337
+ on this branch.
338
+
339
+ Note: The migration branch cannot have an expiry because Neon doesn't
340
+ allow creating child branches from branches with expiration dates.
341
+ Cleanup relies on the fixture teardown at session end.
342
+ """
343
+ # No expiry - Neon doesn't allow children from branches with expiry
344
+ yield from _create_neon_branch(
345
+ request,
346
+ branch_expiry_override=0,
347
+ branch_name_suffix="-migrated",
348
+ )
349
+
350
+
351
+ @pytest.fixture(scope="session")
352
+ def neon_apply_migrations(_neon_migration_branch: NeonBranch) -> None:
353
+ """
354
+ Override this fixture to run migrations on the test database.
355
+
356
+ The migration branch is already created and DATABASE_URL is set.
357
+ Migrations run once per test session, before any tests execute.
358
+
359
+ Example in conftest.py:
360
+
361
+ @pytest.fixture(scope="session")
362
+ def neon_apply_migrations(_neon_migration_branch):
363
+ import subprocess
364
+ subprocess.run(["alembic", "upgrade", "head"], check=True)
365
+
366
+ Or with Django:
367
+
368
+ @pytest.fixture(scope="session")
369
+ def neon_apply_migrations(_neon_migration_branch):
370
+ from django.core.management import call_command
371
+ call_command("migrate", "--noinput")
372
+
373
+ Or with raw SQL:
374
+
375
+ @pytest.fixture(scope="session")
376
+ def neon_apply_migrations(_neon_migration_branch):
377
+ import psycopg
378
+ with psycopg.connect(_neon_migration_branch.connection_string) as conn:
379
+ with open("schema.sql") as f:
380
+ conn.execute(f.read())
381
+ conn.commit()
382
+
383
+ Args:
384
+ _neon_migration_branch: The migration branch with connection details.
385
+ Use _neon_migration_branch.connection_string to connect directly,
386
+ or rely on DATABASE_URL which is already set.
387
+ """
388
+ pass # No-op by default - users override this fixture to run migrations
389
+
390
+
261
391
  @pytest.fixture(scope="module")
262
392
  def _neon_branch_for_reset(
263
393
  request: pytest.FixtureRequest,
394
+ _neon_migration_branch: NeonBranch,
395
+ neon_apply_migrations: None, # Ensures migrations run first
264
396
  ) -> Generator[NeonBranch, None, None]:
265
- """Internal fixture that creates a branch for reset-based isolation."""
266
- yield from _create_neon_branch(request)
397
+ """
398
+ Internal fixture that creates a test branch from the migration branch.
399
+
400
+ The test branch is created as a child of the migration branch, so resets
401
+ restore to post-migration state rather than the original parent state.
402
+ """
403
+ yield from _create_neon_branch(
404
+ request,
405
+ parent_branch_id_override=_neon_migration_branch.branch_id,
406
+ branch_name_suffix="-test",
407
+ )
267
408
 
268
409
 
269
410
  @pytest.fixture(scope="function")
@@ -301,7 +442,7 @@ def neon_branch(
301
442
  conn_string = neon_branch.connection_string
302
443
  """
303
444
  config = request.config
304
- api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY")
445
+ api_key = _get_config_value(config, "neon_api_key", "NEON_API_KEY", "neon_api_key")
305
446
 
306
447
  # Validate that branch has a parent for reset functionality
307
448
  if not _neon_branch_for_reset.parent_id:
@@ -330,6 +471,8 @@ def neon_branch(
330
471
  @pytest.fixture(scope="module")
331
472
  def neon_branch_shared(
332
473
  request: pytest.FixtureRequest,
474
+ _neon_migration_branch: NeonBranch,
475
+ neon_apply_migrations: None, # Ensures migrations run first
333
476
  ) -> Generator[NeonBranch, None, None]:
334
477
  """
335
478
  Provide a shared Neon database branch for all tests in a module.
@@ -338,6 +481,9 @@ def neon_branch_shared(
338
481
  tests without resetting. This is the fastest option but tests can see
339
482
  each other's data modifications.
340
483
 
484
+ If you override the `neon_apply_migrations` fixture, migrations will run
485
+ once before the first test, and this branch will include the migrated schema.
486
+
341
487
  Use this when:
342
488
  - Tests are read-only or don't interfere with each other
343
489
  - You manually clean up test data within each test
@@ -355,7 +501,11 @@ def neon_branch_shared(
355
501
  # Fast: no reset between tests, but be careful about data leakage
356
502
  conn_string = neon_branch_shared.connection_string
357
503
  """
358
- yield from _create_neon_branch(request)
504
+ yield from _create_neon_branch(
505
+ request,
506
+ parent_branch_id_override=_neon_migration_branch.branch_id,
507
+ branch_name_suffix="-shared",
508
+ )
359
509
 
360
510
 
361
511
  @pytest.fixture
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-neon
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: Pytest plugin for Neon database branch isolation in tests
5
5
  Project-URL: Homepage, https://github.com/zain/pytest-neon
6
6
  Project-URL: Repository, https://github.com/zain/pytest-neon
@@ -120,6 +120,7 @@ Returns a `NeonBranch` dataclass with:
120
120
  - `project_id`: The Neon project ID
121
121
  - `connection_string`: Full PostgreSQL connection URI
122
122
  - `host`: The database host
123
+ - `parent_id`: The parent branch ID (used for resets)
123
124
 
124
125
  ```python
125
126
  import os
@@ -199,6 +200,90 @@ def test_query(neon_engine):
199
200
  assert result.scalar() == 1
200
201
  ```
201
202
 
203
+ ## Migrations
204
+
205
+ pytest-neon supports running migrations once before tests, with all test resets preserving the migrated state.
206
+
207
+ ### How It Works
208
+
209
+ When you override the `neon_apply_migrations` fixture, the plugin uses a two-branch architecture:
210
+
211
+ ```
212
+ Parent Branch (your configured parent)
213
+ └── Migration Branch (session-scoped)
214
+ │ ↑ migrations run here ONCE
215
+ └── Test Branch (module-scoped)
216
+ ↑ resets to migration branch after each test
217
+ ```
218
+
219
+ This means:
220
+ - Migrations run **once per test session** (not per test or per module)
221
+ - Each test reset restores to the **post-migration state**
222
+ - Tests always see your migrated schema
223
+
224
+ ### Setup
225
+
226
+ Override the `neon_apply_migrations` fixture in your `conftest.py`:
227
+
228
+ **Alembic:**
229
+ ```python
230
+ # conftest.py
231
+ import subprocess
232
+ import pytest
233
+
234
+ @pytest.fixture(scope="session")
235
+ def neon_apply_migrations(_neon_migration_branch):
236
+ """Run Alembic migrations before tests."""
237
+ # DATABASE_URL is already set to the migration branch
238
+ subprocess.run(["alembic", "upgrade", "head"], check=True)
239
+ ```
240
+
241
+ **Django:**
242
+ ```python
243
+ # conftest.py
244
+ import pytest
245
+
246
+ @pytest.fixture(scope="session")
247
+ def neon_apply_migrations(_neon_migration_branch):
248
+ """Run Django migrations before tests."""
249
+ from django.core.management import call_command
250
+ call_command("migrate", "--noinput")
251
+ ```
252
+
253
+ **Raw SQL:**
254
+ ```python
255
+ # conftest.py
256
+ import pytest
257
+
258
+ @pytest.fixture(scope="session")
259
+ def neon_apply_migrations(_neon_migration_branch):
260
+ """Apply schema from SQL file."""
261
+ import psycopg
262
+ with psycopg.connect(_neon_migration_branch.connection_string) as conn:
263
+ with open("schema.sql") as f:
264
+ conn.execute(f.read())
265
+ conn.commit()
266
+ ```
267
+
268
+ **Custom migration tool:**
269
+ ```python
270
+ # conftest.py
271
+ import pytest
272
+
273
+ @pytest.fixture(scope="session")
274
+ def neon_apply_migrations(_neon_migration_branch):
275
+ """Run custom migrations."""
276
+ from myapp.migrations import run_migrations
277
+ run_migrations(_neon_migration_branch.connection_string)
278
+ ```
279
+
280
+ ### Important Notes
281
+
282
+ - The `_neon_migration_branch` parameter gives you access to the `NeonBranch` object with `connection_string`, `branch_id`, etc.
283
+ - `DATABASE_URL` (or your configured env var) is automatically set when the fixture runs
284
+ - If you don't override `neon_apply_migrations`, no migrations run (the fixture is a no-op by default)
285
+ - Migrations run before any test branches are created, so all tests see the same migrated schema
286
+
202
287
  ## Configuration
203
288
 
204
289
  ### Environment Variables
@@ -237,6 +322,30 @@ pytest --neon-branch-expiry=0
237
322
  pytest --neon-env-var=TEST_DATABASE_URL
238
323
  ```
239
324
 
325
+ ### pyproject.toml / pytest.ini
326
+
327
+ You can also configure options in your `pyproject.toml`:
328
+
329
+ ```toml
330
+ [tool.pytest.ini_options]
331
+ neon_database = "mydb"
332
+ neon_role = "myrole"
333
+ neon_keep_branches = true
334
+ neon_branch_expiry = "300"
335
+ ```
336
+
337
+ Or in `pytest.ini`:
338
+
339
+ ```ini
340
+ [pytest]
341
+ neon_database = mydb
342
+ neon_role = myrole
343
+ neon_keep_branches = true
344
+ neon_branch_expiry = 300
345
+ ```
346
+
347
+ **Priority order**: CLI options > environment variables > ini settings > defaults
348
+
240
349
  ## CI/CD Integration
241
350
 
242
351
  ### GitHub Actions
@@ -0,0 +1,8 @@
1
+ pytest_neon/__init__.py,sha256=GmCMNwXFausX8ZucJLRZmywVEc2IamMolGhQ3JIvqpk,398
2
+ pytest_neon/plugin.py,sha256=1TukuxsBQ88NlgQS6qosUarIrCc94apMrwvVrYt9IOg,23241
3
+ pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pytest_neon-0.5.0.dist-info/METADATA,sha256=UsUvPOuacr1cs2sAjWDhsQJfoGnKFswo3Td3m5unuNw,13532
5
+ pytest_neon-0.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ pytest_neon-0.5.0.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
7
+ pytest_neon-0.5.0.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
8
+ pytest_neon-0.5.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pytest_neon/__init__.py,sha256=RftEKwHM37DDwxA_oVN00DMXTh_3tlpaz6LbbN_IOYA,398
2
- pytest_neon/plugin.py,sha256=A4eTJIV6XKQv4AIBizhg5qRDrvB_Hfx4ysTerjROHUg,17938
3
- pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- pytest_neon-0.3.0.dist-info/METADATA,sha256=JmC3wuJSupLxVqeDlgzpUyYYpawRArzGj96N2CtcTE0,10555
5
- pytest_neon-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
- pytest_neon-0.3.0.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
7
- pytest_neon-0.3.0.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
8
- pytest_neon-0.3.0.dist-info/RECORD,,