Skip to content

Commit a87437f

Browse files
refactor: apply review suggestions to concore doctor
- Use importlib.metadata instead of __import__ for package version checks - Fix Python version check to use tuple comparison (sys.version_info >= (3, 9)) - Make MATLAB version_flag a list to handle multi-arg flags correctly - Support list-type version_flag in _get_version() - Show found executable name (e.g., [g++]) in tool output - Treat Docker-not-found as warning instead of error (optional tool) - Add concore.repo to config file checks - Fix concore.mcr passed counter not incrementing on valid path - Build env var list dynamically from TOOL_DEFINITIONS config_keys - Handle empty summary_parts gracefully - Remove unused IMPORT_NAME_MAP (replaced by importlib.metadata) - Remove duplicate test (test_resolved_path_contains_concore) - Patch os.name via correct module path in platform key tests - Use __version__ import instead of hardcoded version in test
1 parent ae39be0 commit a87437f

2 files changed

Lines changed: 41 additions & 45 deletions

File tree

concore_cli/commands/doctor.py

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import importlib.metadata
12
import shutil
23
import subprocess
34
import sys
@@ -65,7 +66,7 @@
6566
"posix": ["matlab"],
6667
"windows": ["matlab"],
6768
},
68-
"version_flag": "-batch \"disp('ok')\"",
69+
"version_flag": ["-batch", "disp('ok')"],
6970
"config_keys": ["MATLABEXE", "MATLABWIN"],
7071
"install_hints": {
7172
"Linux": "Install from https://mathworks.com/downloads/",
@@ -103,13 +104,6 @@
103104
"matplotlib": "pip install concore[demo]",
104105
}
105106

106-
# Map import names that differ from package names
107-
IMPORT_NAME_MAP = {
108-
"beautifulsoup4": "bs4",
109-
"pyzmq": "zmq",
110-
}
111-
112-
113107
def _get_platform_key():
114108
"""Return 'posix' or 'windows' based on OS."""
115109
return "windows" if os.name == "nt" else "posix"
@@ -146,8 +140,12 @@ def _detect_tool(names):
146140
def _get_version(path, version_flag):
147141
"""Run tool with version flag and return first line of output."""
148142
try:
143+
if isinstance(version_flag, list):
144+
cmd = [path] + version_flag
145+
else:
146+
cmd = [path, version_flag]
149147
result = subprocess.run(
150-
[path, version_flag],
148+
cmd,
151149
capture_output=True,
152150
text=True,
153151
timeout=10,
@@ -176,14 +174,10 @@ def _check_docker_daemon(docker_path):
176174

177175
def _check_package(package_name):
178176
"""Check if a Python package is importable and get its version."""
179-
import_name = IMPORT_NAME_MAP.get(package_name, package_name)
180177
try:
181-
mod = __import__(import_name)
182-
version = getattr(mod, "__version__", None)
183-
if version is None:
184-
version = getattr(mod, "VERSION", "installed")
185-
return True, str(version)
186-
except ImportError:
178+
version = importlib.metadata.version(package_name)
179+
return True, version
180+
except importlib.metadata.PackageNotFoundError:
187181
return False, None
188182

189183

@@ -207,8 +201,7 @@ def doctor_check(console):
207201

208202
# Python version
209203
py_version = platform.python_version()
210-
py_major, py_minor = sys.version_info.major, sys.version_info.minor
211-
if py_major >= 3 and py_minor >= 9:
204+
if sys.version_info >= (3, 9):
212205
console.print(f" [green]✓[/green] Python {py_version} (>= 3.9 required)")
213206
passed += 1
214207
else:
@@ -261,6 +254,7 @@ def doctor_check(console):
261254
if path:
262255
version = _get_version(path, tool_def["version_flag"])
263256
version_str = f" ({version})" if version else ""
257+
exe_info = f" [{found_name}]" if found_name else ""
264258
extra = ""
265259
if tool_label == "Docker":
266260
daemon_ok = _check_docker_daemon(path)
@@ -272,24 +266,20 @@ def doctor_check(console):
272266
if not daemon_ok:
273267
warnings += 1
274268
console.print(
275-
f" [yellow]![/yellow] {tool_label}{version_str} "
276-
f"→ {path}{extra}"
269+
f" [yellow]![/yellow] {tool_label}{exe_info}"
270+
f"{version_str} {path}{extra}"
277271
)
278272
continue
279273
console.print(
280-
f" [green]✓[/green] {tool_label}{version_str}{path}{extra}"
274+
f" [green]✓[/green] {tool_label}{exe_info}"
275+
f"{version_str}{path}{extra}"
281276
)
282277
passed += 1
283278
else:
284279
hint = tool_def["install_hints"].get(plat_name, "")
285280
hint_str = f" (install: {hint})" if hint else ""
286-
# MATLAB is optional if Octave is available, show as warning
287-
if tool_label == "MATLAB":
288-
console.print(
289-
f" [yellow]![/yellow] {tool_label} → Not found{hint_str}"
290-
)
291-
warnings += 1
292-
elif tool_label == "Verilog (iverilog)":
281+
# Docker, MATLAB, Verilog are optional — show as warning
282+
if tool_label in ("MATLAB", "Verilog (iverilog)", "Docker"):
293283
console.print(
294284
f" [yellow]![/yellow] {tool_label} → Not found{hint_str}"
295285
)
@@ -309,6 +299,7 @@ def doctor_check(console):
309299
"concore.tools": "Tool path overrides",
310300
"concore.octave": "Treat .m files as Octave",
311301
"concore.mcr": "MATLAB Compiler Runtime path",
302+
"concore.repo": "Docker repository path",
312303
"concore.sudo": "Docker executable override",
313304
}
314305

