Coverage for little_loops / issue_template.py: 14%

56 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-03-18 16:18 -0500

1"""Issue template assembly using per-type section definition files. 

2 

3Reads per-type template files (bug-sections.json, feat-sections.json, 

4enh-sections.json) from templates/ and assembles structured markdown for 

5issue files. Used by sync pull to produce v2.0-compliant issues. 

6""" 

7 

8from __future__ import annotations 

9 

10import json 

11from pathlib import Path 

12from typing import Any 

13 

14 

15def _default_templates_dir() -> Path: 

16 """Return the bundled templates/ directory relative to this package.""" 

17 return Path(__file__).resolve().parent.parent.parent / "templates" 

18 

19 

20def load_issue_sections(issue_type: str, templates_dir: Path | None = None) -> dict[str, Any]: 

21 """Load per-type sections JSON from the given or default templates directory. 

22 

23 Args: 

24 issue_type: Issue type prefix (BUG, FEAT, ENH). 

25 templates_dir: Optional override path. Defaults to bundled templates/. 

26 

27 Returns: 

28 Parsed JSON data as a dict. 

29 

30 Raises: 

31 FileNotFoundError: If the per-type sections file does not exist. 

32 """ 

33 base = templates_dir if templates_dir is not None else _default_templates_dir() 

34 filename = f"{issue_type.lower()}-sections.json" 

35 path = base / filename 

36 with open(path, encoding="utf-8") as f: 

37 return json.load(f) 

38 

39 

40def assemble_issue_markdown( 

41 sections_data: dict[str, Any], 

42 issue_type: str, 

43 variant: str, 

44 issue_id: str, 

45 title: str, 

46 frontmatter: dict[str, Any], 

47 content: dict[str, str] | None = None, 

48 labels: list[str] | None = None, 

49) -> str: 

50 """Assemble structured markdown from template sections and content. 

51 

52 Args: 

53 sections_data: Parsed per-type sections data. 

54 issue_type: Issue type prefix (BUG, FEAT, ENH). 

55 variant: Creation variant name (full, minimal, legacy). 

56 issue_id: Issue identifier (e.g. "ENH-517"). 

57 title: Issue title text. 

58 frontmatter: Dict of YAML frontmatter key-value pairs. 

59 content: Optional mapping of section name to content string. 

60 Sections not in this dict get their creation_template placeholder. 

61 labels: Optional list of label strings for the Labels section. 

62 

63 Returns: 

64 Complete markdown string with frontmatter, heading, and sections. 

65 """ 

66 content = content or {} 

67 variant_config = sections_data.get("creation_variants", {}).get(variant) 

68 if variant_config is None: 

69 raise ValueError(f"Unknown creation variant: {variant!r}") 

70 

71 common_sections = sections_data.get("common_sections", {}) 

72 type_sections = sections_data.get("type_sections", {}) 

73 exclude_deprecated = variant_config.get("exclude_deprecated", False) 

74 include_common = variant_config.get("include_common", []) 

75 include_type = variant_config.get("include_type_sections", False) 

76 

77 parts: list[str] = [] 

78 

79 # YAML frontmatter 

80 parts.append("---") 

81 for key, value in frontmatter.items(): 

82 parts.append(f"{key}: {value}") 

83 parts.append("---") 

84 parts.append("") 

85 

86 # Title heading 

87 parts.append(f"# {issue_id}: {title}") 

88 parts.append("") 

89 

90 # Common sections from variant 

91 for section_name in include_common: 

92 section_def = common_sections.get(section_name) 

93 if section_def is None: 

94 continue 

95 if exclude_deprecated and section_def.get("deprecated", False): 

96 continue 

97 _append_section(parts, section_name, section_def, content) 

98 

99 # Type-specific sections 

100 if include_type and type_sections: 

101 for section_name, section_def in type_sections.items(): 

102 if exclude_deprecated and section_def.get("deprecated", False): 

103 continue 

104 _append_section(parts, section_name, section_def, content) 

105 

106 # Ensure Labels section is present (even if not in variant's include_common) 

107 if labels is not None and "Labels" not in include_common: 

108 labels_str = ", ".join(f"`{lbl}`" for lbl in labels) if labels else "" 

109 parts.append("## Labels") 

110 parts.append("") 

111 parts.append(labels_str) 

112 parts.append("") 

113 

114 return "\n".join(parts) 

115 

116 

117def _append_section( 

118 parts: list[str], 

119 section_name: str, 

120 section_def: dict[str, Any], 

121 content: dict[str, str], 

122) -> None: 

123 """Append a single section to the parts list.""" 

124 body = content.get(section_name, section_def.get("creation_template", "")) 

125 parts.append(f"## {section_name}") 

126 parts.append("") 

127 if body: 

128 parts.append(body) 

129 parts.append("")