Skip to content

Commit a4267e7

Browse files
Enhance api_endpoint model to track the tree structure of an api, and add router settings for a better integration of viewsets.
1 parent 4cb10da commit a4267e7

File tree

4 files changed

+178
-32
lines changed

4 files changed

+178
-32
lines changed

rest_framework_docs/api_docs.py

+111-17
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
from importlib import import_module
2+
from types import ModuleType
23
from django.conf import settings
34
from django.core.urlresolvers import RegexURLResolver, RegexURLPattern
45
from django.utils.module_loading import import_string
56
from rest_framework.views import APIView
6-
from rest_framework_docs.api_endpoint import ApiEndpoint
7+
from rest_framework.routers import BaseRouter
8+
from rest_framework_docs.api_endpoint import ApiNode, ApiEndpoint
9+
from rest_framework_docs.settings import DRFSettings
10+
11+
12+
drf_settings = DRFSettings().settings
713

814

915
class ApiDocumentation(object):
1016

1117
def __init__(self, drf_router=None):
1218
self.endpoints = []
1319
self.drf_router = drf_router
20+
1421
try:
1522
root_urlconf = import_string(settings.ROOT_URLCONF)
1623
except ImportError:
@@ -21,26 +28,113 @@ def __init__(self, drf_router=None):
2128
else:
2229
self.get_all_view_names(root_urlconf.urlpatterns)
2330

24-
def get_all_view_names(self, urlpatterns, parent_pattern=None):
31+
def get_all_view_names(self, urlpatterns, parent_api_node=None):
2532
for pattern in urlpatterns:
2633
if isinstance(pattern, RegexURLResolver):
27-
parent_pattern = None if pattern._regex == "^" else pattern
28-
self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_pattern=parent_pattern)
29-
elif isinstance(pattern, RegexURLPattern) and self._is_drf_view(pattern) and not self._is_format_endpoint(pattern):
30-
api_endpoint = ApiEndpoint(pattern, parent_pattern, self.drf_router)
34+
# Try to get router from settings, if no router is found,
35+
# Use the instance drf_router property.
36+
router = get_router(pattern)
37+
if router is None:
38+
parent_router = None
39+
if parent_api_node is not None:
40+
parent_router = parent_api_node.drf_router
41+
if parent_router is not None:
42+
router = parent_router
43+
else:
44+
router = self.drf_router
45+
if pattern._regex == "^":
46+
parent = parent_api_node
47+
else:
48+
parent = ApiNode(
49+
pattern,
50+
parent_node=parent_api_node,
51+
drf_router=router
52+
)
53+
self.get_all_view_names(urlpatterns=pattern.url_patterns, parent_api_node=parent)
54+
elif isinstance(pattern, RegexURLPattern) and _is_drf_view(pattern) and not _is_format_endpoint(pattern):
55+
router = parent_api_node.drf_router
56+
router = self.drf_router if router is None else router
57+
api_endpoint = ApiEndpoint(pattern, parent_api_node, router)
3158
self.endpoints.append(api_endpoint)
3259

33-
def _is_drf_view(self, pattern):
34-
"""
35-
Should check whether a pattern inherits from DRF's APIView
36-
"""
37-
return hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls, APIView)
60+
def get_endpoints(self):
61+
return self.endpoints
3862

