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
diff --git a/src/maasserver/api/doc.py b/src/maasserver/api/doc.py
index b22251f..3b777aa 100644
--- a/src/maasserver/api/doc.py
+++ b/src/maasserver/api/doc.py
@@ -19,6 +19,7 @@ from inspect import getdoc
19from io import StringIO19from io import StringIO
20from itertools import zip_longest20from itertools import zip_longest
21import json21import json
22from operator import itemgetter
22from threading import RLock23from threading import RLock
2324
24from django.urls import get_resolver25from django.urls import get_resolver
@@ -44,25 +45,23 @@ def accumulate_api_resources(resolver, accumulator):
44 :rtype: Generator, yielding handlers.45 :rtype: Generator, yielding handlers.
45 """46 """
4647
47 def p_has_resource_uri(resource):48 def visible(handler):
48 return getattr(resource.handler, "resource_uri", None) is not None49 return bool(
4950 getattr(handler, "resource_uri", None)
50 def p_is_not_hidden(resource):51 and not getattr(handler, "hidden", False)
51 return getattr(resource.handler, "hidden", False)52 )
5253
53 for pattern in resolver.url_patterns:54 for pattern in resolver.url_patterns:
54 if isinstance(pattern, URLResolver):55 if isinstance(pattern, URLResolver):
55 accumulate_api_resources(pattern, accumulator)56 accumulate_api_resources(pattern, accumulator)
56 elif isinstance(pattern, URLPattern):57 elif isinstance(pattern, URLPattern):
57 if isinstance(pattern.callback, Resource):58 if isinstance(pattern.callback, Resource) and visible(
58 resource = pattern.callback59 pattern.callback.handler
59 if p_has_resource_uri(resource) and not p_is_not_hidden(60 ):
60 resource61 accumulator.add(pattern.callback)
61 ):
62 accumulator.add(resource)
63 else:62 else:
64 raise AssertionError(63 raise AssertionError(
65 "Not a recognised pattern or resolver: %r" % (pattern,)64 f"Not a recognised pattern or resolver: {pattern!r}"
66 )65 )
6766
6867
@@ -228,10 +227,18 @@ def describe_actions(handler):
228 restful: Indicates if this is a CRUD/ReSTful action.227 restful: Indicates if this is a CRUD/ReSTful action.
229228
230 """229 """
231 from maasserver.api import support # Circular import.230 from maasserver.api import support
232231
233 getname = support.OperationsResource.crudmap.get232 getname = support.OperationsResource.crudmap.get
234 for signature, function in handler.exports.items():233
234 actions = []
235
236 # ensure stable sorting, accounting for operation being None
237 exports = sorted(
238 handler.exports.items(),
239 key=lambda item: (item[0][0], item[0][1] or ""),
240 )
241 for signature, function in exports:
235 http_method, operation = signature242 http_method, operation = signature
236 name = getname(http_method) if operation is None else operation243 name = getname(http_method) if operation is None else operation
237244
@@ -276,13 +283,16 @@ def describe_actions(handler):
276 )283 )
277 doc += ":type %s: %s\n\n " % (pname, param["type"])284 doc += ":type %s: %s\n\n " % (pname, param["type"])
278285
279 yield dict(286 actions.append(
280 method=http_method,287 {
281 name=name,288 "method": http_method,
282 doc=doc,289 "name": name,
283 op=operation,290 "doc": doc,
284 restful=(operation is None),291 "op": operation,
292 "restful": operation is None,
293 }
285 )294 )
295 return sorted(actions, key=itemgetter("name"))
286296
287297
288def describe_handler(handler):298def describe_handler(handler):
@@ -306,7 +316,7 @@ def describe_handler(handler):
306 )316 )
307317
308 return {318 return {
309 "actions": list(describe_actions(handler)),319 "actions": describe_actions(handler),
310 "doc": getdoc(handler),320 "doc": getdoc(handler),
311 "name": handler.__name__,321 "name": handler.__name__,
312 "params": tuple(uri_params),322 "params": tuple(uri_params),
@@ -345,26 +355,30 @@ def describe_api():
345 """355 """
346 from maasserver import urls_api as urlconf356 from maasserver import urls_api as urlconf
347357
358 resources = sorted(
359 (
360 describe_resource(resource)
361 for resource in find_api_resources(urlconf)
362 ),
363 key=itemgetter("name"),
364 )
365
348 # This is the core of it:366 # This is the core of it:
349 description = {367 description = {
350 "doc": "MAAS API",368 "doc": "MAAS API",
351 "resources": [369 "resources": resources,
352 describe_resource(resource)
353 for resource in find_api_resources(urlconf)
354 ],
355 }370 }
356
357 # However, for backward compatibility, add "handlers" as an alias for all371 # However, for backward compatibility, add "handlers" as an alias for all
358 # not-None anon and auth handlers in "resources".372 # not-None anon and auth handlers in "resources".
359 description["handlers"] = []373 description["handlers"] = []
360 description["handlers"].extend(374 description["handlers"].extend(
361 resource["anon"]375 resource["anon"]
362 for resource in description["resources"]376 for resource in resources
363 if resource["anon"] is not None377 if resource["anon"] is not None
364 )378 )
365 description["handlers"].extend(379 description["handlers"].extend(
366 resource["auth"]380 resource["auth"]
367 for resource in description["resources"]381 for resource in resources
368 if resource["auth"] is not None382 if resource["auth"] is not None
369 )383 )
370384
@@ -537,11 +551,7 @@ def hash_canonical(description):
537 into a new `hashlib.sha1` object.551 into a new `hashlib.sha1` object.
538 """552 """
539 description = describe_canonical(description)553 description = describe_canonical(description)
540 description_as_json = json.dumps(description)554 description_as_json = json.dumps(description).encode("ascii")
541 # Python 3's json.dumps returns a `str`, so encode if necessary.
542 if not isinstance(description_as_json, bytes):
543 description_as_json = description_as_json.encode("ascii")
544 # We /could/ instead pass a hashing object in and call .update()...
545 return hashlib.sha1(description_as_json)555 return hashlib.sha1(description_as_json)
546556
547557
diff --git a/src/maasserver/api/doc_handler.py b/src/maasserver/api/doc_handler.py
index 9b332c7..e9e1f4a 100644
--- a/src/maasserver/api/doc_handler.py
+++ b/src/maasserver/api/doc_handler.py
@@ -235,11 +235,12 @@ def describe(request):
235 # The handler URIs returned by describe_resource() are relative paths.235 # The handler URIs returned by describe_resource() are relative paths.
236 absolute = partial(build_absolute_uri, request)236 absolute = partial(build_absolute_uri, request)
237 for resource in description["resources"]:237 for resource in description["resources"]:
238 for handler_type in "anon", "auth":238 for handler_type in ("anon", "auth"):
239 handler = resource[handler_type]239 handler = resource[handler_type]
240 if handler is not None:240 if handler is not None:
241 handler["uri"] = absolute(handler["path"])241 handler["uri"] = absolute(handler["path"])
242 # Return as a JSON document.242 # Return as a JSON document.
243 return HttpResponse(243 return HttpResponse(
244 json.dumps(description), content_type="application/json"244 json.dumps(description, sort_keys=True),
245 content_type="application/json",
245 )246 )
diff --git a/src/maasserver/forms/settings.py b/src/maasserver/forms/settings.py
index 10ab368..5a662a2 100644
--- a/src/maasserver/forms/settings.py
+++ b/src/maasserver/forms/settings.py
@@ -75,7 +75,7 @@ def make_default_osystem_field(*args, **kwargs):
75 "osystem", os_choices75 "osystem", os_choices
76 )76 )
77 },77 },
78 **kwargs78 **kwargs,
79 )79 )
80 return field80 return field
8181
@@ -154,7 +154,7 @@ def make_default_distro_series_field(*args, **kwargs):
154 "release", release_choices154 "release", release_choices
155 )155 )
156 },156 },
157 **kwargs157 **kwargs,
158 )158 )
159 return field159 return field
160160
@@ -186,7 +186,7 @@ def make_default_min_hwe_kernel_field(*args, **kwargs):
186 "default_min_hwe_kernel", kernel_choices186 "default_min_hwe_kernel", kernel_choices
187 )187 )
188 },188 },
189 **kwargs189 **kwargs,
190 )190 )
191 return field191 return field
192192
@@ -206,7 +206,7 @@ def make_commissioning_distro_series_field(*args, **kwargs):
206 "commissioning_distro_series", commissioning_choices206 "commissioning_distro_series", commissioning_choices
207 )207 )
208 },208 },
209 **kwargs209 **kwargs,
210 )210 )
211 return field211 return field
212212
@@ -221,7 +221,7 @@ def make_dnssec_validation_field(*args, **kwargs):
221 "dnssec_validation", DNSSEC_VALIDATION_CHOICES221 "dnssec_validation", DNSSEC_VALIDATION_CHOICES
222 )222 )
223 },223 },
224 **kwargs224 **kwargs,
225 )225 )
226 return field226 return field
227227
@@ -236,7 +236,7 @@ def make_network_discovery_field(*args, **kwargs):
236 "network_discovery", NETWORK_DISCOVERY_CHOICES236 "network_discovery", NETWORK_DISCOVERY_CHOICES
237 )237 )
238 },238 },
239 **kwargs239 **kwargs,
240 )240 )
241 return field241 return field
242242
@@ -251,7 +251,7 @@ def make_active_discovery_interval_field(*args, **kwargs):
251 "active_discovery_interval", ACTIVE_DISCOVERY_INTERVAL_CHOICES251 "active_discovery_interval", ACTIVE_DISCOVERY_INTERVAL_CHOICES
252 )252 )
253 },253 },
254 **kwargs254 **kwargs,
255 )255 )
256 return field256 return field
257257
@@ -998,17 +998,12 @@ def get_config_form(config_name, data=None):
998 return form998 return form
999999
10001000
1001def describe_choices(choices):1001def get_config_doc(config_items=None, indentation=0):
1002 """Describe the items in an enumeration of Django form choices."""
1003 return ", ".join(
1004 "'%s' (%s)" % (value, meaning) for value, meaning in choices
1005 )
1006
1007
1008def get_config_doc(indentation=0):
1009 """Return the documentation about the available configuration settings."""1002 """Return the documentation about the available configuration settings."""
1003 if config_items is None:
1004 config_items = CONFIG_ITEMS
1010 doc = ["Available configuration items:\n\n"]1005 doc = ["Available configuration items:\n\n"]
1011 for config_name, config_details in sorted(CONFIG_ITEMS.items()):1006 for config_name, config_details in sorted(config_items.items()):
1012 form_details = config_details["form_kwargs"]1007 form_details = config_details["form_kwargs"]
1013 doc.append(":" + config_name + ": " + form_details["label"] + ". ")1008 doc.append(":" + config_name + ": " + form_details["label"] + ". ")
1014 # Append help text if present.1009 # Append help text if present.
@@ -1018,7 +1013,9 @@ def get_config_doc(indentation=0):
1018 # Append list of possible choices if present.1013 # Append list of possible choices if present.
1019 choices = form_details.get("choices")1014 choices = form_details.get("choices")
1020 if choices is not None:1015 if choices is not None:
1021 choice_descr = describe_choices(choices)1016 choice_descr = ", ".join(
1017 f"'{value}' ({meaning})" for value, meaning in sorted(choices)
1018 )
1022 doc.append("Available choices are: %s." % choice_descr)1019 doc.append("Available choices are: %s." % choice_descr)
1023 doc.append("\n")1020 doc.append("\n")
1024 full_text = (" " * indentation).join(doc)1021 full_text = (" " * indentation).join(doc)
diff --git a/src/maasserver/forms/tests/test_settings.py b/src/maasserver/forms/tests/test_settings.py
index a1dcc23..f0cd075 100644
--- a/src/maasserver/forms/tests/test_settings.py
+++ b/src/maasserver/forms/tests/test_settings.py
@@ -1,8 +1,7 @@
1# Copyright 2013-2016 Canonical Ltd. This software is licensed under the1# Copyright 2013-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test forms settings."""4from textwrap import dedent
5
65
7from django import forms6from django import forms
8from django.core.exceptions import ValidationError7from django.core.exceptions import ValidationError
@@ -41,9 +40,31 @@ class TestGetConfigForm(MAASServerTestCase):
4140
42class TestGetConfigDoc(MAASServerTestCase):41class TestGetConfigDoc(MAASServerTestCase):
43 def test_get_config_doc(self):42 def test_get_config_doc(self):
44 doc = get_config_doc()43 config_items = {
45 # Just make sure that the doc looks okay.44 "testitem": {
46 self.assertIn("maas_name", doc)45 "default": "foo",
46 "form": forms.CharField,
47 "form_kwargs": {
48 "label": "bar",
49 "choices": [
50 ("b", "B"),
51 ("a", "A"),
52 ],
53 },
54 },
55 }
56 doc = get_config_doc(config_items=config_items)
57 # choices are returned in the correct order
58 self.assertEqual(
59 doc,
60 dedent(
61 """\
62 Available configuration items:
63
64 :testitem: bar. Available choices are: 'a' (A), 'b' (B).
65 """
66 ),
67 )
4768
4869
49class TestSpecificConfigSettings(MAASServerTestCase):70class TestSpecificConfigSettings(MAASServerTestCase):
diff --git a/src/maasserver/urls_api.py b/src/maasserver/urls_api.py
index ff6bf95..bbf76a1 100644
--- a/src/maasserver/urls_api.py
+++ b/src/maasserver/urls_api.py
@@ -566,7 +566,7 @@ urlpatterns += [
566 ),566 ),
567 url(r"^vlans/(?P<vlan_id>[^/]+)/$", vlan_handler, name="vlanid_handler"),567 url(r"^vlans/(?P<vlan_id>[^/]+)/$", vlan_handler, name="vlanid_handler"),
568 url(568 url(
569 r"fabrics/(?P<fabric_id>[^/]+)/vlans/(?P<vid>[^/]+)/$",569 r"^fabrics/(?P<fabric_id>[^/]+)/vlans/(?P<vid>[^/]+)/$",
570 vlan_handler,570 vlan_handler,
571 name="vlan_handler",571 name="vlan_handler",
572 ),572 ),

Subscribers

People subscribed via source and target branches