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
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-12 20:35 +0000
1"""Version and debugging utilities."""
3from __future__ import annotations
5import os
6import platform
7import sys
8from dataclasses import dataclass
9from importlib import metadata
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
17from puzzletree.utils.theme.theme import set_theme
20@dataclass
21class Variable:
22 """Dataclass describing an environment variable."""
24 name: str
25 """Variable name."""
26 value: str
27 """Variable value."""
30@dataclass
31class Package:
32 """Dataclass describing a Python package."""
34 name: str
35 """Package name."""
36 version: str
37 """Package version."""
40@dataclass
41class Environment:
42 """Dataclass to store environment information."""
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."""
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"
69def get_version(dist: str = "puzzletree") -> str:
70 """Get version of the given distribution.
72 Parameters:
73 dist: A distribution name.
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"
86def version_info() -> Text:
87 """Get version information.
89 Returns:
90 Version information.
91 """
92 version = get_version()
93 return Text.assemble(("puzzletree: ", "peach"), (f"{version}", "bold"))
96def get_debug_info() -> Environment:
97 """Get debug/environment information.
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 )
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")
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)
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)
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
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")
181def debug_info(console: Console | None = None) -> None:
182 """Return debug information."""
183 if not console:
184 console = Console(theme=set_theme())
186 env = get_debug_info()
187 from puzzletree.cli.messages.layout import use_layout # noqa: PLC0415 - deferred to avoid circular import
189 if use_layout(console):
190 console.print(_make_debug_layout(env))
191 else:
192 console.print(_make_debug_panel(env))