Skip to content

Commit 3abcbbd

Browse files
committed
Make scenario_directive fully generic, remove project-specific knowledge
Three changes: 1. Remove hardcoded _NON_COMMAND_TAGS, _TAG_LABELS, _TAG_ORDER constants. The directive no longer knows anything about dfetch commands. 2. Sort appendix sections alphabetically by tag. The previous fixed ordering (update, check, add, …) was dfetch-specific. 3. Expose scenario_non_command_tags as a conf.py config value (default []). Any tag in that list is skipped when choosing the group tag for a feature file. dfetch's conf.py now sets: scenario_non_command_tags = ["remote-svn"] Section titles are derived from the tag itself (title-cased, hyphens replaced by spaces), so no label map is needed. https://claude.ai/code/session_01BvyikyxX9c3sDXev8HdP4f
1 parent 8b1a65b commit 3abcbbd

3 files changed

Lines changed: 45 additions & 82 deletions

File tree

doc/_ext/scenario_directive.py

Lines changed: 39 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
66
extensions = ["scenario_directive"]
77
8+
# Tags that are NOT group keys (e.g. environment markers).
9+
# The first tag on a feature file that is not in this list becomes
10+
# the appendix section for that file. Default: no exclusions.
11+
scenario_non_command_tags = ["remote-svn"]
12+
813
Usage in .rst files:
914
1015
.. scenario-include:: ../features/fetch-git-repo.feature
@@ -15,9 +20,8 @@
1520
If ``:scenario:`` is omitted, all scenarios in the feature file are included.
1621
1722
**PDF / LaTeX builds** automatically move scenarios to an appendix grouped by
18-
the behave command-tag (``@update``, ``@check``, …) that was placed on the
19-
feature file. The directive's original location receives a cross-reference to
20-
the appendix entry instead.
23+
the first non-excluded behave tag found on the feature file. The directive's
24+
original location receives a cross-reference to the appendix entry instead.
2125
2226
Use the ``:inline:`` flag to keep a specific inclusion in place even when
2327
building a PDF:
@@ -29,58 +33,18 @@
2933
the document tree:
3034
3135
.. scenario-appendix::
32-
33-
Behave tags treated as *command tags* map to the appendix sections. Any tag
34-
that appears in ``_NON_COMMAND_TAGS`` (e.g. ``remote-svn``) is ignored when
35-
determining the command tag.
3636
"""
3737

3838
import html
3939
import os
4040
import re
41-
from typing import Dict, List, Optional, Tuple
41+
from typing import Dict, FrozenSet, List, Tuple
4242

4343
from docutils import nodes
4444
from docutils.parsers.rst import Directive, directives
4545
from docutils.statemachine import StringList
4646

4747

48-
# ---------------------------------------------------------------------------
49-
# Tags that are NOT command-name tags (infrastructure / skip markers).
50-
# ---------------------------------------------------------------------------
51-
_NON_COMMAND_TAGS = {"remote-svn"}
52-
53-
# Human-readable section titles for each command tag.
54-
_TAG_LABELS: Dict[str, str] = {
55-
"update": "``dfetch update`` scenarios",
56-
"check": "``dfetch check`` scenarios",
57-
"add": "``dfetch add`` scenarios",
58-
"remove": "``dfetch remove`` scenarios",
59-
"diff": "``dfetch diff`` scenarios",
60-
"update-patch": "``dfetch update-patch`` scenarios",
61-
"format-patch": "``dfetch format-patch`` scenarios",
62-
"freeze": "``dfetch freeze`` scenarios",
63-
"import": "``dfetch import`` scenarios",
64-
"report": "``dfetch report`` scenarios",
65-
"validate": "``dfetch validate`` scenarios",
66-
}
67-
68-
# Canonical display order for command tags in the appendix.
69-
_TAG_ORDER = [
70-
"update",
71-
"check",
72-
"add",
73-
"remove",
74-
"diff",
75-
"update-patch",
76-
"format-patch",
77-
"freeze",
78-
"import",
79-
"report",
80-
"validate",
81-
]
82-
83-
8448
# ---------------------------------------------------------------------------
8549
# Custom node types
8650
# ---------------------------------------------------------------------------
@@ -108,10 +72,10 @@ def _feature_tags(feature_path: str) -> List[str]:
10872
return tags
10973

11074

111-
def _command_tag(feature_path: str) -> str:
112-
"""Return the first non-infrastructure tag from the feature file, or 'other'."""
75+
def _group_tag(feature_path: str, non_group_tags: FrozenSet[str]) -> str:
76+
"""Return the first tag not in *non_group_tags*, or ``'other'``."""
11377
for tag in _feature_tags(feature_path):
114-
if tag not in _NON_COMMAND_TAGS:
78+
if tag not in non_group_tags:
11579
return tag
11680
return "other"
11781

@@ -142,6 +106,11 @@ def _full_feature_content(feature_path: str) -> str:
142106
return fh.read()
143107

144108

109+
def _tag_section_title(tag: str) -> str:
110+
"""Human-readable section title derived from *tag* alone."""
111+
return tag.replace("-", " ").title()
112+
113+
145114
# ---------------------------------------------------------------------------
146115
# scenario-include directive
147116
# ---------------------------------------------------------------------------
@@ -166,28 +135,28 @@ class ScenarioIncludeDirective(Directive):
166135
# Internal helpers
167136
# ------------------------------------------------------------------
168137

138+
def _env(self):
139+
return self.state.document.settings.env
140+
169141
def _is_pdf(self) -> bool:
170-
env = self.state.document.settings.env
171-
return env.app.builder.name in ("latex", "rinoh")
142+
return self._env().app.builder.name in ("latex", "rinoh")
172143

173144
def _feature_abs(self, feature_file: str) -> str:
174-
env = self.state.document.settings.env
145+
env = self._env()
175146
path = os.path.abspath(os.path.join(env.app.srcdir, feature_file))
176147
if not os.path.exists(path):
177148
raise self.error(f"Feature file not found: {path}")
178149
return path
179150

180151
def _include_path(self, feature_abs: str) -> str:
181152
"""Path for literalinclude, relative to the current RST document."""
182-
env = self.state.document.settings.env
153+
env = self._env()
183154
current_doc_dir = os.path.dirname(
184155
os.path.join(env.app.srcdir, env.docname)
185156
)
186157
return os.path.relpath(feature_abs, current_doc_dir)
187158

188-
def _requested_scenarios(
189-
self, available: Tuple[str, ...]
190-
) -> List[str]:
159+
def _requested_scenarios(self, available: Tuple[str, ...]) -> List[str]:
191160
return [
192161
t.strip()
193162
for t in self.options.get("scenario", "").splitlines()
@@ -246,10 +215,13 @@ def _render_pdf(
246215
feature_abs: str,
247216
scenario_titles: List[str],
248217
) -> List[nodes.Node]:
249-
env = self.state.document.settings.env
218+
env = self._env()
219+
non_group_tags = frozenset(
220+
getattr(env.config, "scenario_non_command_tags", [])
221+
)
250222
basename = os.path.splitext(os.path.basename(feature_abs))[0]
251223
label = f"appendix-{basename}"
252-
tag = _command_tag(feature_abs)
224+
tag = _group_tag(feature_abs, non_group_tags)
253225
title = _feature_title(feature_abs)
254226

255227
# ----------------------------------------------------------
@@ -265,7 +237,7 @@ def _render_pdf(
265237
"feature_file": feature_file,
266238
"feature_abs": feature_abs,
267239
"feature_title": title,
268-
"command_tag": tag,
240+
"group_tag": tag,
269241
"label": label,
270242
"source_doc": env.docname,
271243
"scenarios": list(scenario_titles),
@@ -319,7 +291,8 @@ class ScenarioAppendixDirective(Directive):
319291
320292
Place this directive once in the document (typically in an appendix
321293
page). During the write phase it is replaced by sections grouped by
322-
command tag, containing the full content of each referenced feature file.
294+
the first non-excluded tag, sorted alphabetically, containing the full
295+
content of each referenced feature file.
323296
324297
In HTML builds scenarios appear inline in the main text, so this
325298
directive emits an explanatory note instead.
@@ -332,13 +305,12 @@ class ScenarioAppendixDirective(Directive):
332305
def run(self) -> List[nodes.Node]:
333306
env = self.state.document.settings.env
334307
if env.app.builder.name not in ("latex", "rinoh"):
335-
# HTML: scenarios are inline; provide a brief orientation note.
336308
note = nodes.note()
337309
para = nodes.paragraph()
338310
para += nodes.Text(
339311
"In the HTML edition, feature scenarios appear as expandable "
340312
"examples directly within each guide section. "
341-
"In the PDF edition they are collected here, grouped by command."
313+
"In the PDF edition they are collected here, grouped by tag."
342314
)
343315
note += para
344316
return [note]
@@ -351,34 +323,22 @@ def run(self) -> List[nodes.Node]:
351323
# Event: replace placeholder with actual appendix content
352324
# ---------------------------------------------------------------------------
353325

354-
def _build_appendix_nodes(
355-
entries: Dict,
356-
) -> List[nodes.Node]:
326+
327+
def _build_appendix_nodes(entries: Dict) -> List[nodes.Node]:
357328
"""Build docutils section nodes for every collected appendix entry."""
358-
# Group by command tag
359329
by_tag: Dict[str, List] = {}
360330
for entry in entries.values():
361-
by_tag.setdefault(entry["command_tag"], []).append(entry)
362-
363-
# Determine display order
364-
ordered_tags = sorted(
365-
by_tag.keys(),
366-
key=lambda t: (
367-
_TAG_ORDER.index(t) if t in _TAG_ORDER else len(_TAG_ORDER),
368-
t,
369-
),
370-
)
331+
by_tag.setdefault(entry["group_tag"], []).append(entry)
371332

372333
result: List[nodes.Node] = []
373-
for tag in ordered_tags:
334+
for tag in sorted(by_tag):
374335
tag_entries = sorted(by_tag[tag], key=lambda e: e["feature_title"])
375336
label = f"appendix-{tag}"
376-
section_title = _TAG_LABELS.get(tag, f"``dfetch {tag}`` scenarios")
377337

378338
tag_section = nodes.section()
379339
tag_section["ids"] = [label]
380340
tag_section["names"] = [label]
381-
tag_section += nodes.title(text=section_title)
341+
tag_section += nodes.title(text=_tag_section_title(tag))
382342

383343
for entry in tag_entries:
384344
feat_section = nodes.section()
@@ -445,6 +405,7 @@ def merge_scenario_appendix(app, env, docnames, other) -> None:
445405

446406
def setup(app):
447407
"""Register directives, nodes, and event hooks."""
408+
app.add_config_value("scenario_non_command_tags", [], "env")
448409
app.add_directive("scenario-include", ScenarioIncludeDirective)
449410
app.add_directive("scenario-appendix", ScenarioAppendixDirective)
450411
app.add_node(scenario_appendix_placeholder)
@@ -453,7 +414,7 @@ def setup(app):
453414
app.connect("env-merge-info", merge_scenario_appendix)
454415

455416
return {
456-
"version": "0.2",
417+
"version": "0.3",
457418
"parallel_read_safe": True,
458419
"parallel_write_safe": True,
459420
}

doc/appendix/scenarios.rst

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ Appendix: Feature scenarios
66
This appendix collects all the Gherkin feature scenarios that illustrate
77
dfetch's behaviour. In the HTML documentation each scenario appears inline,
88
folded inside an expandable *Example* block. In the PDF edition the scenarios
9-
are moved here to keep the main text readable; each location in the guide
10-
carries a cross-reference back to the relevant section below.
11-
12-
The sections are grouped by the dfetch command they exercise.
9+
are moved here, grouped alphabetically by tag.
1310

1411
.. scenario-appendix::

doc/conf.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@
6161
copybutton_prompt_text = r"\$ |>>> |\.\.\. "
6262
copybutton_prompt_is_regexp = True
6363

64+
# scenario_directive: tags that are not group keys (environment markers, etc.)
65+
# The first tag on a feature file that is not in this list determines which
66+
# appendix section the file ends up in.
67+
scenario_non_command_tags = ["remote-svn"]
68+
6469
# Add any paths that contain templates here, relative to this directory.
6570
templates_path = ["_templates"]
6671

0 commit comments

Comments
 (0)