Merge ~ack/maas:1949963-api-description-stable-sorting into maas:master

Proposed by Alberto Donato
Status: Merged
Approved by: Alberto Donato
Approved revision: 6b5ab5590e107aa65deaada1e0e63fb050a6ce4b
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ack/maas:1949963-api-description-stable-sorting
Merge into: maas:master
Diff against target: 333 lines (+87/-58)
5 files modified
src/maasserver/api/doc.py (+43/-33)
src/maasserver/api/doc_handler.py (+3/-2)
src/maasserver/forms/settings.py (+14/-17)
src/maasserver/forms/tests/test_settings.py (+26/-5)
src/maasserver/urls_api.py (+1/-1)
Reviewer Review Type Date Requested Status
Björn Tillenius Approve
MAAS Lander Approve
Review via email: mp+411447@code.launchpad.net

Commit message

LP:1949963 ensure form choices are formatted in order in doc text

To post a comment you must log in.
Revision history for this message
Alberto Donato (ack) :
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b 1949963-api-description-stable-sorting lp:~ack/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/11469/console
COMMIT: 84aaeaa007c67b1431d8e254293dbd8b4951d876

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b 1949963-api-description-stable-sorting lp:~ack/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 6b5ab5590e107aa65deaada1e0e63fb050a6ce4b