@@ -331,17 +322,22 @@ def doctor_check(console):
331322
console.print(
332323
f" [green]✓[/green] {filename}{content}"
333324
)
325+
passed += 1
334326
else:
335327
console.print(
336328
f" [yellow]![/yellow] {filename} → "
337329
f"path does not exist: {content}"
338330
)
339331
warnings += 1
340-
continue
332+
continue
341333
elif filename == "concore.sudo":
342334
console.print(
343335
f" [green]✓[/green] {filename}{content}"
344336
)
337+
elif filename == "concore.repo":
338+
console.print(
339+
f" [green]✓[/green] {filename}{content}"
340+
)
345341
else:
346342
console.print(
347343
f" [green]✓[/green] {filename} → Enabled"
@@ -357,11 +353,12 @@ def doctor_check(console):
357353
f" [dim]—[/dim] {filename} → Not set ({description})"
358354
)
359355

360-
# Check environment variables
361-
env_vars = [
362-
"CONCORE_CPPEXE", "CONCORE_PYTHONEXE", "CONCORE_VEXE",
363-
"CONCORE_OCTAVEEXE", "CONCORE_MATLABEXE", "DOCKEREXE",
364-
]
356+
# Build environment variable list from TOOL_DEFINITIONS config_keys
357+
env_vars = []
358+
for tool_def in TOOL_DEFINITIONS.values():
359+
for key in tool_def.get("config_keys", []):
360+
env_vars.append(f"CONCORE_{key}")
361+
env_vars.append("DOCKEREXE")
365362
env_set = [v for v in env_vars if os.environ.get(v)]
366363
if env_set:
367364
console.print(
@@ -412,7 +409,12 @@ def doctor_check(console):
412409
if errors:
413410
summary_parts.append(f"[red]{errors} error(s)[/red]")
414411

415-
console.print(f"[bold]Summary:[/bold] {', '.join(summary_parts)}")
412+
if summary_parts:
413+
summary_text = ", ".join(summary_parts)
414+
else:
415+
summary_text = "[yellow]No checks were run.[/yellow]"
416+
417+
console.print(f"[bold]Summary:[/bold] {summary_text}")
416418

417419
if errors == 0:
418420
console.print()

tests/test_doctor.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ def test_doctor_shows_python_version(self):
4545

4646
def test_doctor_shows_concore_version(self):
4747
"""Doctor should detect and show concore version."""
48+
from concore_cli import __version__
4849
result = self.runner.invoke(cli, ["doctor"])
4950
self.assertIn("concore", result.output)
50-
self.assertIn("1.0.0", result.output)
51+
self.assertIn(__version__, result.output)
5152

5253
def test_doctor_shows_concorepath(self):
5354
"""Doctor should show the CONCOREPATH."""
@@ -106,13 +107,13 @@ def test_returns_valid_key(self):
106107
key = _get_platform_key()
107108
self.assertIn(key, ["posix", "windows"])
108109

109-
@patch("os.name", "nt")
110+
@patch("concore_cli.commands.doctor.os.name", "nt")
110111
def test_windows_detection(self):
111112
"""Should return 'windows' when os.name is 'nt'."""
112113
key = _get_platform_key()
113114
self.assertEqual(key, "windows")
114115

115-
@patch("os.name", "posix")
116+
@patch("concore_cli.commands.doctor.os.name", "posix")
116117
def test_posix_detection(self):
117118
"""Should return 'posix' when os.name is 'posix'."""
118119
key = _get_platform_key()
@@ -153,13 +154,6 @@ def test_resolves_to_existing_path(self):
153154
result = _resolve_concore_path()
154155
self.assertIsInstance(result, Path)
155156

156-
def test_resolved_path_contains_concore(self):
157-
"""Resolved path should contain concore.py if run from repo."""
158-
result = _resolve_concore_path()
159-
# In the test environment (run from repo), concore.py should exist
160-
# This may not hold in all CI environments, so we just check it's a Path
161-
self.assertIsInstance(result, Path)
162-
163157

164158
class TestDoctorWithConfig(unittest.TestCase):
165159
"""Tests for doctor command with config files present."""

0 commit comments

Comments
 (0)