sumd 0.3.44__tar.gz → 0.3.45__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {sumd-0.3.44 → sumd-0.3.45}/CHANGELOG.md +18 -0
  2. {sumd-0.3.44 → sumd-0.3.45}/PKG-INFO +7 -7
  3. {sumd-0.3.44 → sumd-0.3.45}/README.md +6 -6
  4. {sumd-0.3.44 → sumd-0.3.45}/pyproject.toml +1 -1
  5. {sumd-0.3.44 → sumd-0.3.45}/sumd/__init__.py +1 -1
  6. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/api_stubs.py +2 -2
  7. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/architecture.py +6 -7
  8. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/call_graph.py +2 -2
  9. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/configuration.py +4 -5
  10. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/dependencies.py +2 -2
  11. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/deployment.py +6 -7
  12. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/extras.py +2 -2
  13. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/interfaces.py +4 -4
  14. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/metadata.py +2 -2
  15. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/quality.py +4 -5
  16. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/refactor_analysis.py +2 -2
  17. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/source_snippets.py +4 -5
  18. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/swop.py +2 -2
  19. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/test_contracts.py +4 -5
  20. sumd-0.3.45/sumd/sections/utils/__init__.py +12 -0
  21. sumd-0.3.45/sumd/sections/utils/render.py +25 -0
  22. sumd-0.3.45/sumd/sections/utils/should_render.py +25 -0
  23. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/workflows.py +2 -2
  24. {sumd-0.3.44 → sumd-0.3.45}/.gitignore +0 -0
  25. {sumd-0.3.44 → sumd-0.3.45}/LICENSE +0 -0
  26. {sumd-0.3.44 → sumd-0.3.45}/sumd/cli.py +0 -0
  27. {sumd-0.3.44 → sumd-0.3.45}/sumd/extractor.py +0 -0
  28. {sumd-0.3.44 → sumd-0.3.45}/sumd/generator.py +0 -0
  29. {sumd-0.3.44 → sumd-0.3.45}/sumd/mcp_server.py +0 -0
  30. {sumd-0.3.44 → sumd-0.3.45}/sumd/models.py +0 -0
  31. {sumd-0.3.44 → sumd-0.3.45}/sumd/parser.py +0 -0
  32. {sumd-0.3.44 → sumd-0.3.45}/sumd/pipeline.py +0 -0
  33. {sumd-0.3.44 → sumd-0.3.45}/sumd/renderer.py +0 -0
  34. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/__init__.py +0 -0
  35. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/base.py +0 -0
  36. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/code_analysis.py +0 -0
  37. {sumd-0.3.44 → sumd-0.3.45}/sumd/sections/environment.py +0 -0
  38. {sumd-0.3.44 → sumd-0.3.45}/sumd/toon_parser.py +0 -0
  39. {sumd-0.3.44 → sumd-0.3.45}/sumd/validator.py +0 -0
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.45] - 2026-04-25
11
+
12
+ ### Docs
13
+ - Update README.md
14
+
15
+ ### Other
16
+ - Update sumd/sections/api_stubs.py
17
+ - Update sumd/sections/architecture.py
18
+ - Update sumd/sections/call_graph.py
19
+ - Update sumd/sections/configuration.py
20
+ - Update sumd/sections/dependencies.py
21
+ - Update sumd/sections/deployment.py
22
+ - Update sumd/sections/extras.py
23
+ - Update sumd/sections/interfaces.py
24
+ - Update sumd/sections/metadata.py
25
+ - Update sumd/sections/quality.py
26
+ - ... and 8 more files
27
+
10
28
  ## [0.3.44] - 2026-04-24
11
29
 
12
30
  ### Docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sumd
3
- Version: 0.3.44
3
+ Version: 0.3.45
4
4
  Summary: SUMD - Structured Unified Markdown Descriptor for AI-aware project documentation
5
5
  Project-URL: Homepage, https://github.com/tom/statement
6
6
  Project-URL: Repository, https://github.com/tom/statement
@@ -41,17 +41,17 @@ Description-Content-Type: text/markdown
41
41
 
42
42
  ## AI Cost Tracking
43
43
 
44
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.3.44-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-26.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
44
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.3.45-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
45
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-27.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
46
46
 
