Skip to content

Commit f396857

Browse files
robotlearning123sandia777claude
authored
chore: stabilization merge — CLI fix, pytest warnings, claudeignore (#30)
## Summary - fix(cli): extract vendor from device name in scaffold command - fix: resolve pytest warnings - chore: add .claudeignore for agent context optimization Stabilization merge from analysis-temp branch. ## Test plan - [x] Existing tests pass - [x] No secrets in diff --------- Co-authored-by: Cong <72737794+robolearning123@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3db278d commit f396857

8 files changed

Lines changed: 93 additions & 28 deletions

File tree

.claudeignore

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Python-specific ignores
2+
__pycache__/
3+
*.pyc
4+
*.pyo
5+
*.pyd
6+
.Python
7+
*.so
8+
*.egg
9+
*.egg-info/
10+
dist/
11+
build/
12+
.eggs/
13+
14+
# Virtual environments
15+
.venv/
16+
venv/
17+
ENV/
18+
env/
19+
20+
# Testing & coverage
21+
.pytest_cache/
22+
.mypy_cache/
23+
.ruff_cache/
24+
htmlcov/
25+
.coverage
26+
.coverage.*
27+
.tox/
28+
noxfile.py
29+
.nox/
30+
31+
# IDE
32+
.vscode/
33+
.idea/
34+
*.swp
35+
*.swo
36+
*~
37+
38+
# Git
39+
.git/

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dev = [
4242
"pytest>=8.0",
4343
"pytest-asyncio>=0.23",
4444
"pytest-cov>=5.0",
45+
"pytest-timeout>=2.0",
4546
]
4647

4748
[project.scripts]
@@ -76,6 +77,10 @@ markers = [
7677
"network: tests that require network access (deselect with -m 'not network')",
7778
"skipif: conditional skip",
7879
]
80+
filterwarnings = [
81+
# nmrglue uses deprecated NumPy 2.0 dtype aliases (fixed in their repo, pending release)
82+
"ignore::DeprecationWarning:nmrglue.*",
83+
]
7984

8085
[tool.coverage.run]
8186
source = ["device_use"]

src/device_use/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ def _scaffold(device_name: str, output_dir: str):
255255
class_name = "".join(
256256
w.capitalize() for w in device_name.replace("-", " ").replace("_", " ").split()
257257
)
258+
# Extract vendor from device name (first part before dash/underscore)
259+
vendor = device_name.split("-")[0].split("_")[0].capitalize()
258260
root = os.path.join(output_dir, pkg_name)
259261

260262
if os.path.exists(root):
@@ -337,7 +339,7 @@ def __init__(self, mode: ControlMode = ControlMode.OFFLINE):
337339
def info(self) -> InstrumentInfo:
338340
return InstrumentInfo(
339341
name="{class_name}",
340-
vendor="TODO",
342+
vendor="{vendor}",
341343
instrument_type="{slug}",
342344
supported_modes=[ControlMode.OFFLINE, ControlMode.API, ControlMode.GUI],
343345
version="0.1.0",

tests/test_cli.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,27 @@ def test_scaffold_existing_dir(self, tmp_path, capsys):
257257
out = capsys.readouterr().out
258258
assert "already exists" in out
259259

260+
def test_scaffold_vendor_extraction(self, tmp_path):
261+
"""Vendor should be extracted from device name (first part before dash)."""
262+
cli._scaffold("biotek-gen5", str(tmp_path))
263+
adapter_file = (
264+
tmp_path / "device_use_biotek_gen5" / "src" / "device_use_biotek_gen5" / "adapter.py"
265+
)
266+
content = adapter_file.read_text()
267+
assert 'vendor="Biotek"' in content
268+
# Ensure vendor field doesn't have the placeholder
269+
assert 'vendor="TODO"' not in content
270+
271+
def test_scaffold_vendor_single_word(self, tmp_path):
272+
"""Vendor should be capitalized for single-word device names."""
273+
cli._scaffold("mydevice", str(tmp_path))
274+
adapter_file = (
275+
tmp_path / "device_use_mydevice" / "src" / "device_use_mydevice" / "adapter.py"
276+
)
277+
content = adapter_file.read_text()
278+
assert 'vendor="Mydevice"' in content
279+
assert 'vendor="TODO"' not in content
280+
260281

261282
# ---------------------------------------------------------------------------
262283
# _write

tests/test_converter_coverage.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929

3030
class TestResolveRedirect:
31-
def test_url_without_enUS_pattern_returns_none(self, tmp_path):
31+
def test_url_without_en_us_pattern_returns_none(self, tmp_path):
3232
"""Line 182: redirect URL that doesn't match the en-US regex."""
3333
stub = tmp_path / "stub.html"
3434
stub.write_text('<meta http-equiv="refresh" content="0;url=/some/other/path/page.html">')
@@ -247,7 +247,7 @@ def test_description_fallback_summary(self, tmp_path):
247247
assert result["summary"] == "This is a detailed description for the command page."
248248
assert result["commands"] == []
249249

250-
def test_convert_missing_enUS_dir(self, tmp_path):
250+
def test_convert_missing_en_us_dir(self, tmp_path):
251251
"""convert_topspin_command returns None when en-US dir is missing."""
252252
stub = tmp_path / "missing.html"
253253
stub.write_text(
@@ -291,7 +291,6 @@ def test_exception_in_convert_is_caught(self, tmp_path, capsys):
291291
pass
292292

293293
# More direct approach: patch convert_topspin_command itself
294-
original = convert_topspin_command
295294
with patch(
296295
"device_use.knowledge.converter.convert_topspin_command",
297296
side_effect=RuntimeError("boom"),
@@ -589,7 +588,7 @@ def test_single_word_no_dash(self):
589588

590589

591590
class TestRedirectParserEdgeCases:
592-
def test_uppercase_REFRESH(self):
591+
def test_uppercase_refresh(self):
593592
"""http-equiv='REFRESH' (uppercase) is matched case-insensitively."""
594593
html = (
595594
'<meta http-equiv="REFRESH"'
@@ -774,7 +773,7 @@ def test_main_with_index_output_flag(self, tmp_path, capsys):
774773
):
775774
main()
776775

777-
captured = capsys.readouterr()
776+
capsys.readouterr()
778777
assert custom_index.exists()
779778

780779
def test_main_no_entries_no_index(self, tmp_path, capsys):

tests/test_openai_compat.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
@pytest.fixture
1616
def mock_async_openai():
1717
"""Fixture to mock AsyncOpenAI client."""
18-
with patch("device_use.backends.openai_compat.AsyncOpenAI") as MockAsyncOpenAI:
19-
mock_client = MockAsyncOpenAI.return_value
20-
yield MockAsyncOpenAI, mock_client
18+
with patch("device_use.backends.openai_compat.AsyncOpenAI") as mock_openai_cls:
19+
mock_client = mock_openai_cls.return_value
20+
yield mock_openai_cls, mock_client
2121

2222

2323
class TestSupportsComputerUse:
@@ -55,8 +55,8 @@ class TestOpenAICompatBackendInitialization:
5555
],
5656
)
5757
def test_initialization(self, mock_async_openai, model, expected_native_cu):
58-
MockAsyncOpenAI_class, mock_async_openai_instance = mock_async_openai
59-
MockAsyncOpenAI_class.reset_mock()
58+
mock_openai_cls, mock_async_openai_instance = mock_async_openai
59+
mock_openai_cls.reset_mock()
6060
backend = OpenAICompatBackend(model=model, api_key="test_key", base_url="http://test.url")
6161

6262
assert backend._model == model
@@ -66,19 +66,17 @@ def test_initialization(self, mock_async_openai, model, expected_native_cu):
6666
assert backend.system_prompt == ""
6767
assert backend._previous_response_id is None
6868

69-
MockAsyncOpenAI_class.assert_called_once_with(
70-
api_key="test_key", base_url="http://test.url"
71-
)
69+
mock_openai_cls.assert_called_once_with(api_key="test_key", base_url="http://test.url")
7270

7371
def test_default_values(self, mock_async_openai):
74-
MockAsyncOpenAI_class, mock_async_openai_instance = mock_async_openai
72+
mock_openai_cls, mock_async_openai_instance = mock_async_openai
7573
backend = OpenAICompatBackend(api_key="test_key") # Add api_key here
7674
assert backend._model == "gpt-5.4"
7775
assert backend._max_tokens == 4096
7876
assert backend._native_cu is True # gpt-5.4 is default
7977

8078
def test_supports_grounding_property(self, mock_async_openai):
81-
MockAsyncOpenAI_class, mock_async_openai_instance = mock_async_openai
79+
mock_openai_cls, mock_async_openai_instance = mock_async_openai
8280
cu_backend = OpenAICompatBackend(model="gpt-5.4", api_key="test_key")
8381
assert cu_backend.supports_grounding is True
8482

@@ -87,7 +85,7 @@ def test_supports_grounding_property(self, mock_async_openai):
8785

8886
@pytest.fixture(autouse=True)
8987
def setup(self, mock_async_openai):
90-
MockAsyncOpenAI_class, self.mock_client = mock_async_openai
88+
mock_openai_cls, self.mock_client = mock_async_openai
9189
self.cu_backend = OpenAICompatBackend(model="gpt-5.4", api_key="test_key")
9290
self.mock_responses_create = AsyncMock()
9391
self.mock_client.responses.create = self.mock_responses_create
@@ -272,7 +270,7 @@ class TestMapCUAction:
272270

273271
@pytest.fixture(autouse=True)
274272
def setup(self, mock_async_openai):
275-
MockAsyncOpenAI_class, mock_client = (
273+
mock_openai_cls, mock_client = (
276274
mock_async_openai # unpack here, not used directly in this fixture, but good practice
277275
)
278276
self.backend = OpenAICompatBackend(model="gpt-5.4", api_key="test_key")
@@ -633,7 +631,7 @@ class TestPlanObserveLocate:
633631

634632
@pytest.fixture(autouse=True)
635633
def setup(self, mock_async_openai):
636-
MockAsyncOpenAI_class, self.mock_client = mock_async_openai
634+
mock_openai_cls, self.mock_client = mock_async_openai
637635
self.cu_backend = OpenAICompatBackend(model="gpt-5.4", api_key="test_key")
638636
self.legacy_backend = OpenAICompatBackend(model="gpt-4o", api_key="test_key")
639637
self.mock_responses_create = AsyncMock()
@@ -813,9 +811,10 @@ async def test_plan_native_remaining_actions(self):
813811

814812
async def test_plan_legacy(self):
815813
mock_choice = MagicMock()
816-
mock_choice.message.content = """
817-
{"action": {"action_type": "type", "text": "hello"}, "reasoning": "type text", "done": false, "confidence": 0.8}
818-
"""
814+
mock_choice.message.content = (
815+
'{"action": {"action_type": "type", "text": "hello"},'
816+
' "reasoning": "type text", "done": false, "confidence": 0.8}'
817+
)
819818
self.mock_chat_completions_create.return_value = MagicMock(choices=[mock_choice])
820819

821820
result = await self.legacy_backend.plan(self.screenshot_bytes, "task", history=[])
@@ -865,7 +864,7 @@ class TestLegacyChatCompletions:
865864

866865
@pytest.fixture(autouse=True)
867866
def setup(self, mock_async_openai):
868-
MockAsyncOpenAI_class, self.mock_client = mock_async_openai
867+
mock_openai_cls, self.mock_client = mock_async_openai
869868
self.legacy_backend = OpenAICompatBackend(model="gpt-4o", api_key="test_key")
870869
self.mock_chat_completions_create = AsyncMock()
871870
self.mock_client.chat.completions.create = self.mock_chat_completions_create

tests/test_spectral_library.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ def test_from_examdata_skips_no_fid(self, tmp_path):
527527
expno_dir.mkdir()
528528
# No fid file
529529

530-
mock_processor = MagicMock()
530+
MagicMock() # processor not directly needed
531531
lib = SpectralLibrary(tolerance_ppm=0.05)
532532
for sample_dir_child in sorted(examdata.iterdir()):
533533
if not sample_dir_child.is_dir() or sample_dir_child.name.startswith("."):

tests/test_web.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,8 @@ def test_analyze_stream_sse_with_formula(self, mock_get_adapter, client):
356356

357357
class TestPubChemEndpoint:
358358
def test_pubchem_lookup_success(self, client):
359-
with patch("device_use.tools.pubchem.PubChemTool") as MockTool:
360-
mock_tool = MockTool.return_value
359+
with patch("device_use.tools.pubchem.PubChemTool") as mock_tool_cls:
360+
mock_tool = mock_tool_cls.return_value
361361
mock_tool.lookup_by_name.return_value = {
362362
"CID": 1234,
363363
"IUPACName": "test-name",
@@ -373,8 +373,8 @@ def test_pubchem_lookup_success(self, client):
373373
def test_pubchem_lookup_not_found(self, client):
374374
from device_use.tools.pubchem import PubChemError
375375

376-
with patch("device_use.tools.pubchem.PubChemTool") as MockTool:
377-
mock_tool = MockTool.return_value
376+
with patch("device_use.tools.pubchem.PubChemTool") as mock_tool_cls:
377+
mock_tool = mock_tool_cls.return_value
378378
mock_tool.lookup_by_name.side_effect = PubChemError("Not found")
379379
res = client.get("/api/pubchem/nonexistent_xyz")
380380
assert res.status_code == 404

0 commit comments

Comments
 (0)