Coverage for src / puzzletree / _version.py: 98.18%

96 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-12 20:35 +0000

1"""Version and debugging utilities.""" 

2 

3from __future__ import annotations 

4 

5import os 

6import platform 

7import sys 

8from dataclasses import dataclass 

9from importlib import metadata 

10 

11from rich.console import Console 

12from rich.layout import Layout 

13from rich.panel import Panel 

14from rich.table import Table 

15from rich.text import Text 

16 

17from puzzletree.utils.theme.theme import set_theme 

18 

19 

20@dataclass 

21class Variable: 

22 """Dataclass describing an environment variable.""" 

23 

24 name: str 

25 """Variable name.""" 

26 value: str 

27 """Variable value.""" 

28 

29 

30@dataclass 

31class Package: 

32 """Dataclass describing a Python package.""" 

33 

34 name: str 

35 """Package name.""" 

36 version: str 

37 """Package version.""" 

38 

39 

40@dataclass 

41class Environment: 

42 """Dataclass to store environment information.""" 

43 

44 interpreter_name: str 

45 """Python interpreter name.""" 

46 interpreter_version: str 

47 """Python interpreter version.""" 

48 interpreter_path: str 

49 """Path to Python executable.""" 

50 platform: str 

51 """Operating System.""" 

52 packages: list[Package] 

53 """Installed packages.""" 

54 variables: list[Variable] 

55 """Environment variables.""" 

56 

57 

58def _interpreter_name_version() -> tuple[str, str]: 

59 if hasattr(sys, "implementation"): 

60 impl = sys.implementation.version 

61 version = f"{impl.major}.{impl.minor}.{impl.micro}" 

62 kind = impl.releaselevel 

63 if kind != "final": 

64 version += kind[0] + str(impl.serial) 

65 return sys.implementation.name, version 

66 return "", "0.0.0" 

67 

68 

69def get_version(dist: str = "puzzletree") -> str: 

70 """Get version of the given distribution. 

71 

72 Parameters: 

73 dist: A distribution name. 

74 

75 Returns: 

76 A version number. 

77 """ 

78 if not dist: 

79 raise ValueError("Distribution name cannot be empty or None") 

80 try: 

81 return metadata.version(dist) 

82 except metadata.PackageNotFoundError: 

83 return "0.0.0" 

84 

85 

86def version_info() -> Text: 

87 """Get version information. 

88 

89 Returns: 

90 Version information. 

91 """ 

92 version = get_version() 

93 return Text.assemble(("puzzletree: ", "peach"), (f"{version}", "bold")) 

94 

95 

96def get_debug_info() -> Environment: 

97 """Get debug/environment information. 

98 

99 Returns: 

100 Environment information. 

101 """ 

102 py_name, py_version = _interpreter_name_version() 

103 packages = ["puzzletree"] 

104 variables = list( 

105 dict.fromkeys( 

106 [ 

107 "PYTHONPATH", 

108 *[var for var in os.environ if var.lower().startswith("puzzletree")], 

109 ], 

110 ), 

111 ) 

112 return Environment( 

113 interpreter_name=py_name, 

114 interpreter_version=py_version, 

115 interpreter_path=sys.executable, 

116 platform=platform.platform(), 

117 variables=[Variable(var, val) for var in variables if (val := os.getenv(var))], 

118 packages=[Package(pkg, get_version(pkg)) for pkg in packages], 

119 ) 

120 

121 

122def _make_debug_layout(env: Environment) -> Layout: 

123 """Build a Layout for debug info: header + packages | env vars.""" 

124 header_text = Text( 

125 f"{env.interpreter_name} {env.interpreter_version} | {env.interpreter_path} | {env.platform}", 

126 style="bold", 

127 ) 

128 header = Panel(header_text, title="Debug Info", title_align="left", border_style="bright_blue") 

129 

130 packages_table = Table(highlight=True, box=None, show_header=True, title=f"Packages ({len(env.packages)})") 

131 packages_table.add_column("Package", style="rosewater") 

132 packages_table.add_column("Version", style="bold") 

133 for pkg in env.packages: 

134 packages_table.add_row(pkg.name, pkg.version) 

135 

136 env_table = Table(highlight=True, box=None, show_header=True, title="Environment Variables") 

137 env_table.add_column("Variable", style="rosewater") 

138 env_table.add_column("Value", style="bold") 

139 for var in env.variables: 139 ↛ 140line 139 didn't jump to line 140 because the loop on line 139 never started

140 env_table.add_row(var.name, var.value) 

141 

142 layout = Layout() 

143 layout.split_column( 

144 Layout(header, name="header", size=5), 

145 Layout(name="main", ratio=1), 

146 ) 

147 layout["main"].split_row( 

148 Layout(Panel(packages_table, border_style="bright_blue"), name="packages", ratio=1), 

149 Layout(Panel(env_table, border_style="bright_blue"), name="vars", ratio=1, minimum_size=30), 

150 ) 

151 return layout 

152 

153 

154def _make_debug_panel(env: Environment) -> Panel: 

155 """Build a single Panel for debug info (fallback for narrow terminals).""" 

156 table = Table(highlight=True, box=None, show_header=False) 

157 table.add_row( 

158 Text("Interpreter Name", style="rosewater"), 

159 Text(env.interpreter_name, style="bold"), 

160 ) 

161 table.add_row( 

162 Text("Interpreter Version", style="rosewater"), 

163 Text(env.interpreter_version, style="bold"), 

164 ) 

165 table.add_row( 

166 Text("Interpreter Path", style="rosewater"), 

167 Text(env.interpreter_path, style="bold"), 

168 ) 

169 table.add_row(Text("Platform", style="rosewater"), Text(env.platform, style="bold")) 

170 table.add_row( 

171 Text(f"Packages ({len(env.packages)})", style="rosewater"), 

172 Text.assemble(*[Text(str(pkg), style="bold") for pkg in env.packages]), 

173 ) 

174 table.add_row( 

175 Text("Environment Variables", style="rosewater"), 

176 Text.assemble(*[Text(str(var), style="bold") for var in env.variables]), 

177 ) 

178 return Panel(table, title="Debug Information", title_align="left") 

179 

180 

181def debug_info(console: Console | None = None) -> None: 

182 """Return debug information.""" 

183 if not console: 

184 console = Console(theme=set_theme()) 

185 

186 env = get_debug_info() 

187 from puzzletree.cli.messages.layout import use_layout # noqa: PLC0415 - deferred to avoid circular import 

188 

189 if use_layout(console): 

190 console.print(_make_debug_layout(env)) 

191 else: 

192 console.print(_make_debug_panel(env))