fastapi-router-versioning 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/PKG-INFO +7 -5
  2. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/README.md +6 -4
  3. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/RELEASE_NOTES.md +12 -0
  4. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/fastapi_router_versioning/__init__.py +1 -1
  5. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/fastapi_router_versioning/versioner.py +40 -6
  6. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/tests/test_versioner.py +108 -0
  7. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/dependabot.yml +0 -0
  8. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/labeler.yml +0 -0
  9. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/create-draft-release.yml +0 -0
  10. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/detect-conflicts.yml +0 -0
  11. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/guard-dependencies.yml +0 -0
  12. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/labeler.yml +0 -0
  13. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/latest-changes.yml +0 -0
  14. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/pre-commit.yml +0 -0
  15. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/prepare-release.yml +0 -0
  16. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/publish.yml +0 -0
  17. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/test-redistribute.yml +0 -0
  18. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/test.yml +0 -0
  19. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.github/workflows/zizmor.yml +0 -0
  20. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.gitignore +0 -0
  21. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.pre-commit-config.yaml +0 -0
  22. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/.python-version +0 -0
  23. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/LICENSE +0 -0
  24. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/calver_app.py +0 -0
  25. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/download_static_assets.py +0 -0
  26. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/mounted_subapps_app.py +0 -0
  27. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/multi_router_app.py +0 -0
  28. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/openapi_hook_app.py +0 -0
  29. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/self_hosted_docs_app.py +0 -0
  30. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/semver_app.py +0 -0
  31. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/semver_major_only_app.py +0 -0
  32. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/validation_error_code_app.py +0 -0
  33. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/validation_error_code_custom_handler_app.py +0 -0
  34. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/examples/webhook_versioning_app.py +0 -0
  35. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/fastapi_router_versioning/py.typed +0 -0
  36. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/pyproject.toml +0 -0
  37. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/scripts/format.sh +0 -0
  38. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/scripts/lint.sh +0 -0
  39. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/scripts/prepare_release.py +0 -0
  40. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/scripts/test-cov-html.sh +0 -0
  41. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/scripts/test-cov.sh +0 -0
  42. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/scripts/test.sh +0 -0
  43. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/tests/__init__.py +0 -0
  44. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/tests/test_calver.py +0 -0
  45. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/tests/test_fastapi_integration.py +0 -0
  46. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/tests/test_prepare_release.py +0 -0
  47. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/tests/test_semver.py +0 -0
  48. {fastapi_router_versioning-0.2.4 → fastapi_router_versioning-0.2.6}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-router-versioning
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Router-based API versioning for FastAPI, with per-version docs and a declarative route lifecycle.
5
5
  Project-URL: Homepage, https://github.com/mat81black/fastapi-router-versioning
6
6
  Project-URL: Repository, https://github.com/mat81black/fastapi-router-versioning
@@ -32,12 +32,12 @@ Description-Content-Type: text/markdown
32
32
  # FastAPI Router Versioning
33
33
 
