pytest-neon 0.4.0__py3-none-any.whl → 0.5.1__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.4.0"
12
+ __version__ = "0.5.1"
13
13
  __all__ = [
14
14
  "NeonBranch",
15
15
  "neon_branch",
pytest_neon/plugin.py CHANGED
@@ -137,11 +137,20 @@ def _get_config_value(
137
137
 
138
138
  def _create_neon_branch(
139
139
  request: pytest.FixtureRequest,
140
+ parent_branch_id_override: str | None = None,
141
+ branch_expiry_override: int | None = None,
142
+ branch_name_suffix: str = "",
140
143
  ) -> Generator[NeonBranch, None, None]:
141
144
  """
142
145
  Internal helper that creates and manages a Neon branch lifecycle.
143
146
 
144
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")
145
154
  """
146
155
  config = request.config
147
156
 
@@ -149,7 +158,8 @@ def _create_neon_branch(
149
158
  project_id = _get_config_value(
150
159
  config, "neon_project_id", "NEON_PROJECT_ID", "neon_project_id"
151
160
  )
152
- parent_branch_id = _get_config_value(
161
+ # Use override if provided, otherwise read from config
162
+ parent_branch_id = parent_branch_id_override or _get_config_value(
153
163
  config, "neon_parent_branch", "NEON_PARENT_BRANCH_ID", "neon_parent_branch"
154
164
  )
155
165
  database_name = _get_config_value(
@@ -164,9 +174,13 @@ def _create_neon_branch(
164
174
  if keep_branches is None:
165
175
  keep_branches = config.getini("neon_keep_branches")
166
176
 
167
- branch_expiry = config.getoption("neon_branch_expiry", default=None)
168
- if branch_expiry is None:
169
- branch_expiry = int(config.getini("neon_branch_expiry"))
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"))
170
184
 
171
185
  env_var_name = _get_config_value(
172
186
  config, "neon_env_var", "", "neon_env_var", "DATABASE_URL"
@@ -185,7 +199,7 @@ def _create_neon_branch(
185
199
  neon = NeonAPI(api_key=api_key)
186
200
 
187
201
  # Generate unique branch name
188
- branch_name = f"pytest-{os.urandom(4).hex()}"
202
+ branch_name = f"pytest-{os.urandom(4).hex()}{branch_name_suffix}"
189
203
 
190
204
  # Build branch creation payload
191
205
  branch_config: dict[str, Any] = {"name": branch_name}
@@ -311,12 +325,86 @@ def _reset_branch_to_parent(branch: NeonBranch, api_key: str) -> None:
311
325
  response.raise_for_status()
312
326
 
313
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
+
314
391
  @pytest.fixture(scope="module")
315
392
  def _neon_branch_for_reset(
316
393
  request: pytest.FixtureRequest,
394
+ _neon_migration_branch: NeonBranch,
395
+ neon_apply_migrations: None, # Ensures migrations run first
317
396
  ) -> Generator[NeonBranch, None, None]:
318
- """Internal fixture that creates a branch for reset-based isolation."""
319
- 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
+ )
320
408
 
321
409
 
322
410
  @pytest.fixture(scope="function")
@@ -383,6 +471,8 @@ def neon_branch(
383
471
  @pytest.fixture(scope="module")
384
472
  def neon_branch_shared(
385
473
  request: pytest.FixtureRequest,
474
+ _neon_migration_branch: NeonBranch,
475
+ neon_apply_migrations: None, # Ensures migrations run first
386
476
  ) -> Generator[NeonBranch, None, None]:
387
477
  """
388
478
  Provide a shared Neon database branch for all tests in a module.
@@ -391,6 +481,9 @@ def neon_branch_shared(
391
481
  tests without resetting. This is the fastest option but tests can see
392
482
  each other's data modifications.
393
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
+
394
487
  Use this when:
395
488
  - Tests are read-only or don't interfere with each other
396
489
  - You manually clean up test data within each test
@@ -408,7 +501,11 @@ def neon_branch_shared(
408
501
  # Fast: no reset between tests, but be careful about data leakage
409
502
  conn_string = neon_branch_shared.connection_string
410
503
  """
411
- 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
+ )
412
509
 
413
510
 
414
511
  @pytest.fixture
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-neon
3
- Version: 0.4.0
3
+ Version: 0.5.1
4
4
  Summary: Pytest plugin for Neon database branch isolation in tests
5
- Project-URL: Homepage, https://github.com/zain/pytest-neon
6
- Project-URL: Repository, https://github.com/zain/pytest-neon
7
- Project-URL: Issues, https://github.com/zain/pytest-neon/issues
5
+ Project-URL: Homepage, https://github.com/ZainRizvi/pytest-neon
6
+ Project-URL: Repository, https://github.com/ZainRizvi/pytest-neon
7
+ Project-URL: Issues, https://github.com/ZainRizvi/pytest-neon/issues
8
8
  Author: Zain Rizvi
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
@@ -200,6 +200,90 @@ def test_query(neon_engine):
200
200
  assert result.scalar() == 1
201
201
  ```
202
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
+
203
287
  ## Configuration
204
288
 
205
289
  ### Environment Variables
@@ -0,0 +1,8 @@
1
+ pytest_neon/__init__.py,sha256=YQt1LLTbNLiq4kChmiPEmOD5aTsqhCgpRpxK80VAY7Y,398
2
+ pytest_neon/plugin.py,sha256=1TukuxsBQ88NlgQS6qosUarIrCc94apMrwvVrYt9IOg,23241
3
+ pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pytest_neon-0.5.1.dist-info/METADATA,sha256=i1h6TFZWjG0QY2h_UZsYzz5e9AaQewfCXKIXi_JC00E,13547
5
+ pytest_neon-0.5.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ pytest_neon-0.5.1.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
7
+ pytest_neon-0.5.1.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
8
+ pytest_neon-0.5.1.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pytest_neon/__init__.py,sha256=C_3fxrYTEBtLi_n9gJNJ9KyDk3yPFYcAmVDm87Sj-JQ,398
2
- pytest_neon/plugin.py,sha256=dYNcaC1kiBWsoi-_jgANTQsCRRyi3ynONqBG0ZXX4i8,19526
3
- pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- pytest_neon-0.4.0.dist-info/METADATA,sha256=XIRZcTXGiYefebFw1Vm6Uc2cL9ZvHTxzukWtUmtN40Q,11052
5
- pytest_neon-0.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
- pytest_neon-0.4.0.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
7
- pytest_neon-0.4.0.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
8
- pytest_neon-0.4.0.dist-info/RECORD,,