Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion ninja/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,8 +385,9 @@ def urls_paths(self, prefix: str) -> Iterator[URLPattern]:
# Ensure decorators are applied before generating URLs
self._apply_decorators_to_operations()

all_path_operations = self._get_all_path_operations(prefix)
prefix = replace_path_param_notation(prefix)
for path, path_view in self.path_operations.items():
for path, path_view in all_path_operations.items():
for operation in path_view.operations:
path = replace_path_param_notation(path)
route = "/".join([i for i in (prefix, path) if i])
Expand All @@ -400,6 +401,43 @@ def urls_paths(self, prefix: str) -> Iterator[URLPattern]:

yield django_path(route, path_view.get_view(), name=url_name)

def _get_all_path_operations(self, prefix: str) -> Dict[str, Any]:
all_path_operations = dict(self.path_operations)

if not self.api:
return all_path_operations # pragma: no cover

current_prefix_norm = normalize_path(prefix).lstrip("/")

for other_prefix, other_router in self.api._routers:
if other_router == self:
continue

other_prefix_norm = normalize_path(other_prefix).lstrip("/")
if other_prefix_norm != current_prefix_norm:
continue

# merge operations from router with same normalized prefix
for path, other_path_view in other_router.path_operations.items():
if path not in all_path_operations:
all_path_operations[path] = other_path_view
continue

# merge operations for same path
existing_methods = {
m for op in all_path_operations[path].operations for m in op.methods
}
for operation in other_path_view.operations:
if any(m in existing_methods for m in operation.methods):
continue

all_path_operations[path].operations.append(operation)
all_path_operations[path].is_async = (
all_path_operations[path].is_async or operation.is_async
)

return all_path_operations

def add_router(
self,
prefix: str,
Expand Down
114 changes: 114 additions & 0 deletions tests/test_router_multiple_methods_same_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from ninja import NinjaAPI, Router
from ninja.testing import TestClient


def test_multiple_routers_same_path_different_methods():
api = NinjaAPI()

router1 = Router()

@router1.get("/items")
def get_items(request):
return {"method": "GET", "router": 1}

@router1.post("/items")
def create_item(request):
return {"method": "POST", "router": 1}

router2 = Router()

@router2.put("/items")
def update_item(request):
return {"method": "PUT", "router": 2}

@router2.delete("/items")
def delete_item(request):
return {"method": "DELETE", "router": 2}

api.add_router("", router1)
api.add_router("", router2)

client = TestClient(api)

response = client.get("/items")
assert response.status_code == 200
assert response.json() == {"method": "GET", "router": 1}

response = client.post("/items")
assert response.status_code == 200
assert response.json() == {"method": "POST", "router": 1}

response = client.put("/items")
assert response.status_code == 200
assert response.json() == {"method": "PUT", "router": 2}

response = client.delete("/items")
assert response.status_code == 200
assert response.json() == {"method": "DELETE", "router": 2}

# unsupported method returns 405
response = client.patch("/items")
assert response.status_code == 405


def test_api_and_router_same_path_different_methods():
api = NinjaAPI()

@api.get("/users")
def get_users(request):
return {"method": "GET", "source": "api"}

router = Router()

@router.put("/users")
def update_user(request):
return {"method": "PUT", "source": "router"}

@router.delete("/users")
def delete_user(request):
return {"method": "DELETE", "source": "router"}

api.add_router("", router)

client = TestClient(api)

response = client.get("/users")
assert response.status_code == 200
assert response.json() == {"method": "GET", "source": "api"}

response = client.put("/users")
assert response.status_code == 200
assert response.json() == {"method": "PUT", "source": "router"}

response = client.delete("/users")
assert response.status_code == 200
assert response.json() == {"method": "DELETE", "source": "router"}

# unsupported method returns 405
response = client.post("/users")
assert response.status_code == 405


def test_overlapping_methods_different_routers():
api = NinjaAPI()

router1 = Router()

@router1.get("/overlap")
def get_overlap_1(request):
return {"source": "router1"}

router2 = Router()

@router2.get("/overlap")
def get_overlap_2(request):
return {"source": "router2"}

api.add_router("", router1)
api.add_router("", router2)

client = TestClient(api)

# first router's handler should be used
response = client.get("/overlap")
assert response.status_code == 200