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 = ( '