Skip to content

Commit 24f7fb4

Browse files
committed
feat(docs): Render argparse metadata as semantic definition list
Replace inline pipe-separated metadata (Default: None | Type: str | Required) with a semantic <dl> structure that enables independent CSS styling of keys, values, and tags. Uses Furo's guilabel pattern for Required tag with semi-transparent amber background for light/dark mode compatibility.
1 parent 3487e8b commit 24f7fb4

3 files changed

Lines changed: 178 additions & 26 deletions

File tree

docs/_ext/sphinx_argparse_neo/nodes.py

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -518,30 +518,46 @@ def depart_argparse_argument_html(
518518
node : argparse_argument
519519
The argument node being departed.
520520
"""
521-
# Add metadata (default, choices, type)
522-
metadata: list[str] = []
523-
521+
# Build metadata as definition list items
524522
default = node.get("default_string")
525-
if default is not None:
526-
# Wrap default value in nv span for yellow/italic styling
527-
metadata.append(f'Default: <span class="nv">{self.encode(default)}</span>')
528-
529523
choices = node.get("choices")
530-
if choices:
531-
choices_str = ", ".join(str(c) for c in choices)
532-
metadata.append(f"Choices: {self.encode(choices_str)}")
533-
534524
type_name = node.get("type_name")
535-
if type_name:
536-
metadata.append(f"Type: {self.encode(type_name)}")
537-
538525
required = node.get("required", False)
539-
if required:
540-
metadata.append("Required")
541526

542-
if metadata:
543-
meta_str = " | ".join(metadata)
544-
self.body.append(f'<p class="argparse-argument-meta">{meta_str}</p>')
527+
if default is not None or choices or type_name or required:
528+
self.body.append('<dl class="argparse-argument-meta">\n')
529+
530+
if default is not None:
531+
self.body.append('<div class="argparse-meta-item">')
532+
self.body.append('<dt class="argparse-meta-key">Default</dt>')
533+
self.body.append(
534+
f'<dd class="argparse-meta-value">'
535+
f'<span class="nv">{self.encode(default)}</span></dd>'
536+
)
537+
self.body.append("</div>\n")
538+
539+
if type_name:
540+
self.body.append('<div class="argparse-meta-item">')
541+
self.body.append('<dt class="argparse-meta-key">Type</dt>')
542+
self.body.append(
543+
f'<dd class="argparse-meta-value">'
544+
f'<span class="nv">{self.encode(type_name)}</span></dd>'
545+
)
546+
self.body.append("</div>\n")
547+
548+
if choices:
549+
choices_str = ", ".join(str(c) for c in choices)
550+
self.body.append('<div class="argparse-meta-item">')
551+
self.body.append('<dt class="argparse-meta-key">Choices</dt>')
552+
self.body.append(
553+
f'<dd class="argparse-meta-value">{self.encode(choices_str)}</dd>'
554+
)
555+
self.body.append("</div>\n")
556+
557+
if required:
558+
self.body.append('<dt class="argparse-meta-tag">Required</dt>\n')
559+
560+
self.body.append("</dl>\n")
545561

546562
self.body.append("</dd>\n")
547563
# Close wrapper div

docs/_static/css/argparse-highlight.css

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,18 +348,65 @@ body:not([data-theme="dark"]) .argparse-argument-name .headerlink {
348348
}
349349

350350
/*
351-
* Default value styling in metadata
352-
* Styled like inline code with monokai background.
351+
* Argument metadata definition list
352+
*
353+
* Renders metadata (Default, Type, Choices, Required) as a horizontal
354+
* flexbox of key-value pairs and standalone tags.
353355
*/
354-
.argparse-argument-meta .nv {
356+
.argparse-argument-meta {
357+
margin: 0.5rem 0 0 0;
358+
padding: 0;
359+
display: flex;
360+
flex-wrap: wrap;
361+
gap: 0.5rem 1rem;
362+
align-items: center;
363+
}
364+
365+
.argparse-meta-item {
366+
display: flex;
367+
align-items: center;
368+
gap: 0.25rem;
369+
}
370+
371+
.argparse-meta-key {
372+
color: var(--color-foreground-secondary, #6c757d);
373+
font-size: var(--font-size--small);
374+
}
375+
376+
.argparse-meta-key::after {
377+
content: ":";
378+
}
379+
380+
.argparse-meta-value .nv {
355381
background: var(--argparse-code-background);
356382
border-radius: 0.2rem;
357-
padding: 0.1405rem 0.3rem;
383+
padding: 0.1rem 0.3rem;
358384
font-family: var(--font-stack--monospace);
359385
font-size: var(--font-size--small);
360386
color: #e5c07b;
361387
}
362388

389+
/*
390+
* Meta tag (e.g., "Required") - follows Furo's guilabel pattern
391+
* Uses semi-transparent amber background with border for visibility
392+
* without the harshness of solid fills. Amber conveys "needs attention".
393+
*/
394+
.argparse-meta-tag {
395+
background-color: #fef3c780;
396+
border: 1px solid #fcd34d80;
397+
color: var(--color-foreground-primary);
398+
font-size: var(--font-size--small);
399+
padding: 0.1rem 0.4rem;
400+
border-radius: 0.2rem;
401+
font-weight: 500;
402+
}
403+
404+
/* Dark mode: darker amber with adjusted border */
405+
body[data-theme="dark"] .argparse-meta-tag {
406+
background-color: #78350f60;
407+
border-color: #b4530980;
408+
}
409+
363410
/*
364411
* Help text description
365412
* Adds spacing above for visual separation from argument name.

tests/docs/_ext/sphinx_argparse_neo/test_nodes.py

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ class ArgumentHTMLCase(t.NamedTuple):
314314
metavar: str | None
315315
help_text: str | None
316316
default: str | None
317+
type_name: str | None
318+
required: bool
317319
id_prefix: str
318320
expected_patterns: list[str] # Regex patterns to match
319321

@@ -325,14 +327,18 @@ class ArgumentHTMLCase(t.NamedTuple):
325327
metavar="socket-name",
326328
help_text="pass-through for tmux -L",
327329
default="None",
330+
type_name=None,
331+
required=False,
328332
id_prefix="shell",
329333
expected_patterns=[
330334
r'<div class="argparse-argument-wrapper" id="shell-L">',
331335
r'<dt class="argparse-argument-name">',
332336
r'<span class="na">-L</span>',
333337
r'<span class="nv">socket-name</span>',
334338
r'<a class="headerlink" href="#shell-L">¶</a>',
335-
r'Default: <span class="nv">None</span>',
339+
r'<dl class="argparse-argument-meta">',
340+
r'<dt class="argparse-meta-key">Default</dt>',
341+
r'<dd class="argparse-meta-value"><span class="nv">None</span></dd>',
336342
r"</div>",
337343
],
338344
),
@@ -342,6 +348,8 @@ class ArgumentHTMLCase(t.NamedTuple):
342348
metavar=None,
343349
help_text="show help",
344350
default=None,
351+
type_name=None,
352+
required=False,
345353
id_prefix="",
346354
expected_patterns=[
347355
r'<span class="nt">--help</span>',
@@ -355,6 +363,8 @@ class ArgumentHTMLCase(t.NamedTuple):
355363
metavar=None,
356364
help_text="input file",
357365
default=None,
366+
type_name=None,
367+
required=False,
358368
id_prefix="",
359369
expected_patterns=[
360370
r'<span class="nl">filename</span>',
@@ -367,6 +377,8 @@ class ArgumentHTMLCase(t.NamedTuple):
367377
metavar=None,
368378
help_text="Enable verbose mode",
369379
default=None,
380+
type_name=None,
381+
required=False,
370382
id_prefix="load",
371383
expected_patterns=[
372384
r'id="load-v-verbose"',
@@ -375,6 +387,25 @@ class ArgumentHTMLCase(t.NamedTuple):
375387
r'href="#load-v-verbose"',
376388
],
377389
),
390+
ArgumentHTMLCase(
391+
test_id="metadata_definition_list",
392+
names=["workspace_file"],
393+
metavar="workspace-file",
394+
help_text="checks current tmuxp for workspace files.",
395+
default="None",
396+
type_name="str",
397+
required=True,
398+
id_prefix="edit",
399+
expected_patterns=[
400+
r'<dl class="argparse-argument-meta">',
401+
r'<dt class="argparse-meta-key">Default</dt>',
402+
r'<dd class="argparse-meta-value"><span class="nv">None</span></dd>',
403+
r'<dt class="argparse-meta-key">Type</dt>',
404+
r'<dd class="argparse-meta-value"><span class="nv">str</span></dd>',
405+
r'<dt class="argparse-meta-tag">Required</dt>',
406+
r"</dl>",
407+
],
408+
),
378409
]
379410

380411

@@ -395,6 +426,8 @@ def render_argument_to_html(
395426
metavar: str | None,
396427
help_text: str | None,
397428
default: str | None,
429+
type_name: str | None,
430+
required: bool,
398431
id_prefix: str,
399432
) -> str:
400433
"""Render an argument node to HTML string for testing.
@@ -409,6 +442,10 @@ def render_argument_to_html(
409442
Help text for the argument.
410443
default
411444
Default value string.
445+
type_name
446+
Type name for the argument (e.g., "str", "int").
447+
required
448+
Whether the argument is required.
412449
id_prefix
413450
Prefix for the argument ID.
414451
@@ -427,6 +464,8 @@ def render_argument_to_html(
427464
node["metavar"] = metavar
428465
node["help"] = help_text
429466
node["default_string"] = default
467+
node["type_name"] = type_name
468+
node["required"] = required
430469
node["id_prefix"] = id_prefix
431470

432471
translator = MockTranslator()
@@ -448,6 +487,8 @@ def test_argument_html_rendering(case: ArgumentHTMLCase) -> None:
448487
metavar=case.metavar,
449488
help_text=case.help_text,
450489
default=case.default,
490+
type_name=case.type_name,
491+
required=case.required,
451492
id_prefix=case.id_prefix,
452493
)
453494

@@ -462,6 +503,8 @@ def test_argument_wrapper_has_id() -> None:
462503
metavar="PATH",
463504
help_text="Input file",
464505
default=None,
506+
type_name=None,
507+
required=False,
465508
id_prefix="convert",
466509
)
467510

@@ -476,23 +519,29 @@ def test_argument_headerlink_present() -> None:
476519
metavar="FILE",
477520
help_text="Output file",
478521
default=None,
522+
type_name=None,
523+
required=False,
479524
id_prefix="freeze",
480525
)
481526

482527
assert '<a class="headerlink" href="#freeze-output">¶</a>' in html
483528

484529

485530
def test_default_value_styled() -> None:
486-
"""Verify default value is wrapped in nv span."""
531+
"""Verify default value is wrapped in nv span within definition list."""
487532
html = render_argument_to_html(
488533
names=["--format"],
489534
metavar=None,
490535
help_text="Output format",
491536
default="json",
537+
type_name=None,
538+
required=False,
492539
id_prefix="",
493540
)
494541

495-
assert 'Default: <span class="nv">json</span>' in html
542+
assert '<dl class="argparse-argument-meta">' in html
543+
assert '<dt class="argparse-meta-key">Default</dt>' in html
544+
assert '<dd class="argparse-meta-value"><span class="nv">json</span></dd>' in html
496545

497546

498547
def test_wrapper_div_closed() -> None:
@@ -502,6 +551,8 @@ def test_wrapper_div_closed() -> None:
502551
metavar=None,
503552
help_text="Verbose",
504553
default=None,
554+
type_name=None,
555+
required=False,
505556
id_prefix="",
506557
)
507558

@@ -518,8 +569,46 @@ def test_argument_no_id_prefix() -> None:
518569
metavar=None,
519570
help_text="Enable debug mode",
520571
default=None,
572+
type_name=None,
573+
required=False,
521574
id_prefix="",
522575
)
523576

