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+
813Usage in .rst files:
914
1015 .. scenario-include:: ../features/fetch-git-repo.feature
1520If ``: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
2226Use the ``:inline:`` flag to keep a specific inclusion in place even when
2327building a PDF:
2933the 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
3838import html
3939import os
4040import re
41- from typing import Dict , List , Optional , Tuple
41+ from typing import Dict , FrozenSet , List , Tuple
4242
4343from docutils import nodes
4444from docutils .parsers .rst import Directive , directives
4545from 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
446406def 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 }
0 commit comments