47
- - 🤖 **LLM usage:** $7.5000 (74 commits)
48
- - 👤 **Human dev:** ~$2650 (26.5h @ $100/h, 30min dedup)
47
+ - 🤖 **LLM usage:** $7.5000 (75 commits)
48
+ - 👤 **Human dev:** ~$2765 (27.6h @ $100/h, 30min dedup)
49
49
 
50
- Generated on 2026-04-24 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
50
+ Generated on 2026-04-25 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
51
51
 
52
52
  ---
53
53
 
54
- ![Version](https://img.shields.io/badge/version-0.3.44-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
54
+ ![Version](https://img.shields.io/badge/version-0.3.45-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
55
55
 
56
56
  **SUMD** (Structured Unified Markdown Descriptor) is a semantic project descriptor format in Markdown.
57
57
  It defines intent, structure, execution entry points, and the mental model of a system for both humans and LLMs.
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.3.44-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-26.5h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.3.45-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.50-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-27.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.5000 (74 commits)
10
- - 👤 **Human dev:** ~$2650 (26.5h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.5000 (75 commits)
10
+ - 👤 **Human dev:** ~$2765 (27.6h @ $100/h, 30min dedup)
11
11
 
12
- Generated on 2026-04-24 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
12
+ Generated on 2026-04-25 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![Version](https://img.shields.io/badge/version-0.3.44-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![Version](https://img.shields.io/badge/version-0.3.45-blue) ![Python](https://img.shields.io/badge/python-3.10+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  **SUMD** (Structured Unified Markdown Descriptor) is a semantic project descriptor format in Markdown.
19
19
  It defines intent, structure, execution entry points, and the mental model of a system for both humans and LLMs.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sumd"
7
- version = "0.3.44"
7
+ version = "0.3.45"
8
8
  description = "SUMD - Structured Unified Markdown Descriptor for AI-aware project documentation"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -4,7 +4,7 @@ SUMD is a semantic project descriptor format in Markdown that defines intent,
4
4
  structure, execution entry points, and mental model of a system for both humans and LLMs.
5
5
  """
6
6
 
7
- __version__ = "0.3.44"
7
+ __version__ = "0.3.45"
8
8
 
9
9
  from sumd.models import SUMDDocument
10
10
  from sumd.parser import (
@@ -7,6 +7,7 @@ LLM sees function signatures and HTTP method/path without reading full openapi.y
7
7
  from __future__ import annotations
8
8
 
9
9
  from sumd.sections.base import RenderContext, Section
10
+ from sumd.sections.utils.render import call_with_ctx
10
11
 
11
12
 
12
13
  # ---------------------------------------------------------------------------
@@ -69,8 +70,7 @@ class ApiStubsSection:
69
70
  def should_render(self, ctx: RenderContext) -> bool:
70
71
  return bool(ctx.openapi.get("endpoints"))
71
72
 
72
- def render(self, ctx: RenderContext) -> list[str]:
73
- return _render_api_stubs(ctx.openapi)
73
+ render = call_with_ctx(_render_api_stubs, "openapi")
74
74
 
75
75
 
76
76
  assert isinstance(ApiStubsSection(), Section)
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
 
7
7
  from sumd.sections.base import RenderContext, Section
8
+ from sumd.sections.utils.render import call_with_ctx
9
+ from sumd.sections.utils.should_render import always
8
10
 
9
11
 
10
12
  # ---------------------------------------------------------------------------
@@ -143,13 +145,10 @@ class ArchitectureSection:
143
145
  level = 2
144
146
  profiles = frozenset({"minimal", "light", "rich"})
145
147
 
146
- def should_render(self, ctx: RenderContext) -> bool:
147
- return True
148
-
149
- def render(self, ctx: RenderContext) -> list[str]:
150
- return _render_architecture(
151
- ctx.doql, ctx.modules, ctx.name, ctx.proj_dir, ctx.raw_sources
152
- )
148
+ should_render = always
149
+ render = call_with_ctx(
150
+ _render_architecture, "doql", "modules", "name", "proj_dir", "raw_sources"
151
+ )
153
152
 
154
153
 
155
154
  assert isinstance(ArchitectureSection(), Section)
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
  import re
12
12
 
13
13
  from sumd.sections.base import RenderContext, Section
14
+ from sumd.sections.utils.render import call_with_ctx
14
15
 
15
16
 
16
17
  # ---------------------------------------------------------------------------
@@ -149,8 +150,7 @@ class CallGraphSection:
149
150
  def should_render(self, ctx: RenderContext) -> bool:
150
151
  return any("calls.toon" in e.get("file", "") for e in ctx.project_analysis)
151
152
 
152
- def render(self, ctx: RenderContext) -> list[str]:
153
- return _render_call_graph(ctx.project_analysis)
153
+ render = call_with_ctx(_render_call_graph, "project_analysis")
154
154
 
155
155
 
156
156
  assert isinstance(CallGraphSection(), Section)
@@ -3,6 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from sumd.sections.base import RenderContext, Section
6
+ from sumd.sections.utils.render import call_with_ctx
7
+ from sumd.sections.utils.should_render import always
6
8
 
7
9
 
8
10
  # ---------------------------------------------------------------------------
@@ -34,11 +36,8 @@ class ConfigurationSection:
34
36
  level = 2
35
37
  profiles = frozenset({"light", "rich"})
36
38
 
37
- def should_render(self, ctx: RenderContext) -> bool:
38
- return True
39
-
40
- def render(self, ctx: RenderContext) -> list[str]:
41
- return _render_configuration_section(ctx.name, ctx.version)
39
+ should_render = always
40
+ render = call_with_ctx(_render_configuration_section, "name", "version")
42
41
 
43
42
 
44
43
  assert isinstance(ConfigurationSection(), Section)
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from sumd.sections.base import RenderContext, Section
6
+ from sumd.sections.utils.render import call_with_ctx
6
7
 
7
8
 
8
9
  # ---------------------------------------------------------------------------
@@ -90,8 +91,7 @@ class DependenciesSection:
90
91
  or ctx.pkg_json.get("devDependencies")
91
92
  )
92
93
 
93
- def render(self, ctx: RenderContext) -> list[str]:
94
- return _render_dependencies(ctx.deps, ctx.dev_deps, ctx.pkg_json)
94
+ render = call_with_ctx(_render_dependencies, "deps", "dev_deps", "pkg_json")
95
95
 
96
96
 
97
97
  assert isinstance(DependenciesSection(), Section)
@@ -3,6 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from sumd.sections.base import RenderContext, Section
6
+ from sumd.sections.utils.render import call_with_ctx
7
+ from sumd.sections.utils.should_render import always
6
8
 
7
9
 
8
10
  # ---------------------------------------------------------------------------
@@ -99,13 +101,10 @@ class DeploymentSection:
99
101
  level = 2
100
102
  profiles = frozenset({"light", "rich"})
101
103
 
102
- def should_render(self, ctx: RenderContext) -> bool:
103
- return True
104
-
105
- def render(self, ctx: RenderContext) -> list[str]:
106
- return _render_deployment(
107
- ctx.pkg_json, ctx.name, ctx.reqs, ctx.dockerfile, ctx.compose
108
- )
104
+ should_render = always
105
+ render = call_with_ctx(
106
+ _render_deployment, "pkg_json", "name", "reqs", "dockerfile", "compose"
107
+ )
109
108
 
110
109
 
111
110
  assert isinstance(DeploymentSection(), Section)
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from sumd.sections.base import RenderContext, Section
6
+ from sumd.sections.utils.render import call_with_ctx
6
7
 
7
8
 
8
9
  # ---------------------------------------------------------------------------
@@ -65,8 +66,7 @@ class ExtrasSection:
65
66
  def should_render(self, ctx: RenderContext) -> bool:
66
67
  return bool(ctx.makefile or ctx.pkg_json.get("scripts"))
67
68
 
68
- def render(self, ctx: RenderContext) -> list[str]:
69
- return _render_extras(ctx.makefile, ctx.pkg_json)
69
+ render = call_with_ctx(_render_extras, "makefile", "pkg_json")
70
70
 
71
71
 
72
72
  assert isinstance(ExtrasSection(), Section)
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
 
7
7
  from sumd.sections.base import RenderContext, Section
8
+ from sumd.sections.utils.render import call_with_ctx
8
9
 
9
10
 
10
11
  # ---------------------------------------------------------------------------
@@ -148,10 +149,9 @@ class InterfacesSection:
148
149
  def should_render(self, ctx: RenderContext) -> bool:
149
150
  return bool(ctx.scripts or ctx.openapi.get("endpoints") or ctx.scenarios)
150
151
 
151
- def render(self, ctx: RenderContext) -> list[str]:
152
- return _render_interfaces(
153
- ctx.scripts, ctx.openapi, ctx.scenarios, ctx.proj_dir, ctx.raw_sources
154
- )
152
+ render = call_with_ctx(
153
+ _render_interfaces, "scripts", "openapi", "scenarios", "proj_dir", "raw_sources"
154
+ )
155
155
 
156
156
 
157
157
  assert isinstance(InterfacesSection(), Section)
@@ -8,6 +8,7 @@ This serves as the reference implementation for all other sections.
8
8
  from __future__ import annotations
9
9
 
10
10
  from sumd.sections.base import RenderContext, Section
11
+ from sumd.sections.utils.should_render import always
11
12
 
12
13
 
13
14
  class MetadataSection:
@@ -17,8 +18,7 @@ class MetadataSection:
17
18
  level: int = 2
18
19
  profiles: frozenset[str] = frozenset({"minimal", "light", "rich"})
19
20
 
20
- def should_render(self, ctx: RenderContext) -> bool:
21
- return True # Always render
21
+ should_render = always
22
22
 
23
23
  def render(self, ctx: RenderContext) -> list[str]:
24
24
  L: list[str] = []
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
 
7
7
  from sumd.sections.base import RenderContext, Section
8
+ from sumd.sections.utils.render import call_with_ctx
9
+ from sumd.sections.utils.should_render import has_attr
8
10
 
9
11
 
10
12
  # ---------------------------------------------------------------------------
@@ -72,11 +74,8 @@ class QualitySection:
72
74
  level = 2
73
75
  profiles = frozenset({"light", "rich", "refactor"})
74
76
 
75
- def should_render(self, ctx: RenderContext) -> bool:
76
- return bool(ctx.pyqual)
77
-
78
- def render(self, ctx: RenderContext) -> list[str]:
79
- return _render_quality(ctx.pyqual, ctx.proj_dir, ctx.raw_sources)
77
+ should_render = has_attr("pyqual")
78
+ render = call_with_ctx(_render_quality, "pyqual", "proj_dir", "raw_sources")
80
79
 
81
80
 
82
81
  assert isinstance(QualitySection(), Section)
@@ -17,6 +17,7 @@ Files that don't exist are silently skipped (non-blocking).
17
17
  from __future__ import annotations
18
18
 
19
19
  from sumd.sections.base import RenderContext, Section
20
+ from sumd.sections.utils.should_render import has_attr
20
21
 
21
22
  # Display order and labels for refactor analysis files.
22
23
  _REFACTOR_FILE_LABELS: dict[str, str] = {
@@ -36,8 +37,7 @@ class RefactorAnalysisSection:
36
37
  level = 2
37
38
  profiles = frozenset({"refactor"})
38
39
 
39
- def should_render(self, ctx: RenderContext) -> bool:
40
- return bool(ctx.project_analysis)
40
+ should_render = has_attr("project_analysis")
41
41
 
42
42
  def render(self, ctx: RenderContext) -> list[str]:
43
43
  L: list[str] = []
@@ -8,6 +8,8 @@ to read full source files.
8
8
  from __future__ import annotations
9
9
 
10
10
  from sumd.sections.base import RenderContext, Section
11
+ from sumd.sections.utils.render import call_with_ctx
12
+ from sumd.sections.utils.should_render import has_attr
11
13
 
12
14
 
13
15
  # ---------------------------------------------------------------------------
@@ -59,11 +61,8 @@ class SourceSnippetsSection:
59
61
  level = 2
60
62
  profiles = frozenset({"rich"})
61
63
 
62
- def should_render(self, ctx: RenderContext) -> bool:
63
- return bool(ctx.source_snippets)
64
-
65
- def render(self, ctx: RenderContext) -> list[str]:
66
- return _render_source_snippets(ctx.source_snippets)
64
+ should_render = has_attr("source_snippets")
65
+ render = call_with_ctx(_render_source_snippets, "source_snippets")
67
66
 
68
67
 
69
68
  assert isinstance(SourceSnippetsSection(), Section)
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from sumd.sections.base import RenderContext, Section
6
+ from sumd.sections.utils.render import call_with_ctx
6
7
 
7
8
 
8
9
  def _render_swop_section(swop: dict, raw_sources: bool) -> list[str]:
@@ -61,8 +62,7 @@ class SwopSection:
61
62
  def should_render(self, ctx: RenderContext) -> bool:
62
63
  return bool(ctx.swop.get("contexts"))
63
64
 
64
- def render(self, ctx: RenderContext) -> list[str]:
65
- return _render_swop_section(ctx.swop, ctx.raw_sources)
65
+ render = call_with_ctx(_render_swop_section, "swop", "raw_sources")
66
66
 
67
67
 
68
68
  assert isinstance(SwopSection(), Section)
@@ -7,6 +7,8 @@ LLM sees what the system guarantees without reading full scenario files.
7
7
  from __future__ import annotations
8
8
 
9
9
  from sumd.sections.base import RenderContext, Section
10
+ from sumd.sections.utils.render import call_with_ctx
11
+ from sumd.sections.utils.should_render import has_attr
10
12
 
11
13
 
12
14
  # ---------------------------------------------------------------------------
@@ -68,11 +70,8 @@ class TestContractsSection:
68
70
  level = 2
69
71
  profiles = frozenset({"rich"})
70
72
 
71
- def should_render(self, ctx: RenderContext) -> bool:
72
- return bool(ctx.scenarios)
73
-
74
- def render(self, ctx: RenderContext) -> list[str]:
75
- return _render_test_contracts(ctx.scenarios)
73
+ should_render = has_attr("scenarios")
74
+ render = call_with_ctx(_render_test_contracts, "scenarios")
76
75
 
77
76
 
78
77
  assert isinstance(TestContractsSection(), Section)
@@ -0,0 +1,12 @@
1
+ """sumd.sections.utils — shared utilities for section renderers.
2
+
3
+ Factories for common ``should_render`` and ``render`` patterns so section
4
+ classes stay DRY.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from sumd.sections.utils.render import call_with_ctx
10
+ from sumd.sections.utils.should_render import always, has_attr
11
+
12
+ __all__ = ["always", "has_attr", "call_with_ctx"]
@@ -0,0 +1,25 @@
1
+ """Utility factory for common ``render`` patterns.
2
+
3
+ Usage::
4
+
5
+ class MySection:
6
+ render = call_with_ctx(_render_my_section, "attr1", "attr2")
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Callable
12
+
13
+ from sumd.sections.base import RenderContext
14
+
15
+
16
+ def call_with_ctx(render_fn: Callable, *attr_names: str):
17
+ """Return a ``render`` method that calls *render_fn* with ctx attributes.
18
+
19
+ The returned callable accepts ``(self, ctx)`` so it works when assigned
20
+ as a class attribute (Python binds ``self`` automatically).
21
+ """
22
+ def render(_self: object, ctx: RenderContext) -> list[str]:
23
+ args = [getattr(ctx, name) for name in attr_names]
24
+ return render_fn(*args)
25
+ return render
@@ -0,0 +1,25 @@
1
+ """Utility factories for common ``should_render`` patterns.
2
+
3
+ Usage::
4
+
5
+ class MySection:
6
+ should_render = always
7
+ # or
8
+ should_render = has_attr("my_data")
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from sumd.sections.base import RenderContext
14
+
15
+
16
+ def always(_self: object, _ctx: RenderContext) -> bool:
17
+ """Always render the section."""
18
+ return True
19
+
20
+
21
+ def has_attr(attr: str):
22
+ """Return a ``should_render`` that checks ``bool(ctx.<attr>)``."""
23
+ def should_render(_self: object, ctx: RenderContext) -> bool:
24
+ return bool(getattr(ctx, attr))
25
+ return should_render
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
 
7
7
  from sumd.sections.base import RenderContext, Section
8
+ from sumd.sections.utils.render import call_with_ctx
8
9
 
9
10
 
10
11
  # ---------------------------------------------------------------------------
@@ -79,8 +80,7 @@ class WorkflowsSection:
79
80
  def should_render(self, ctx: RenderContext) -> bool:
80
81
  return bool(ctx.tasks or ctx.doql.get("workflows"))
81
82
 
82
- def render(self, ctx: RenderContext) -> list[str]:
83
- return _render_workflows(ctx.doql, ctx.tasks, ctx.proj_dir, ctx.raw_sources)
83
+ render = call_with_ctx(_render_workflows, "doql", "tasks", "proj_dir", "raw_sources")
84
84
 
85
85
 
86
86
  assert isinstance(WorkflowsSection(), Section)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes