Coverage for openhcs/textual_tui/windows/help_windows.py: 0.0%
179 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""Unified help system for displaying docstring and parameter information."""
3from typing import Union, Callable, Optional
4from textual import on
5from textual.app import ComposeResult
6from textual.containers import ScrollableContainer, Horizontal
7from textual.widgets import Button, Static, Markdown
8from textual.css.query import NoMatches
10from openhcs.textual_tui.windows.base_window import BaseOpenHCSWindow
11from openhcs.textual_tui.widgets.shared.signature_analyzer import DocstringExtractor
14class BaseHelpWindow(BaseOpenHCSWindow):
15 """Base class for all help windows with unified button handling."""
17 @on(Button.Pressed, "#close")
18 def close_help(self) -> None:
19 """Handle close button press."""
20 self.close_window()
23class DocstringHelpWindow(BaseHelpWindow):
24 """Window for displaying docstring information."""
26 DEFAULT_CSS = """
27 DocstringHelpWindow ScrollableContainer {
28 text-align: left;
29 align: left top;
30 }
32 DocstringHelpWindow Static {
33 text-align: left;
34 }
36 .help-summary {
37 margin: 1 0;
38 padding: 1;
39 background: $surface;
40 border: solid $primary;
41 text-align: left;
42 }
44 .help-description {
45 margin: 1 0;
46 padding: 1;
47 text-align: left;
48 }
50 .help-section-header {
51 margin: 1 0 0 0;
52 text-style: bold;
53 color: $accent;
54 text-align: left;
55 }
57 .help-parameter {
58 margin: 0 0 0 2;
59 color: $text;
60 text-align: left;
61 }
63 .help-returns {
64 margin: 0 0 0 2;
65 color: $text;
66 text-align: left;
67 }
69 .help-examples {
70 margin: 1 0;
71 padding: 1;
72 background: $surface;
73 border: solid $accent;
74 text-align: left;
75 }
76 """
78 def __init__(self, target: Union[Callable, type], title: Optional[str] = None, **kwargs):
79 self.target = target
80 self.docstring_info = DocstringExtractor.extract(target)
82 # Generate title from target if not provided
83 if title is None:
84 if hasattr(target, '__name__'):
85 title = f"Help: {target.__name__}"
86 else:
87 title = "Help"
89 super().__init__(
90 window_id="docstring_help",
91 title=title,
92 mode="temporary",
93 **kwargs
94 )
96 # Calculate dynamic minimum size based on content
97 self._calculate_dynamic_size()
99 def compose(self) -> ComposeResult:
100 """Compose the help window content with scrollable area."""
101 # Scrollable content area
102 with ScrollableContainer():
103 # Function/class summary
104 if self.docstring_info.summary:
105 yield Static(f"[bold]{self.docstring_info.summary}[/bold]", classes="help-summary")
107 # Full description
108 if self.docstring_info.description:
109 yield Markdown(self.docstring_info.description, classes="help-description")
111 # Parameters section
112 if self.docstring_info.parameters:
113 yield Static("[bold]Parameters[/bold]", classes="help-section-header")
114 for param_name, param_desc in self.docstring_info.parameters.items():
115 yield Static(f"• [bold]{param_name}[/bold]: {param_desc}", classes="help-parameter")
116 # Add spacing between parameters for better readability
117 yield Static("", classes="help-parameter-spacer")
119 # Returns section
120 if self.docstring_info.returns:
121 yield Static("[bold]Returns:[/bold]", classes="help-section-header")
122 yield Static(f"• {self.docstring_info.returns}", classes="help-returns")
124 # Examples section
125 if self.docstring_info.examples:
126 yield Static("[bold]Examples:[/bold]", classes="help-section-header")
127 yield Markdown(f"```python\n{self.docstring_info.examples}\n```", classes="help-examples")
129 # Close button at bottom - wrapped in Horizontal for automatic centering
130 with Horizontal():
131 yield Button("Close", id="close", compact=True)
133 def _calculate_dynamic_size(self) -> None:
134 """Calculate and set dynamic minimum window size based on content."""
135 try:
136 # Calculate width based on longest line in content
137 max_width = 40 # Base minimum width
139 # Check summary length
140 if self.docstring_info.summary:
141 max_width = max(max_width, len(self.docstring_info.summary) + 10)
143 # Check parameter descriptions
144 if self.docstring_info.parameters:
145 for param_name, param_desc in self.docstring_info.parameters.items():
146 line_length = len(f"• {param_name}: {param_desc}")
147 max_width = max(max_width, line_length + 10)
149 # Check returns description
150 if self.docstring_info.returns:
151 max_width = max(max_width, len(self.docstring_info.returns) + 10)
153 # Calculate height based on content sections
154 min_height = 10 # Base minimum height
155 content_lines = 0
157 if self.docstring_info.summary:
158 content_lines += 2 # Summary + margin
159 if self.docstring_info.description:
160 # Estimate lines for description (rough approximation)
161 content_lines += max(3, len(self.docstring_info.description) // 60)
162 if self.docstring_info.parameters:
163 content_lines += 1 + len(self.docstring_info.parameters) # Header + params
164 if self.docstring_info.returns:
165 content_lines += 2 # Header + returns
166 if self.docstring_info.examples:
167 content_lines += 5 # Header + example block
169 content_lines += 3 # Close button + margins
170 min_height = max(min_height, content_lines)
172 # Cap maximum size to reasonable limits
173 max_width = min(max_width, 120)
174 min_height = min(min_height, 40)
176 # Set dynamic window size
177 self.styles.width = max_width
178 self.styles.height = min_height
180 except Exception:
181 # Fallback to default sizes if calculation fails
182 self.styles.width = 60
183 self.styles.height = 20
186class ParameterHelpWindow(BaseHelpWindow):
187 """Window for displaying individual parameter documentation."""
189 DEFAULT_CSS = """
190 ParameterHelpWindow ScrollableContainer {
191 text-align: left;
192 align: left top;
193 }
195 ParameterHelpWindow Static {
196 text-align: left;
197 }
199 .param-header {
200 margin: 0 0 1 0;
201 text-style: bold;
202 color: $primary;
203 text-align: left;
204 }
206 .param-description {
207 margin: 1 0;
208 text-align: left;
209 }
211 .param-no-desc {
212 margin: 1 0;
213 color: $text-muted;
214 text-style: italic;
215 text-align: left;
216 }
217 """
219 def __init__(self, param_name: str, param_description: str, param_type: type = None, **kwargs):
220 self.param_name = param_name
221 self.param_description = param_description
222 self.param_type = param_type
224 title = f"Help: {param_name}"
226 super().__init__(
227 window_id="parameter_help",
228 title=title,
229 mode="temporary",
230 **kwargs
231 )
233 # Calculate dynamic minimum size based on content
234 self._calculate_dynamic_size()
236 def compose(self) -> ComposeResult:
237 """Compose the parameter help content with scrollable area."""
238 # Scrollable content area
239 with ScrollableContainer():
240 # Parameter name and type
241 type_info = f" ({self._format_type_info(self.param_type)})" if self.param_type else ""
242 yield Static(f"[bold]{self.param_name}[/bold]{type_info}", classes="param-header")
244 # Parameter description
245 if self.param_description:
246 yield Markdown(self.param_description.rstrip(), classes="param-description")
247 else:
248 yield Static("[dim]No description available[/dim]", classes="param-no-desc")
250 # Close button at bottom - wrapped in Horizontal for automatic centering
251 with Horizontal():
252 yield Button("Close", id="close", compact=True)
254 def _calculate_dynamic_size(self) -> None:
255 """Calculate and set dynamic minimum window size based on parameter content."""
256 try:
257 # Calculate width based on parameter content
258 max_width = 30 # Base minimum width for parameter windows
260 # Check parameter name length
261 max_width = max(max_width, len(self.param_name) + 15)
263 # Check parameter description length
264 if self.param_description:
265 # Split description into lines and find longest
266 desc_lines = self.param_description.split('\n')
267 for line in desc_lines:
268 max_width = max(max_width, len(line) + 10)
270 # Check parameter type length
271 if self.param_type:
272 type_str = str(self.param_type)
273 max_width = max(max_width, len(f"Type: {type_str}") + 10)
275 # Calculate height based on content
276 min_height = 8 # Base minimum height for parameter windows
277 content_lines = 2 # Parameter name + margin
279 if self.param_description:
280 # Estimate lines for description
281 desc_lines = self.param_description.split('\n')
282 content_lines += len(desc_lines) + 1 # Description lines + margin
284 if self.param_type:
285 content_lines += 1 # Type line
287 content_lines += 3 # Close button + margins
288 min_height = max(min_height, content_lines)
290 # Cap maximum size to reasonable limits
291 max_width = min(max_width, 100)
292 min_height = min(min_height, 30)
294 # Set dynamic minimum size
295 self.styles.min_width = max_width
296 self.styles.min_height = min_height
298 except Exception:
299 # Fallback to default sizes if calculation fails
300 self.styles.min_width = 30
301 self.styles.min_height = 8
303 def _format_type_info(self, param_type) -> str:
304 """Format type information for display, showing full Union types."""
305 if not param_type:
306 return "Unknown"
308 try:
309 from typing import get_origin, get_args, Union
311 # Handle Union types (including Optional)
312 origin = get_origin(param_type)
313 if origin is Union:
314 args = get_args(param_type)
315 # Filter out NoneType for cleaner display
316 non_none_args = [arg for arg in args if arg is not type(None)]
318 if len(non_none_args) == 1 and type(None) in args:
319 # This is Optional[T] - show as "T (optional)"
320 return f"{self._format_single_type(non_none_args[0])} (optional)"
321 else:
322 # This is a true Union - show all types
323 type_names = [self._format_single_type(arg) for arg in args]
324 return f"Union[{', '.join(type_names)}]"
325 else:
326 # Regular type
327 return self._format_single_type(param_type)
329 except Exception:
330 # Fallback to simple name if anything goes wrong
331 return getattr(param_type, '__name__', str(param_type))
333 def _format_single_type(self, type_obj) -> str:
334 """Format a single type for display."""
335 try:
336 from typing import get_origin, get_args
338 origin = get_origin(type_obj)
339 if origin:
340 # Handle generic types like List[str], Dict[str, int]
341 args = get_args(type_obj)
342 if args:
343 arg_names = [self._format_single_type(arg) for arg in args]
344 return f"{origin.__name__}[{', '.join(arg_names)}]"
345 else:
346 return origin.__name__
347 else:
348 # Simple type
349 return getattr(type_obj, '__name__', str(type_obj))
350 except Exception:
351 return str(type_obj)
354class HelpWindowManager:
355 """Unified help window management system - consolidates all help window logic."""
357 @staticmethod
358 async def show_docstring_help(app, target: Union[Callable, type], title: Optional[str] = None):
359 """Show help for a function or class using the window system."""
360 try:
361 window = app.query_one(DocstringHelpWindow)
362 # Window exists, update it and open
363 window.target = target
364 window.docstring_info = DocstringExtractor.extract(target)
365 window.open_state = True
366 except NoMatches:
367 # Expected case: window doesn't exist yet, create new one
368 window = DocstringHelpWindow(target, title)
369 await app.mount(window)
370 window.open_state = True
372 @staticmethod
373 async def show_parameter_help(app, param_name: str, param_description: str, param_type: type = None):
374 """Show help for a parameter using the window system."""
375 try:
376 window = app.query_one(ParameterHelpWindow)
377 # Window exists, update it and open
378 window.param_name = param_name
379 window.param_description = param_description
380 window.param_type = param_type
381 window.open_state = True
382 except NoMatches:
383 # Expected case: window doesn't exist yet, create new one
384 window = ParameterHelpWindow(param_name, param_description, param_type)
385 await app.mount(window)
386 window.open_state = True
389class HelpableWidget:
390 """Mixin class to add help functionality to widgets - uses unified manager."""
392 async def show_function_help(self, target: Union[Callable, type]) -> None:
393 """Show help window for a function or class."""
394 if hasattr(self, 'app'):
395 await HelpWindowManager.show_docstring_help(self.app, target)
397 async def show_parameter_help(self, param_name: str, param_description: str, param_type: type = None) -> None:
398 """Show help window for a parameter."""
399 if hasattr(self, 'app'):
400 await HelpWindowManager.show_parameter_help(self.app, param_name, param_description, param_type)