diff --git a/.claudeignore b/.claudeignore
new file mode 100644
index 0000000..375aed6
--- /dev/null
+++ b/.claudeignore
@@ -0,0 +1,39 @@
+# Python-specific ignores
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+.Python
+*.so
+*.egg
+*.egg-info/
+dist/
+build/
+.eggs/
+
+# Virtual environments
+.venv/
+venv/
+ENV/
+env/
+
+# Testing & coverage
+.pytest_cache/
+.mypy_cache/
+.ruff_cache/
+htmlcov/
+.coverage
+.coverage.*
+.tox/
+noxfile.py
+.nox/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# Git
+.git/
diff --git a/pyproject.toml b/pyproject.toml
index cea7995..a31d216 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,6 +42,7 @@ dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
"pytest-cov>=5.0",
+ "pytest-timeout>=2.0",
]
[project.scripts]
@@ -76,6 +77,10 @@ markers = [
"network: tests that require network access (deselect with -m 'not network')",
"skipif: conditional skip",
]
+filterwarnings = [
+ # nmrglue uses deprecated NumPy 2.0 dtype aliases (fixed in their repo, pending release)
+ "ignore::DeprecationWarning:nmrglue.*",
+]
[tool.coverage.run]
source = ["device_use"]
diff --git a/src/device_use/cli.py b/src/device_use/cli.py
index f4dcb2a..47d78c5 100644
--- a/src/device_use/cli.py
+++ b/src/device_use/cli.py
@@ -255,6 +255,8 @@ def _scaffold(device_name: str, output_dir: str):
class_name = "".join(
w.capitalize() for w in device_name.replace("-", " ").replace("_", " ").split()
)
+ # Extract vendor from device name (first part before dash/underscore)
+ vendor = device_name.split("-")[0].split("_")[0].capitalize()
root = os.path.join(output_dir, pkg_name)
if os.path.exists(root):
@@ -337,7 +339,7 @@ def __init__(self, mode: ControlMode = ControlMode.OFFLINE):
def info(self) -> InstrumentInfo:
return InstrumentInfo(
name="{class_name}",
- vendor="TODO",
+ vendor="{vendor}",
instrument_type="{slug}",
supported_modes=[ControlMode.OFFLINE, ControlMode.API, ControlMode.GUI],
version="0.1.0",
diff --git a/tests/test_cli.py b/tests/test_cli.py
index c4d9c80..f1864ae 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -257,6 +257,27 @@ def test_scaffold_existing_dir(self, tmp_path, capsys):
out = capsys.readouterr().out
assert "already exists" in out
+ def test_scaffold_vendor_extraction(self, tmp_path):
+ """Vendor should be extracted from device name (first part before dash)."""
+ cli._scaffold("biotek-gen5", str(tmp_path))
+ adapter_file = (
+ tmp_path / "device_use_biotek_gen5" / "src" / "device_use_biotek_gen5" / "adapter.py"
+ )
+ content = adapter_file.read_text()
+ assert 'vendor="Biotek"' in content
+ # Ensure vendor field doesn't have the placeholder
+ assert 'vendor="TODO"' not in content
+
+ def test_scaffold_vendor_single_word(self, tmp_path):
+ """Vendor should be capitalized for single-word device names."""
+ cli._scaffold("mydevice", str(tmp_path))
+ adapter_file = (
+ tmp_path / "device_use_mydevice" / "src" / "device_use_mydevice" / "adapter.py"
+ )
+ content = adapter_file.read_text()
+ assert 'vendor="Mydevice"' in content
+ assert 'vendor="TODO"' not in content
+
# ---------------------------------------------------------------------------
# _write
diff --git a/tests/test_converter_coverage.py b/tests/test_converter_coverage.py
index c0b8fd0..75289b2 100644
--- a/tests/test_converter_coverage.py
+++ b/tests/test_converter_coverage.py
@@ -28,7 +28,7 @@
class TestResolveRedirect:
- def test_url_without_enUS_pattern_returns_none(self, tmp_path):
+ def test_url_without_en_us_pattern_returns_none(self, tmp_path):
"""Line 182: redirect URL that doesn't match the en-US regex."""
stub = tmp_path / "stub.html"
stub.write_text('')
@@ -247,7 +247,7 @@ def test_description_fallback_summary(self, tmp_path):
assert result["summary"] == "This is a detailed description for the command page."
assert result["commands"] == []
- def test_convert_missing_enUS_dir(self, tmp_path):
+ def test_convert_missing_en_us_dir(self, tmp_path):
"""convert_topspin_command returns None when en-US dir is missing."""
stub = tmp_path / "missing.html"
stub.write_text(
@@ -291,7 +291,6 @@ def test_exception_in_convert_is_caught(self, tmp_path, capsys):
pass
# More direct approach: patch convert_topspin_command itself
- original = convert_topspin_command
with patch(
"device_use.knowledge.converter.convert_topspin_command",
side_effect=RuntimeError("boom"),
@@ -589,7 +588,7 @@ def test_single_word_no_dash(self):
class TestRedirectParserEdgeCases:
- def test_uppercase_REFRESH(self):
+ def test_uppercase_refresh(self):
"""http-equiv='REFRESH' (uppercase) is matched case-insensitively."""
html = (
'