Skip to content

Commit 7ea4cbb

Browse files
jmchiltonclaude
andcommitted
Implement composable, rerunnable story sections.
Adds infrastructure for organizing tutorial generation into reusable sections that can be selectively regenerated or composed together. Core features: - Section context manager via driver.section(title, anchor) - CLI flags: --only-sections, --skip-sections, --merge-into, --section-mode - Automatic heading level management (H2 for top-level, H3+ for nested) - Section filtering with skip taking precedence over only - Markdown merging in replace/append/standalone modes Section execution control: - SectionProxy evaluates to False when filtered out - Allows conditional execution: `with section(...) as s: if s: ...` - Prevents expensive browser automation when sections are skipped Implementation: - Added TypedDict types for strong typing (SectionMetadata, etc.) - Section tracking via section_elements dict and section_stack list - Markdown extraction/merging preserves section order - Updated generate_rule_builder_tutorial.py with 10 example sections Tests: 19/19 passing - Section tracking and filtering - Heading level auto-increment - SectionProxy truthiness and execution skipping - Markdown extraction and merging (replace/append/standalone) - Section order preservation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 6c7516e commit 7ea4cbb

File tree

6 files changed

+1052
-29
lines changed

6 files changed

+1052
-29
lines changed

lib/galaxy/selenium/cli.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from .navigates_galaxy import galaxy_timeout_handler
2121
from .stories import (
2222
NoopStory,
23+
SectionMode,
24+
SectionProxy,
2325
Story,
2426
StoryProtocol,
2527
)
@@ -97,6 +99,35 @@ def add_story_arguments(parser):
9799
default=None,
98100
help="Description for the generated story",
99101
)
102+
parser.add_argument(
103+
"--only-sections",
104+
type=str,
105+
default=None,
106+
help='Comma-separated list of section anchors to generate (e.g., "basics,advanced")',
107+
)
108+
parser.add_argument(
109+
"--skip-sections",
110+
type=str,
111+
default=None,
112+
help="Comma-separated list of section anchors to skip",
113+
)
114+
parser.add_argument(
115+
"--list-sections",
116+
action="store_true",
117+
help="List all available sections and exit (requires running the script)",
118+
)
119+
parser.add_argument(
120+
"--merge-into",
121+
type=str,
122+
default=None,
123+
help="Path to existing markdown file to merge sections into",
124+
)
125+
parser.add_argument(
126+
"--section-mode",
127+
choices=['replace', 'append', 'standalone'],
128+
default='standalone',
129+
help="How to handle sections: replace existing, append to existing, or standalone (default)",
130+
)
100131

101132
return parser
102133

@@ -137,7 +168,28 @@ def __init__(self, args):
137168

138169
title = args.story_title or "Galaxy Tutorial"
139170
description = args.story_description or ""
140-
self.story: StoryProtocol = Story(title, description, args.story_output)
171+
172+
# Parse section filters
173+
only_sections = None
174+
skip_sections = None
175+
if hasattr(args, "only_sections") and args.only_sections:
176+
only_sections = set(s.strip() for s in args.only_sections.split(','))
177+
if hasattr(args, "skip_sections") and args.skip_sections:
178+
skip_sections = set(s.strip() for s in args.skip_sections.split(','))
179+
180+
# Get section mode
181+
section_mode: SectionMode = getattr(args, "section_mode", "standalone")
182+
merge_target = getattr(args, "merge_into", None)
183+
184+
self.story: StoryProtocol = Story(
185+
title,
186+
description,
187+
args.story_output,
188+
only_sections=only_sections,
189+
skip_sections=skip_sections,
190+
merge_target=merge_target,
191+
section_mode=section_mode,
192+
)
141193
else:
142194
self.story = NoopStory()
143195

@@ -167,6 +219,18 @@ def screenshot(self, label: str, caption: Optional[str] = None) -> None:
167219
# Add to story
168220
self.story.add_screenshot(target, caption or label)
169221