review: Approve
Revision history for this message
Björn Tillenius (bjornt) wrote :

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/api/doc.py b/src/maasserver/api/doc.py
2index b22251f..3b777aa 100644
3--- a/src/maasserver/api/doc.py
4+++ b/src/maasserver/api/doc.py
5@@ -19,6 +19,7 @@ from inspect import getdoc
6 from io import StringIO
7 from itertools import zip_longest
8 import json
9+from operator import itemgetter
10 from threading import RLock
11
12 from django.urls import get_resolver
13@@ -44,25 +45,23 @@ def accumulate_api_resources(resolver, accumulator):
14 :rtype: Generator, yielding handlers.
15 """
16
17- def p_has_resource_uri(resource):
18- return getattr(resource.handler, "resource_uri", None) is not None
19-
20- def p_is_not_hidden(resource):
21- return getattr(resource.handler, "hidden", False)
22+ def visible(handler):
23+ return bool(
24+ getattr(handler, "resource_uri", None)
25+ and not getattr(handler, "hidden", False)
26+ )
27
28 for pattern in resolver.url_patterns:
29 if isinstance(pattern, URLResolver):
30 accumulate_api_resources(pattern, accumulator)
31 elif isinstance(pattern, URLPattern):
32- if isinstance(pattern.callback, Resource):
33- resource = pattern.callback
34- if p_has_resource_uri(resource) and not p_is_not_hidden(
35- resource
36- ):
37- accumulator.add(resource)
38+ if isinstance(pattern.callback, Resource) and visible(
39+ pattern.callback.handler
40+ ):
41+ accumulator.add(pattern.callback)
42 else:
43 raise AssertionError(
44- "Not a recognised pattern or resolver: %r" % (pattern,)
45+ f"Not a recognised pattern or resolver: {pattern!r}"
46 )
47
48
49@@ -228,10 +227,18 @@ def describe_actions(handler):
50 restful: Indicates if this is a CRUD/ReSTful action.
51
52 """
53- from maasserver.api import support # Circular import.
54+ from maasserver.api import support
55
56 getname = support.OperationsResource.crudmap.get
57- for signature, function in handler.exports.items():
58+
59+ actions = []
60+
61+ # ensure stable sorting, accounting for operation being None
62+ exports = sorted(
63+ handler.exports.items(),
64+ key=lambda item: (item[0][0], item[0][1] or ""),
65+ )
66+ for signature, function in exports:
67 http_method, operation = signature
68 name = getname(http_method) if operation is None else operation
69
70@@ -276,13 +283,16 @@ def describe_actions(handler):
71 )
72 doc += ":type %s: %s\n\n " % (pname, param["type"])
73
74- yield dict(
75- method=http_method,
76- name=name,
77- doc=doc,
78- op=operation,
79- restful=(operation is None),
80+ actions.append(
81+ {
82+ "method": http_method,
83+ "name": name,
84+ "doc": doc,
85+ "op": operation,
86+ "restful": operation is None,
87+ }
88 )
89+ return sorted(actions, key=itemgetter("name"))
90
91
92 def describe_handler(handler):
93@@ -306,7 +316,7 @@ def describe_handler(handler):
94 )
95
96 return {
97- "actions": list(describe_actions(handler)),
98+ "actions": describe_actions(handler),
99 "doc": getdoc(handler),
100 "name": handler.__name__,
101 "params": tuple(uri_params),
102@@ -345,26 +355,30 @@ def describe_api():
103 """
104 from maasserver import urls_api as urlconf
105
106+ resources = sorted(
107+ (
108+ describe_resource(resource)
109+ for resource in find_api_resources(urlconf)
110+ ),
111+ key=itemgetter("name"),
112+ )
113+
114 # This is the core of it:
115 description = {
116 "doc": "MAAS API",
117- "resources": [
118- describe_resource(resource)
119- for resource in find_api_resources(urlconf)
120- ],
121+ "resources": resources,
122 }
123-
124 # However, for backward compatibility, add "handlers" as an alias for all
125 # not-None anon and auth handlers in "resources".
126 description["handlers"] = []
127 description["handlers"].extend(
128 resource["anon"]
129- for resource in description["resources"]
130+ for resource in resources
131 if resource["anon"] is not None
132 )
133 description["handlers"].extend(
134 resource["auth"]
135- for resource in description["resources"]
136+ for resource in resources
137 if resource["auth"] is not None
138 )
139
140@@ -537,11 +551,7 @@ def hash_canonical(description):
141 into a new `hashlib.sha1` object.
142 """
143 description = describe_canonical(description)
144- description_as_json = json.dumps(description)
145- # Python 3's json.dumps returns a `str`, so encode if necessary.
146- if not isinstance(description_as_json, bytes):
147- description_as_json = description_as_json.encode("ascii")
148- # We /could/ instead pass a hashing object in and call .update()...
149+ description_as_json = json.dumps(description).encode("ascii")
150 return hashlib.sha1(description_as_json)
151
152
153diff --git a/src/maasserver/api/doc_handler.py b/src/maasserver/api/doc_handler.py
154index 9b332c7..e9e1f4a 100644
155--- a/src/maasserver/api/doc_handler.py
156+++ b/src/maasserver/api/doc_handler.py
157@@ -235,11 +235,12 @@ def describe(request):
158 # The handler URIs returned by describe_resource() are relative paths.
159 absolute = partial(build_absolute_uri, request)
160 for resource in description["resources"]:
161- for handler_type in "anon", "auth":
162+ for handler_type in ("anon", "auth"):
163 handler = resource[handler_type]
164 if handler is not None:
165 handler["uri"] = absolute(handler["path"])
166 # Return as a JSON document.
167 return HttpResponse(
168- json.dumps(description), content_type="application/json"
169+ json.dumps(description, sort_keys=True),
170+ content_type="application/json",
171 )
172diff --git a/src/maasserver/forms/settings.py b/src/maasserver/forms/settings.py
173index 10ab368..5a662a2 100644
174--- a/src/maasserver/forms/settings.py
175+++ b/src/maasserver/forms/settings.py
176@@ -75,7 +75,7 @@ def make_default_osystem_field(*args, **kwargs):
177 "osystem", os_choices
178 )
179 },
180- **kwargs
181+ **kwargs,
182 )
183 return field
184
185@@ -154,7 +154,7 @@ def make_default_distro_series_field(*args, **kwargs):
186 "release", release_choices
187 )
188 },
189- **kwargs
190+ **kwargs,
191 )
192 return field
193
194@@ -186,7 +186,7 @@ def make_default_min_hwe_kernel_field(*args, **kwargs):
195 "default_min_hwe_kernel", kernel_choices
196 )
197 },
198- **kwargs
199+ **kwargs,
200 )
201 return field
202
203@@ -206,7 +206,7 @@ def make_commissioning_distro_series_field(*args, **kwargs):
204 "commissioning_distro_series", commissioning_choices
205 )
206 },
207- **kwargs
208+ **kwargs,
209 )
210 return field
211
212@@ -221,7 +221,7 @@ def make_dnssec_validation_field(*args, **kwargs):
213 "dnssec_validation", DNSSEC_VALIDATION_CHOICES
214 )
215 },
216- **kwargs
217+ **kwargs,
218 )
219 return field
220
221@@ -236,7 +236,7 @@ def make_network_discovery_field(*args, **kwargs):
222 "network_discovery", NETWORK_DISCOVERY_CHOICES
223 )
224 },
225- **kwargs
226+ **kwargs,
227 )
228 return field
229
230@@ -251,7 +251,7 @@ def make_active_discovery_interval_field(*args, **kwargs):
231 "active_discovery_interval", ACTIVE_DISCOVERY_INTERVAL_CHOICES
232 )
233 },
234- **kwargs
235+ **kwargs,
236 )
237 return field
238
239@@ -998,17 +998,12 @@ def get_config_form(config_name, data=None):
240 return form
241
242
243-def describe_choices(choices):
244- """Describe the items in an enumeration of Django form choices."""
245- return ", ".join(
246- "'%s' (%s)" % (value, meaning) for value, meaning in choices
247- )
248-
249-
250-def get_config_doc(indentation=0):
251+def get_config_doc(config_items=None, indentation=0):
252 """Return the documentation about the available configuration settings."""
253+ if config_items is None:
254+ config_items = CONFIG_ITEMS
255 doc = ["Available configuration items:\n\n"]
256- for config_name, config_details in sorted(CONFIG_ITEMS.items()):
257+ for config_name, config_details in sorted(config_items.items()):
258 form_details = config_details["form_kwargs"]
259 doc.append(":" + config_name + ": " + form_details["label"] + ". ")
260 # Append help text if present.
261@@ -1018,7 +1013,9 @@ def get_config_doc(indentation=0):
262 # Append list of possible choices if present.
263 choices = form_details.get("choices")
264 if choices is not None:
265- choice_descr = describe_choices(choices)
266+ choice_descr = ", ".join(
267+ f"'{value}' ({meaning})" for value, meaning in sorted(choices)
268+ )
269 doc.append("Available choices are: %s." % choice_descr)
270 doc.append("\n")
271 full_text = (" " * indentation).join(doc)
272diff --git a/src/maasserver/forms/tests/test_settings.py b/src/maasserver/forms/tests/test_settings.py
273index a1dcc23..f0cd075 100644
274--- a/src/maasserver/forms/tests/test_settings.py
275+++ b/src/maasserver/forms/tests/test_settings.py
276@@ -1,8 +1,7 @@
277 # Copyright 2013-2016 Canonical Ltd. This software is licensed under the
278 # GNU Affero General Public License version 3 (see the file LICENSE).
279
280-"""Test forms settings."""
281-
282+from textwrap import dedent
283
284 from django import forms
285 from django.core.exceptions import ValidationError
286@@ -41,9 +40,31 @@ class TestGetConfigForm(MAASServerTestCase):
287
288 class TestGetConfigDoc(MAASServerTestCase):
289 def test_get_config_doc(self):
290- doc = get_config_doc()
291- # Just make sure that the doc looks okay.
292- self.assertIn("maas_name", doc)
293+ config_items = {
294+ "testitem": {
295+ "default": "foo",
296+ "form": forms.CharField,
297+ "form_kwargs": {
298+ "label": "bar",
299+ "choices": [
300+ ("b", "B"),
301+ ("a", "A"),
302+ ],
303+ },
304+ },
305+ }
306+ doc = get_config_doc(config_items=config_items)
307+ # choices are returned in the correct order
308+ self.assertEqual(
309+ doc,
310+ dedent(
311+ """\
312+ Available configuration items:
313+
314+ :testitem: bar. Available choices are: 'a' (A), 'b' (B).
315+ """
316+ ),
317+ )
318
319
320 class TestSpecificConfigSettings(MAASServerTestCase):
321diff --git a/src/maasserver/urls_api.py b/src/maasserver/urls_api.py
322index ff6bf95..bbf76a1 100644
323--- a/src/maasserver/urls_api.py
324+++ b/src/maasserver/urls_api.py
325@@ -566,7 +566,7 @@ urlpatterns += [
326 ),
327 url(r"^vlans/(?P<vlan_id>[^/]+)/$", vlan_handler, name="vlanid_handler"),
328 url(
329- r"fabrics/(?P<fabric_id>[^/]+)/vlans/(?P<vid>[^/]+)/$",
330+ r"^fabrics/(?P<fabric_id>[^/]+)/vlans/(?P<vid>[^/]+)/$",
331 vlan_handler,
332 name="vlan_handler",
333 ),

Subscribers

People subscribed via source and target branches