diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9fcbfde..1db8f4c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,9 @@ Major changes Minor changes ............. +- Honor ``:order: path`` in ``qrefflask`` without regrouping endpoints by + resource name, matching ``autoflask`` ordering. [:issue:`33`] + Version 2.2.1 ````````````` diff --git a/docs/index.rst b/docs/index.rst index 0f83760..dc4f81d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -933,6 +933,11 @@ of your configuration (:file:`conf.py`) file:: to that of :mod:`sphinxcontrib.autohttp.flask` with the addition of the ``autoquickref`` option. + With ``:order: path``, the summary follows the same endpoint order as + ``autoflask``. Resource names and descriptions are omitted only for + consecutive repetitions, so a resource may appear in more than one group. + Without this option, the summary groups resources alphabetically. + Basic usage ``````````` diff --git a/src/sphinxcontrib/autohttp/flaskqref.py b/src/sphinxcontrib/autohttp/flaskqref.py index efa6434..20ffc62 100644 --- a/src/sphinxcontrib/autohttp/flaskqref.py +++ b/src/sphinxcontrib/autohttp/flaskqref.py @@ -11,6 +11,8 @@ """ +from itertools import groupby + from docutils import nodes from docutils.statemachine import ViewList @@ -42,25 +44,18 @@ def run(self): result = ViewList() for line in QuickReferenceFlaskDirective.header: result.append(line, '') - table={} - table_sorted_names=[] - - for table_row in self.make_rst(qref=True): - name = table_row['name'] - if table.get(name) is None: - table[name]=[] - table[name].append(table_row) - if name not in table_sorted_names: - table_sorted_names.append(name) - - table_sorted_names.sort() + rows = list(self.make_rst(qref=True)) + if self.order != 'path': + # Keep the historical resource grouping unless endpoint order was + # requested. The base directive already supplies path order. + rows.sort(key=lambda row: row['name']) - for name in table_sorted_names: + for name, resource_rows in groupby(rows, key=lambda row: row['name']): # Keep table display clean by not repeating duplicate # resource names and descriptions display_name = name previous_description=None - for row in table[name]: + for row in resource_rows: result.append(' * - %s' % display_name, '') display_name ="" result.append(row['operation'], '') diff --git a/tests/flaskqref_order_test.py b/tests/flaskqref_order_test.py new file mode 100644 index 0000000..d20fcf8 --- /dev/null +++ b/tests/flaskqref_order_test.py @@ -0,0 +1,83 @@ +import pickle +import subprocess +import sys + +import pytest + +from docutils import nodes +from sphinx import addnodes + + +@pytest.mark.parametrize('order', [None, 'path']) +@pytest.mark.parametrize('shared_resource', ['Root', '']) +def test_quick_reference_respects_order(tmp_path, order, shared_resource): + source = tmp_path / 'source' + source.mkdir() + (source / 'conf.py').write_text( + "import sys\n" + "from pathlib import Path\n" + "sys.path.insert(0, str(Path(__file__).parent))\n" + "extensions = ['sphinxcontrib.autohttp.flask', " + "'sphinxcontrib.autohttp.flaskqref']\n" + "project = 'Quick reference ordering'\n" + "master_doc = 'index'\n", + encoding='utf-8', + ) + # Registration order, resource-name order and path order all differ. + # The shared resource occurs on both sides of another resource in path + # order, so preserving only the first resource occurrence is insufficient. + (source / 'sampleapp.py').write_text( + "from flask import Flask\n" + "app = Flask(__name__, static_folder=None)\n" + "specs = [('/z', %r), ('/b', 'Accounts'), " + "('/a', %r), ('/c', %r)]\n" + "for i, (path, resource) in enumerate(specs):\n" + " def view():\n" + " return ''\n" + " description = 'Shared description' if resource != 'Accounts' else 'Account description'\n" + " view.__doc__ = '.. :quickref: ' + resource + ';' + description\n" + " app.add_url_rule(path, 'endpoint' + str(i), view)\n" + % (shared_resource, shared_resource, shared_resource), + encoding='utf-8', + ) + option = ' :order: path\n' if order == 'path' else '' + (source / 'index.rst').write_text( + 'API\n===\n\n.. qrefflask:: sampleapp:app\n' + option + + '\n.. autoflask:: sampleapp:app\n' + option, + encoding='utf-8', + ) + doctrees = tmp_path / 'doctrees' + output = tmp_path / 'html' + result = subprocess.run( + [sys.executable, '-m', 'sphinx', '-W', '-b', 'html', + '-d', str(doctrees), str(source), str(output)], + capture_output=True, text=True, check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + # This is the trusted doctree produced by the build just run above. + with (doctrees / 'index.doctree').open('rb') as stream: + document = pickle.load(stream) + table = next(document.findall(nodes.table)) + body = next(table.findall(nodes.tbody)) + rows = [[entry.astext() for entry in row.children] for row in body.children] + paths = [row[1].split()[1] for row in rows] + details = [sig.astext().split()[1] + for sig in document.findall(addnodes.desc_signature)] + + if order == 'path': + assert paths == ['/a', '/b', '/c', '/z'] + assert paths == details + assert [row[0] for row in rows] == [shared_resource, 'Accounts', shared_resource, ''] + assert [row[2] for row in rows] == [ + 'Shared description', 'Account description', 'Shared description', '', + ] + else: + assert details == ['/z', '/b', '/a', '/c'] + if shared_resource: + assert paths == ['/b', '/z', '/a', '/c'] + assert [row[0] for row in rows] == ['Accounts', shared_resource, '', ''] + assert [row[2] for row in rows] == ['Account description', 'Shared description', '', ''] + else: + assert paths == ['/z', '/a', '/c', '/b'] + assert [row[0] for row in rows] == ['', '', '', 'Accounts'] + assert [row[2] for row in rows] == ['Shared description', '', '', 'Account description']