524577
assert 'id="debug"' in html
525578
assert 'href="#debug"' in html
579+
580+
581+
def test_metadata_uses_definition_list() -> None:
582+
"""Verify metadata renders as definition list, not inline paragraph."""
583+
html = render_argument_to_html(
584+
names=["--format"],
585+
metavar=None,
586+
help_text="Output format",
587+
default="json",
588+
type_name="str",
589+
required=False,
590+
id_prefix="",
591+
)
592+
593+
assert '<dl class="argparse-argument-meta">' in html
594+
assert '<dt class="argparse-meta-key">Default</dt>' in html
595+
assert '<dd class="argparse-meta-value"><span class="nv">json</span></dd>' in html
596+
assert '<dt class="argparse-meta-key">Type</dt>' in html
597+
assert '<dd class="argparse-meta-value"><span class="nv">str</span></dd>' in html
598+
599+
600+
def test_required_renders_as_tag() -> None:
601+
"""Verify Required renders as standalone tag, not key-value."""
602+
html = render_argument_to_html(
603+
names=["--config"],
604+
metavar="FILE",
605+
help_text="Config file",
606+
default=None,
607+
type_name=None,
608+
required=True,
609+
id_prefix="",
610+
)
611+
612+
assert '<dt class="argparse-meta-tag">Required</dt>' in html
613+
# Should NOT have a matching dd for Required
614+
assert 'argparse-meta-value">Required' not in html

0 commit comments

Comments
 (0)