222+
def section(self, title: str, anchor: str) -> SectionProxy:
223+
"""Create a story section context manager.
224+
225+
Args:
226+
title: Display title for the section
227+
anchor: Unique identifier for the section (used for filtering/merging)
228+
229+
Returns:
230+
SectionProxy context manager
231+
"""
232+
return SectionProxy(self, title, anchor)
233+
170234
@property
171235
def default_timeout(self):
172236
return 15

lib/galaxy/selenium/context.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
galaxy_timeout_handler,
1111
NavigatesGalaxy,
1212
)
13+
from .stories import SectionProxy
1314

1415

1516
class GalaxySeleniumContext(NavigatesGalaxy):
@@ -116,6 +117,18 @@ def document_file(self, file_path: str, caption: Optional[str] = None):
116117

117118
self.document("\n".join(markdown_parts))
118119

120+
def section(self, title: str, anchor: str) -> SectionProxy:
121+
"""Create a story section context manager.
122+
123+
Args:
124+
title: Display title for the section
125+
anchor: Unique identifier for the section (used for filtering/merging)
126+
127+
Returns:
128+
SectionProxy context manager
129+
"""
130+
return SectionProxy(self, title, anchor)
131+
119132
@abstractmethod
120133
def _screenshot_path(self, label: str, extension=".png") -> Optional[str]:
121134
"""Path to store screenshots in."""

lib/galaxy/selenium/scripts/generate_rule_builder_tutorial.py

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,33 @@
22
"""Script to generate a tutorial for the Galaxy Rule Builder.
33
44
This script demonstrates using the Story class to create user documentation
5-
outside of the test framework.
5+
outside of the test framework with composable, rerunnable sections.
66
77
Usage:
8+
# Generate full tutorial
9+
python generate_rule_builder_tutorial.py \
10+
--galaxy_url http://localhost:8081 \
11+
--story-output ./rules_tutorial
12+
13+
# Generate only specific sections
14+
python generate_rule_builder_tutorial.py \
15+
--galaxy_url http://localhost:8081 \
16+
--story-output ./rules_tutorial \
17+
--only-sections "workbook_example_1,rules_example_1"
18+
19+
# Skip sections
820
python generate_rule_builder_tutorial.py \
921
--galaxy_url http://localhost:8081 \
1022
--story-output ./rules_tutorial \
23+
--skip-sections "workbook_example_3,workbook_example_4"
24+
25+
# Regenerate one section and merge into existing
26+
python generate_rule_builder_tutorial.py \
27+
--galaxy_url http://localhost:8081 \
28+
--story-output ./rules_tutorial_v2 \
29+
--only-sections "workbook_example_1" \
30+
--merge-into ./rules_tutorial/story.md \
31+
--section-mode replace
1132
"""
1233
import argparse
1334
import sys
@@ -52,31 +73,59 @@ def generate_rule_builder_tutorial(has_driver, include_result=False):
5273
has_driver.home()
5374
has_driver.ensure_rules_activity_enabled()
5475