39-
def _is_format_endpoint(self, pattern):
40-
"""
41-
Exclude endpoints with a "format" parameter
63+
64+
def _is_drf_view(pattern):
65+
"""
66+
Should check whether a pattern inherits from DRF's APIView
67+
"""
68+
return hasattr(pattern.callback, 'cls') and issubclass(pattern.callback.cls,
69+
APIView)
70+
71+
72+
def _is_format_endpoint(pattern):
73+
"""
74+
Exclude endpoints with a "format" parameter
75+
"""
76+
return '?P<format>' in pattern._regex
77+
78+
79+
def get_router(pattern):
80+
urlconf = pattern.urlconf_name
81+
router = None
82+
if isinstance(urlconf, ModuleType):
83+
# First: try MODULE_ROUTERS setting - Don't ignore errors
84+
router = get_module_router(urlconf)
85+
if router is not None:
86+
return router
87+
# Second: try DEFAULT_MODULE_ROUTER setting - Ignore errors
88+
try:
89+
router = get_default_module_router(urlconf)
90+
if router is not None:
91+
return router
92+
except:
93+
pass
94+
# Third: try DEFAULT_ROUTER setting - Don't ignore errors
95+
router = get_default_router()
96+
if router is not None:
97+
return router
98+
return router
99+
100+
101+
def get_module_router(module):
102+
routers = drf_settings['MODULE_ROUTERS']
103+
if routers is None:
104+
return None
105+
if module.__name__ in routers:
106+
router_name = routers[module.__name__]
107+
router = getattr(module, router_name)
108+
assert isinstance(router, BaseRouter), \
109+
"""
110+
drfdocs 'ROUTERS' setting does not correspond to
111+
a router instance for module {}.
112+
""".format(module.__name__)
113+
return router
114+
return None
115+
116+
117+
def get_default_module_router(module):
118+
default_module_router = drf_settings['DEFAULT_MODULE_ROUTER']
119+
if default_module_router is None:
120+
return None
121+
router = getattr(module, default_module_router)
122+
assert isinstance(router, BaseRouter), \
42123
"""
43-
return '?P<format>' in pattern._regex
124+
drfdocs 'DEFAULT_MODULE_ROUTER' setting does not correspond to
125+
a router instance for module {}.
126+
""".format(module.__name__)
127+
return router
44128

45-
def get_endpoints(self):
46-
return self.endpoints
129+
130+
def get_default_router():
131+
default_router_path = drf_settings['DEFAULT_ROUTER']
132+
if default_router_path is None:
133+
return None
134+
router = import_string(default_router_path)
135+
assert isinstance(router, BaseRouter), \
136+
"""
137+
drfdocs 'DEFAULT_ROUTER_NAME' setting does not correspond to
138+
a router instance {}.
139+
""".format(router.__name__)
140+
return router

rest_framework_docs/api_endpoint.py

+62-13
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,73 @@
55
from rest_framework.serializers import BaseSerializer
66

77

8-
class ApiEndpoint(object):
8+
class ApiNode(object):
99

10-
def __init__(self, pattern, parent_pattern=None, drf_router=None):
11-
self.drf_router = drf_router
10+
def __init__(self, pattern, parent_node=None, drf_router=None):
1211
self.pattern = pattern
12+
self.parent_node = parent_node
13+
self.drf_router = drf_router
14+
15+
@property
16+
def parent_pattern(self):
17+
if self.parent_node is None:
18+
return None
19+
return self.parent_node.pattern
20+
21+
@property
22+
def name_parent(self):
23+
if self.parent_pattern is None:
24+
return None
25+
return simplify_regex(self.parent_pattern.regex.pattern).strip('/')
26+
27+
@property
28+
def name_parent_full(self):
29+
name_parent_full = self.name_parent
30+
if name_parent_full is None:
31+
return None
32+
parent_node = self.parent_node
33+
parent_name = parent_node.name_parent
34+
while parent_name is not None:
35+
name_parent_full = "{}/{}".format(parent_name, name_parent_full)
36+
parent_node = parent_node.parent_node
37+
parent_name = parent_node.name_parent
38+
return name_parent_full
39+
40+
@property
41+
def path(self):
42+
pattern = self.__get_path__(self.parent_pattern)
43+
if pattern is None:
44+
return None
45+
parent = self.parent_node
46+
name_parent = None if parent is None else parent.name_parent
47+
while name_parent is not None:
48+
pattern = "{}/{}".format(name_parent, pattern)
49+
parent = parent.parent_node
50+
name_parent = parent.name_parent
51+
if pattern[0] != "/":
52+
pattern = "/{}".format(pattern)
53+
return pattern
54+
55+
def __get_path__(self, parent_pattern):
56+
if parent_pattern:
57+
return "{0}{1}".format(
58+
self.name_parent,
59+
simplify_regex(self.pattern.regex.pattern)
60+
)
61+
return simplify_regex(self.pattern.regex.pattern)
62+
63+
64+
class ApiEndpoint(ApiNode):
65+
66+
def __init__(self, pattern, parent_node=None, drf_router=None):
67+
super(ApiEndpoint, self).__init__(
68+
pattern,
69+
parent_node=parent_node,
70+
drf_router=drf_router
71+
)
1372
self.callback = pattern.callback
14-
# self.name = pattern.name
1573
self.docstring = self.__get_docstring__()
16-
self.name_parent = simplify_regex(parent_pattern.regex.pattern).strip('/') if parent_pattern else None
17-
self.path = self.__get_path__(parent_pattern)
1874
self.allowed_methods = self.__get_allowed_methods__()
19-
# self.view_name = pattern.callback.__name__
2075
self.errors = None
2176
self.serializer_class = self.__get_serializer_class__()
2277
if self.serializer_class:
@@ -26,13 +81,7 @@ def __init__(self, pattern, parent_pattern=None, drf_router=None):
2681

2782
self.permissions = self.__get_permissions_class__()
2883

29-
def __get_path__(self, parent_pattern):
30-
if parent_pattern:
31-
return "/{0}{1}".format(self.name_parent, simplify_regex(self.pattern.regex.pattern))
32-
return simplify_regex(self.pattern.regex.pattern)
33-
3484
def __get_allowed_methods__(self):
35-
3685
viewset_methods = []
3786
if self.drf_router:
3887
for prefix, viewset, basename in self.drf_router.registry:

rest_framework_docs/settings.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ class DRFSettings(object):
55

66
def __init__(self):
77
self.drf_settings = {
8-
"HIDE_DOCS": self.get_setting("HIDE_DOCS") or False
8+
"HIDE_DOCS": self.get_setting("HIDE_DOCS") or False,
9+
"MODULE_ROUTERS": self.get_setting("MODULE_ROUTERS"),
10+
"DEFAULT_MODULE_ROUTER": self.get_setting("DEFAULT_MODULE_ROUTER"),
11+
"DEFAULT_ROUTER": self.get_setting("DEFAULT_ROUTER")
912
}
1013

1114
def get_setting(self, name):

rest_framework_docs/templates/rest_framework_docs/home.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
{% block content %}
1717

18-
{% regroup endpoints by name_parent as endpoints_grouped %}
18+
{% regroup endpoints by name_parent_full as endpoints_grouped %}
1919

2020
{% if endpoints_grouped %}
2121
{% for group in endpoints_grouped %}

0 commit comments

Comments
 (0)