34
34
  [![Build Status](https://github.com/mat81black/fastapi-router-versioning/workflows/Test/badge.svg)](https://github.com/mat81black/fastapi-router-versioning/actions)
35
- [![codecov](https://codecov.io/github/mat81black/fastapi-validation-override/graph/badge.svg?token=SL4JPWAB0O)](https://codecov.io/github/mat81black/fastapi-validation-override)
36
- [![Package version](https://badge.fury.io/py/fastapi-router-versioning.svg)](https://pypi.org/project/fastapi-router-versioning/)
35
+ [![codecov](https://codecov.io/github/mat81black/fastapi-router-versioning/graph/badge.svg?token=4WQ63Q7ESY)](https://codecov.io/github/mat81black/fastapi-router-versioning)
36
+ [![pypi package](https://img.shields.io/pypi/v/fastapi-router-versioning?color=%2334D058&label=pypi%20package)](https://pypi.org/project/fastapi-router-versioning/)
37
37
  [![Supported Python versions](https://img.shields.io/pypi/pyversions/fastapi-router-versioning.svg?color=%2334D058)](https://pypi.org/project/fastapi-router-versioning/)
38
38
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
39
39
 
40
- FastAPI doesn't ship a way to run multiple API versions side by side. Teams typically end up duplicating routers, hand-rolling prefixes, or branching on request paths, and every one of those gets harder to maintain as versions pile up. `RouterVersioner` takes a declarative approach instead: annotate each route with the version it belongs to, and it generates the URL prefixes, the per-version OpenAPI schema, and the docs for you, without moving anything else in your app.
40
+ Running multiple API versions side by side usually means duplicating routers, hand-rolling prefixes, or branching on request paths, and every one of those gets harder to maintain as versions pile up. `RouterVersioner` takes a declarative approach instead: annotate each route with the version it belongs to, and it generates the URL prefixes, the per-version OpenAPI schema, and the docs for you, without moving anything else in your app.
41
41
 
42
42
  ---
43
43
 
@@ -183,7 +183,9 @@ A route without `@api_version` isn't excluded, it falls back to `default_version
183
183
  | `validation_error_code` | `int` | `422` | Status code returned for request validation errors; also replaces the `422` entry everywhere it appears in the OpenAPI schema, root schema included |
184
184
  | `handle_validation_exceptions` | `bool` | `True` | Set `False` to only patch the schema and register your own `RequestValidationError` handler |
185
185
 
186
- `.versionize()` returns the list of versions it activated.
186
+ `.versionize()` returns the list of versions it activated. It can only be called once per
187
+ instance; a second call raises `RuntimeError`, since it mutates the live FastAPI app in a way
188
+ that can't be undone.
187
189
 
188
190
  ---
189
191
 
@@ -1,12 +1,12 @@
1
1
  # FastAPI Router Versioning
2
2
 
3
3
  [![Build Status](https://github.com/mat81black/fastapi-router-versioning/workflows/Test/badge.svg)](https://github.com/mat81black/fastapi-router-versioning/actions)
4
- [![codecov](https://codecov.io/github/mat81black/fastapi-validation-override/graph/badge.svg?token=SL4JPWAB0O)](https://codecov.io/github/mat81black/fastapi-validation-override)
5
- [![Package version](https://badge.fury.io/py/fastapi-router-versioning.svg)](https://pypi.org/project/fastapi-router-versioning/)
4
+ [![codecov](https://codecov.io/github/mat81black/fastapi-router-versioning/graph/badge.svg?token=4WQ63Q7ESY)](https://codecov.io/github/mat81black/fastapi-router-versioning)
5
+ [![pypi package](https://img.shields.io/pypi/v/fastapi-router-versioning?color=%2334D058&label=pypi%20package)](https://pypi.org/project/fastapi-router-versioning/)
6
6
  [![Supported Python versions](https://img.shields.io/pypi/pyversions/fastapi-router-versioning.svg?color=%2334D058)](https://pypi.org/project/fastapi-router-versioning/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
8
8
 
9
- FastAPI doesn't ship a way to run multiple API versions side by side. Teams typically end up duplicating routers, hand-rolling prefixes, or branching on request paths, and every one of those gets harder to maintain as versions pile up. `RouterVersioner` takes a declarative approach instead: annotate each route with the version it belongs to, and it generates the URL prefixes, the per-version OpenAPI schema, and the docs for you, without moving anything else in your app.
9
+ Running multiple API versions side by side usually means duplicating routers, hand-rolling prefixes, or branching on request paths, and every one of those gets harder to maintain as versions pile up. `RouterVersioner` takes a declarative approach instead: annotate each route with the version it belongs to, and it generates the URL prefixes, the per-version OpenAPI schema, and the docs for you, without moving anything else in your app.
10
10
 
11
11
  ---
12
12
 
@@ -152,7 +152,9 @@ A route without `@api_version` isn't excluded, it falls back to `default_version
152
152
  | `validation_error_code` | `int` | `422` | Status code returned for request validation errors; also replaces the `422` entry everywhere it appears in the OpenAPI schema, root schema included |
153
153
  | `handle_validation_exceptions` | `bool` | `True` | Set `False` to only patch the schema and register your own `RequestValidationError` handler |
154
154
 
155
- `.versionize()` returns the list of versions it activated.
155
+ `.versionize()` returns the list of versions it activated. It can only be called once per
156
+ instance; a second call raises `RuntimeError`, since it mutates the live FastAPI app in a way
157
+ that can't be undone.
156
158
 
157
159
  ---
158
160
 
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Latest Changes
4
4
 
5
+ ## 0.2.6 (2026-07-11)
6
+
7
+ ### Refactors
8
+
9
+ * ♻️ Use fastapi-native imports instead of importing from starlette directly. PR [#38](https://github.com/mat81black/fastapi-router-versioning/pull/38) by [@mat81black](https://github.com/mat81black).
10
+
11
+ ## 0.2.5 (2026-07-10)
12
+
13
+ ### Fixes
14
+
15
+ * 🐛 Fix misleading self-collision error in RouterVersioner prefix claiming. PR [#36](https://github.com/mat81black/fastapi-router-versioning/pull/36) by [@mat81black](https://github.com/mat81black).
16
+
5
17
  ## 0.2.4 (2026-07-09)
6
18
 
7
19
  ### Internal
@@ -1,5 +1,5 @@
1
1
  from .versioner import RouterVersioner, VersionFormat, VersionT, api_version
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.2.6"
4
4
 
5
5
  __all__ = ["RouterVersioner", "api_version", "VersionFormat", "VersionT"]
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ import itertools
2
3
 
3
4
  from collections import defaultdict
4
5
  from collections.abc import Callable, Iterator
@@ -8,7 +9,7 @@ from typing import Any, TypeAlias, TypeVar
8
9
  import fastapi.openapi.utils
9
10
  import fastapi.routing
10
11
 
11
- from fastapi import APIRouter, FastAPI
12
+ from fastapi import APIRouter, FastAPI, Request
12
13
  from fastapi.encoders import jsonable_encoder
13
14
  from fastapi.exceptions import RequestValidationError
14
15
  from fastapi.openapi.docs import (
@@ -18,7 +19,6 @@ from fastapi.openapi.docs import (
18
19
  )
19
20
  from fastapi.responses import HTMLResponse, JSONResponse
20
21
  from fastapi.routing import APIRoute, APIWebSocketRoute
21
- from starlette.requests import Request
22
22
 
23
23
  # iter_route_contexts was introduced in FastAPI 0.137.2. On older versions the
24
24
  # attribute does not exist and getattr returns None, activating the fallback path.
@@ -33,6 +33,12 @@ _ATTR_API_VERSION = "_api_version"
33
33
  _ATTR_DEPRECATE_IN = "_deprecate_in_version"
34
34
  _ATTR_REMOVE_IN = "_remove_in_version"
35
35
 
36
+ # Unique per-instance token for _claim_prefix, so it can tell apart a prefix collision
37
+ # caused by this same RouterVersioner instance (e.g. a degenerate prefix_format) from one
38
+ # caused by a different instance sharing the app. Not id(self): that can be reused after
39
+ # garbage collection for the common `RouterVersioner(...).versionize()` one-liner pattern.
40
+ _instance_counter = itertools.count()
41
+
36
42
 
37
43
  class VersionFormat(str, Enum):
38
44
  """
@@ -194,6 +200,8 @@ class RouterVersioner:
194
200
 
195
201
  self._validation_error_code = validation_error_code
196
202
  self._handle_validation_exceptions = handle_validation_exceptions
203
+ self._versionized = False
204
+ self._instance_token = next(_instance_counter)
197
205
 
198
206
  if self._validation_error_code != 422 and self._handle_validation_exceptions:
199
207
  self._register_validation_exception_handler()
@@ -345,6 +353,24 @@ class RouterVersioner:
345
353
  return None
346
354
 
347
355
  def versionize(self) -> list[VersionT]:
356
+ """
357
+ Reads the configured routers, groups their routes by version, and mounts one
358
+ versioned router per active version on the app.
359
+
360
+ May only be called once per instance: it mutates the live FastAPI app (including
361
+ routers, registering routes) in a way that cannot be undone, so a second call
362
+ would either duplicate routes or, if a version prefix was already claimed, raise.
363
+ Create a new RouterVersioner if you need to versionize again.
364
+
365
+ :return: The list of versions that were actually mounted.
366
+ """
367
+ if self._versionized:
368
+ raise RuntimeError(
369
+ "versionize() was already called on this RouterVersioner instance. "
370
+ "Create a new RouterVersioner if you need to versionize a router again."
371
+ )
372
+ self._versionized = True
373
+
348
374
  latest_version: VersionT | None = None
349
375
  latest_routes: dict[tuple[str, str], Any] = {}
350
376
  latest_webhooks: list[Any] = []
@@ -450,19 +476,27 @@ class RouterVersioner:
450
476
  return {version: list(routes.values()) for version, routes in self._resolve_lifecycle(all_webhooks).items()}
451
477
 
452
478
  def _claim_prefix(self, prefix: str) -> None:
453
- claimed: set[str] | None = getattr(self._app.state, "_router_versioner_claimed_prefixes", None)
479
+ claimed: dict[str, int] | None = getattr(self._app.state, "_router_versioner_claimed_prefixes", None)
454
480
  if claimed is None:
455
- claimed = set()
481
+ claimed = {}
456
482
  self._app.state._router_versioner_claimed_prefixes = claimed
457
483
 
458
- if prefix in claimed:
484
+ owner = claimed.get(prefix)
485
+ if owner is not None:
486
+ if owner == self._instance_token:
487
+ raise RuntimeError(
488
+ f"Prefix '{prefix}' was already claimed by this same RouterVersioner instance. "
489
+ "This usually means prefix_format doesn't use {major}/{minor}/{version} and "
490
+ "produces the same prefix for multiple versions, or latest_prefix collides "
491
+ "with an already-active version prefix."
492
+ )
459
493
  raise RuntimeError(
460
494
  f"Prefix '{prefix}' is already used by another RouterVersioner attached to this app. "
461
495
  "Two RouterVersioner instances sharing the same app must use distinct "
462
496
  "prefix_format/latest_prefix values, otherwise their docs/openapi routes silently "
463
497
  "shadow each other."
464
498
  )
465
- claimed.add(prefix)
499
+ claimed[prefix] = self._instance_token
466
500
 
467
501
  def _resolve_webhooks_for_version(
468
502
  self, version: VersionT, webhooks_by_version: dict[VersionT, list[Any]]
@@ -687,6 +687,29 @@ def test_default_version_mixed_with_explicitly_decorated_routes() -> None:
687
687
  assert client.get("/v3_0/explicit").status_code == 200
688
688
 
689
689
 
690
+ def test_routers_as_list() -> None:
691
+ """routers accepts a list of APIRouter, not just a single one (multi-router support)."""
692
+ app = FastAPI()
693
+ router1 = APIRouter()
694
+ router2 = APIRouter()
695
+
696
+ @router1.get("/a")
697
+ @api_version((1, 0))
698
+ def route_a() -> dict[str, str]:
699
+ return {"msg": "a"}
700
+
701
+ @router2.get("/b")
702
+ @api_version((1, 0))
703
+ def route_b() -> dict[str, str]:
704
+ return {"msg": "b"}
705
+
706
+ RouterVersioner(app=app, routers=[router1, router2], version_format=VersionFormat.SEMVER).versionize()
707
+
708
+ client = TestClient(app)
709
+ assert client.get("/v1_0/a").status_code == 200
710
+ assert client.get("/v1_0/b").status_code == 200
711
+
712
+
690
713
  def test_openapi_callbacks_are_propagated_to_versioned_routes() -> None:
691
714
  """OpenAPI callbacks defined on a route are propagated to every versioned copy of that route."""
692
715
  app = FastAPI()
@@ -815,6 +838,38 @@ def test_webhook_routers_none_falls_back_to_app_webhooks() -> None:
815
838
  assert "/global-event" in schema_v1.get("webhooks", {})
816
839
 
817
840
 
841
+ def test_webhook_routers_as_list() -> None:
842
+ """webhook_routers accepts a list of APIRouter, not just a single one."""
843
+ app = FastAPI()
844
+ router = APIRouter()
845
+ webhook_router1 = APIRouter()
846
+ webhook_router2 = APIRouter()
847
+
848
+ @router.get("/items")
849
+ @api_version((1, 0))
850
+ def get_items() -> dict[str, str]: ...
851
+
852
+ @webhook_router1.post("/hook-a")
853
+ @api_version((1, 0))
854
+ def hook_a(body: dict[str, str]) -> None: ...
855
+
856
+ @webhook_router2.post("/hook-b")
857
+ @api_version((1, 0))
858
+ def hook_b(body: dict[str, str]) -> None: ...
859
+
860
+ RouterVersioner(
861
+ app=app,
862
+ routers=router,
863
+ webhook_routers=[webhook_router1, webhook_router2],
864
+ version_format=VersionFormat.SEMVER,
865
+ ).versionize()
866
+
867
+ client = TestClient(app)
868
+ schema = client.get("/v1_0/openapi.json").json()
869
+ assert "/hook-a" in schema.get("webhooks", {})
870
+ assert "/hook-b" in schema.get("webhooks", {})
871
+
872
+
818
873
  def test_webhook_routers_no_webhook_before_first_version() -> None:
819
874
  """When all webhook versions are higher than the current route version, returns no webhooks."""
820
875
  app = FastAPI()
@@ -1281,6 +1336,59 @@ def test_duplicate_version_prefix_across_versioners_raises() -> None:
1281
1336
  RouterVersioner(app=app, routers=router2, version_format=VersionFormat.SEMVER).versionize()
1282
1337
 
1283
1338
 
1339
+ def test_degenerate_prefix_format_self_collision_raises() -> None:
1340
+ """A prefix_format without {major}/{minor}/{version} placeholders makes every version
1341
+ resolve to the same prefix. This is a self-collision on a single instance, not a clash
1342
+ with another RouterVersioner, so the error message must say so explicitly."""
1343
+ app = FastAPI()
1344
+ router = APIRouter()
1345
+
1346
+ @router.get("/items")
1347
+ @api_version((1, 0))
1348
+ def get_items_v1() -> dict[str, str]: ...
1349
+
1350
+ @router.get("/items")
1351
+ @api_version((2, 0))
1352
+ def get_items_v2() -> dict[str, str]: ...
1353
+
1354
+ with pytest.raises(RuntimeError, match="already claimed by this same RouterVersioner instance"):
1355
+ RouterVersioner(app=app, routers=router, version_format=VersionFormat.SEMVER, prefix_format="/api").versionize()
1356
+
1357
+
1358
+ def test_latest_prefix_collides_with_own_version_prefix_raises() -> None:
1359
+ """latest_prefix accidentally set to the same value as an actual version's own prefix is
1360
+ a self-collision on a single instance, not a clash with another RouterVersioner."""
1361
+ app = FastAPI()
1362
+ router = APIRouter()
1363
+
1364
+ @router.get("/items")
1365
+ @api_version((1, 0))
1366
+ def get_items() -> dict[str, str]: ...
1367
+
1368
+ with pytest.raises(RuntimeError, match="already claimed by this same RouterVersioner instance"):
1369
+ RouterVersioner(
1370
+ app=app, routers=router, version_format=VersionFormat.SEMVER, latest_prefix="/v1_0"
1371
+ ).versionize()
1372
+
1373
+
1374
+ def test_calling_versionize_twice_on_same_instance_raises() -> None:
1375
+ """Calling .versionize() a second time on the same instance must raise a clear,
1376
+ self-explanatory error instead of the misleading '...used by another RouterVersioner'
1377
+ message that _claim_prefix would otherwise produce (it's the same instance, not another)."""
1378
+ app = FastAPI()
1379
+ router = APIRouter()
1380
+
1381
+ @router.get("/items")
1382
+ @api_version((1, 0))
1383
+ def get_items() -> dict[str, str]: ...
1384
+
1385
+ versioner = RouterVersioner(app=app, routers=router, version_format=VersionFormat.SEMVER)
1386
+ versioner.versionize()
1387
+
1388
+ with pytest.raises(RuntimeError, match="versionize\\(\\) was already called on this RouterVersioner instance"):
1389
+ versioner.versionize()
1390
+
1391
+
1284
1392
  def test_duplicate_latest_prefix_across_versioners_raises() -> None:
1285
1393
  """Two RouterVersioners sharing the same latest_prefix on the same app must fail fast."""
1286
1394
  app = FastAPI()