55-
with example_history(has_driver, "Workbook Example 1"):
56-
has_driver.upload_example_1_full_wizard()
57-
has_driver.home()
58-
has_driver.upload_workbook_example_1(include_result=include_result)
59-
with example_history(has_driver, "Workbook Example 2"):
60-
has_driver.upload_workbook_example_2_prereq_pick_a_collection()
61-
has_driver.home()
62-
has_driver.upload_workbook_example_2(include_result=include_result)
63-
with example_history(has_driver, "Workbook Example 3"):
64-
has_driver.upload_workbook_example_3(include_result=include_result)
65-
with example_history(has_driver, "Workbook Example 4"):
66-
has_driver.upload_workbook_example_4(include_result=include_result)
67-
68-
with example_history(has_driver, "Rules Example 1"):
69-
has_driver.upload_rules_example_1(include_result=include_result)
70-
with example_history(has_driver, "Rules Example 2"):
71-
has_driver.upload_rules_example_2(include_result=include_result)
72-
with example_history(has_driver, "Rules Example 3"):
73-
has_driver.upload_rules_example_3(include_result=include_result)
74-
with example_history(has_driver, "Rules Example 4"):
75-
has_driver.upload_rules_example_4(include_result=include_result)
76-
with example_history(has_driver, "Rules Example 5"):
77-
has_driver.upload_rules_example_5(include_result=include_result)
78-
with example_history(has_driver, "Rules Example 6"):
79-
has_driver.upload_rules_example_6(include_result=include_result)
76+
with has_driver.section("Workbook Example 1", "workbook_example_1") as section:
77+
if section:
78+
with example_history(has_driver, "Workbook Example 1"):
79+
has_driver.upload_example_1_full_wizard()
80+
has_driver.home()
81+
has_driver.upload_workbook_example_1(include_result=include_result)
82+
83+
with has_driver.section("Workbook Example 2", "workbook_example_2") as section:
84+
if section:
85+
with example_history(has_driver, "Workbook Example 2"):
86+
has_driver.upload_workbook_example_2_prereq_pick_a_collection()
87+
has_driver.home()
88+
has_driver.upload_workbook_example_2(include_result=include_result)
89+
90+
with has_driver.section("Workbook Example 3", "workbook_example_3") as section:
91+
if section:
92+
with example_history(has_driver, "Workbook Example 3"):
93+
has_driver.upload_workbook_example_3(include_result=include_result)
94+
95+
with has_driver.section("Workbook Example 4", "workbook_example_4") as section:
96+
if section:
97+
with example_history(has_driver, "Workbook Example 4"):
98+
has_driver.upload_workbook_example_4(include_result=include_result)
99+
100+
with has_driver.section("Rules Example 1", "rules_example_1") as section:
101+
if section:
102+
with example_history(has_driver, "Rules Example 1"):
103+
has_driver.upload_rules_example_1(include_result=include_result)
104+
105+
with has_driver.section("Rules Example 2", "rules_example_2") as section:
106+
if section:
107+
with example_history(has_driver, "Rules Example 2"):
108+
has_driver.upload_rules_example_2(include_result=include_result)
109+
110+
with has_driver.section("Rules Example 3", "rules_example_3") as section:
111+
if section:
112+
with example_history(has_driver, "Rules Example 3"):
113+
has_driver.upload_rules_example_3(include_result=include_result)
114+
115+
with has_driver.section("Rules Example 4", "rules_example_4") as section:
116+
if section:
117+
with example_history(has_driver, "Rules Example 4"):
118+
has_driver.upload_rules_example_4(include_result=include_result)
119+
120+
with has_driver.section("Rules Example 5", "rules_example_5") as section:
121+
if section:
122+
with example_history(has_driver, "Rules Example 5"):
123+
has_driver.upload_rules_example_5(include_result=include_result)
124+
125+
with has_driver.section("Rules Example 6", "rules_example_6") as section:
126+
if section:
127+
with example_history(has_driver, "Rules Example 6"):
128+
has_driver.upload_rules_example_6(include_result=include_result)
80129

81130

82131
@contextmanager

lib/galaxy/selenium/stories/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,27 @@
66
"""
77

88
from .story import (
9+
ElementMetadata,
10+
ElementType,
911
NoopStory,
12+
SectionMetadata,
13+
SectionMetadataInput,
14+
SectionMode,
15+
SectionProxy,
1016
Story,
17+
StoryElement,
1118
StoryProtocol,
1219
)
1320

1421
__all__ = [
1522
"Story",
1623
"NoopStory",
1724
"StoryProtocol",
25+
"SectionProxy",
26+
"SectionMetadata",
27+
"SectionMetadataInput",
28+
"SectionMode",
29+
"ElementMetadata",
30+
"ElementType",
31+
"StoryElement",
1832
]

0 commit comments

Comments
 (0)