Skip to content

Commit 8fa531c

Browse files
feat: Add SUPPORTED_DETECTION_MODES to parsers for auto-generated table (#1219)
## Summary - PR #1209 manually added a "Supported Detection Modes" column to the instruments table, but the table is auto-generated by `hatch run scripts:update-instrument-table` - This PR moves detection mode data into parser class attributes (`SUPPORTED_DETECTION_MODES`) so the generation script produces the column automatically - Adds `SUPPORTED_DETECTION_MODES: str | None = None` to the `VendorParser` base class - Declares detection modes in all 53 parser classes (matching the values from PR #1209) - Updates `get_table_contents()` to render the 5th column ## Changes - `vendor_parser.py`: New optional class attribute - `parser_factory.py`: New `supported_detection_modes` property on `Vendor`, updated table generation - All 53 parser files: One-line addition of `SUPPORTED_DETECTION_MODES = "..."` (or `None` for liquid handlers) ## Test plan - [x] `test_table_contents` passes — generated output matches the existing `.adoc` file - [x] All 318 parser_factory and discover_vendor tests pass - [x] Lint passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f1ef5c6 commit 8fa531c

59 files changed

Lines changed: 98 additions & 15 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/skills/parser-generator/scripts/create_parser.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class {class_name}(VendorParser[Data, Model]):
5858
DISPLAY_NAME = "{display_name}"
5959
RELEASE_STATE = ReleaseState.WORKING_DRAFT
6060
SUPPORTED_EXTENSIONS = "{extension}"
61+
SUPPORTED_DETECTION_MODES = None # TODO: Set detection modes (e.g. "Absorbance, Fluorescence")
6162
SCHEMA_MAPPER = Mapper
6263
6364
def create_data(self, named_file_contents: NamedFileContents) -> Data:
@@ -367,10 +368,11 @@ def main():
367368
print("\n📝 Next steps:")
368369
print(f"1. Edit {parser_name}_reader.py to implement file parsing")
369370
print(f"2. Edit {parser_name}_structure.py to map data to schema")
370-
print(f"3. Add test data to {testdata_dir}")
371-
print(f"4. Run tests: hatch run test:pytest tests/parsers/{parser_name}/")
372-
print("5. Register parser in src/allotropy/parser_factory.py")
373-
print("6. Update README.md with parser information")
371+
print(f"3. Set SUPPORTED_DETECTION_MODES in {parser_name}_parser.py")
372+
print(f"4. Add test data to {testdata_dir}")
373+
print(f"5. Run tests: hatch run test:pytest tests/parsers/{parser_name}/")
374+
print("6. Register parser in src/allotropy/parser_factory.py")
375+
print("7. Run: hatch run scripts:update-instrument-table")
374376

375377

376378
if __name__ == "__main__":

.claude/skills/parser-generator/skill.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@ The script dynamically scans your local allotropy repository for all available s
6868
Use the `create_parser.py` script to generate the complete parser:
6969

7070
```bash
71-
python scripts/create_parser.py <parser_name> <vendor_name> --example <example_file>
71+
python scripts/create_parser.py <parser_name> <schema_regex> --display_name "Vendor Instrument" --detection_modes "Absorbance, Fluorescence"
7272
```
7373

74+
The `--detection_modes` flag sets the `SUPPORTED_DETECTION_MODES` class attribute, which populates the instruments table. Use comma-separated values for multiple modes (e.g. `"Absorbance, Fluorescence, Luminescence"`). Omit for instruments without detection (liquid handlers).
75+
7476
### Parser Structure Created
7577

7678
```
@@ -122,6 +124,7 @@ Generate `VendorParser` subclass with:
122124
- `DISPLAY_NAME` - User-friendly instrument name
123125
- `RELEASE_STATE` - Start with `ReleaseState.WORKING_DRAFT`
124126
- `SUPPORTED_EXTENSIONS` - File extensions (from analysis)
127+
- `SUPPORTED_DETECTION_MODES` - Detection modes the parser supports (e.g. `"Absorbance, Fluorescence"`) or `None` for instruments without detection (e.g. liquid handlers). This populates the "Supported Detection Modes" column in the supported instruments table.
125128
- `SCHEMA_MAPPER` - Reference to schema mapper
126129
- `create_data()` - Orchestrate reader + structure → Data
127130

@@ -196,10 +199,11 @@ _VENDOR_TO_PARSER: dict[Vendor, type[VendorParser]] = {
196199
- [ ] Confirm or select schema with `list_schemas.py`
197200
- [ ] Generate parser using `create_parser.py`
198201
- [ ] Review and adjust generated code
202+
- [ ] Set `SUPPORTED_DETECTION_MODES` to the correct value for the instrument
199203
- [ ] Add example test data to `testdata/`
200204
- [ ] Run tests and validate output
201205
- [ ] Register parser in `parser_factory.py`
202-
- [ ] Update README with parser info
206+
- [ ] Run `hatch run scripts:update-instrument-table` to regenerate the supported instruments table
203207
- [ ] Update `RELEASE_STATE` when stable
204208

205209
## Key Design Principles

scripts/create_parser.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ def add_to_parser_factory(parser_name: str, enum_name: str, class_name: str) ->
6161

6262

6363
def create_parser(
64-
name: str, schema_path: Path, display_name: str | None = None
64+
name: str,
65+
schema_path: Path,
66+
display_name: str | None = None,
67+
detection_modes: str | None = None,
6568
) -> None:
6669
name = name.replace(" ", "_").replace("-", "_").lower()
6770
enum_name = name.upper()
@@ -84,6 +87,8 @@ def create_parser(
8487
Path(tests_dir, "__init__.py").touch()
8588
Path(tests_dir, "testdata").mkdir()
8689

90+
detection_modes_value = f'"{detection_modes}"' if detection_modes else "None"
91+
8792
template_replacements = {
8893
"PARSER_NAME": name,
8994
"CLASS_NAME_PREFIX": class_name_prefix,
@@ -92,6 +97,7 @@ def create_parser(
9297
"MODEL_IMPORT_PATH": import_path,
9398
"MAPPER_IMPORT_PATH": import_path.replace("models", "schema_mappers"),
9499
"MANIFEST": manifest,
100+
"SUPPORTED_DETECTION_MODES": detection_modes_value,
95101
}
96102
write_template_files(
97103
parser_dir,
@@ -119,8 +125,15 @@ def create_parser(
119125
@click.option(
120126
"--display_name", help="Display name for parser, defaults to title case of NAME"
121127
)
128+
@click.option(
129+
"--detection_modes",
130+
help="Comma-separated detection modes (e.g. 'Absorbance, Fluorescence')",
131+
)
122132
def _create_parser(
123-
name: str, schema_regex: str, display_name: str | None = None
133+
name: str,
134+
schema_regex: str,
135+
display_name: str | None = None,
136+
detection_modes: str | None = None,
124137
) -> None:
125138
"""Create parser with name NAME for schema found with SCHEMA_REGEX."""
126139
if "parser" in name.lower():
@@ -135,7 +148,7 @@ def _create_parser(
135148
msg = f"Found more than one schema with schema regex, please narrow down: {[str(p) for p in schema_paths]}"
136149
raise ValueError(msg)
137150

138-
create_parser(name, schema_paths[0], display_name)
151+
create_parser(name, schema_paths[0], display_name, detection_modes)
139152

140153
from allotropy.parser_factory import update_supported_instruments
141154

scripts/templates/parser_template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class $CLASS_NAME_PREFIX$Parser(VendorParser[Data, Model]):
2323
DISPLAY_NAME = DISPLAY_NAME
2424
RELEASE_STATE = ReleaseState.WORKING_DRAFT
2525
SUPPORTED_EXTENSIONS = $CLASS_NAME_PREFIX$Reader.SUPPORTED_EXTENSIONS
26+
SUPPORTED_DETECTION_MODES = $SUPPORTED_DETECTION_MODES$
2627
SCHEMA_MAPPER = Mapper
2728

2829
def create_data(self, named_file_contents: NamedFileContents) -> Data:

src/allotropy/parser_factory.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ def supported_extensions(self) -> list[str]:
207207
for ext in self.get_parser().SUPPORTED_EXTENSIONS.split(",")
208208
]
209209

210+
@property
211+
def supported_detection_modes(self) -> str | None:
212+
return self.get_parser().SUPPORTED_DETECTION_MODES
213+
210214
@property
211215
def manifests(self) -> list[str]:
212216
# NOTE: this is a list because eventually parsers will support multiple schemas as they are upgraded.
@@ -355,16 +359,16 @@ def get_table_contents() -> str:
355359
continue
356360
table_data[vendor.technique].append(vendor)
357361

358-
contents += '[cols="4*^.^"]\n'
362+
contents += '[cols="5*^.^"]\n'
359363
contents += "|===\n"
360-
contents += (
361-
"|Instrument Category|Instrument Software|Release Status|Exported ASM Schema\n"
362-
)
364+
contents += "|Instrument Category|Instrument Software|Release Status|Exported ASM Schema|Supported Detection Modes\n"
363365
for technique in sorted(table_data):
364366
vendors = table_data[technique]
365-
contents += f".{len(vendors)}+|{technique}|{vendors[0].display_name}|{vendors[0].release_state}|{vendors[0].asm_versions[0]}\n"
367+
modes = vendors[0].supported_detection_modes or "N/A"
368+
contents += f".{len(vendors)}+|{technique}|{vendors[0].display_name}|{vendors[0].release_state}|{vendors[0].asm_versions[0]}|{modes}\n"
366369
for vendor in vendors[1:]:
367-
contents += f"|{vendor.display_name}|{vendor.release_state}|{vendor.asm_versions[0]}\n"
370+
modes = vendor.supported_detection_modes or "N/A"
371+
contents += f"|{vendor.display_name}|{vendor.release_state}|{vendor.asm_versions[0]}|{modes}\n"
368372
contents += "|==="
369373

370374
return contents

src/allotropy/parsers/agilent_gen5/agilent_gen5_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class AgilentGen5Parser(VendorParser[Data, Model]):
1919
DISPLAY_NAME = "Agilent Gen5"
2020
RELEASE_STATE = ReleaseState.RECOMMENDED
2121
SUPPORTED_EXTENSIONS = AgilentGen5Reader.SUPPORTED_EXTENSIONS
22+
SUPPORTED_DETECTION_MODES = "Absorbance, Fluorescence, Luminescence"
2223
SCHEMA_MAPPER = Mapper
2324

2425
@classmethod

src/allotropy/parsers/agilent_gen5_image/agilent_gen5_image_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class AgilentGen5ImageParser(VendorParser[Data, Model]):
2424
DISPLAY_NAME = "Agilent Gen5 Image"
2525
RELEASE_STATE = ReleaseState.RECOMMENDED
2626
SUPPORTED_EXTENSIONS = AgilentGen5Reader.SUPPORTED_EXTENSIONS
27+
SUPPORTED_DETECTION_MODES = "Optical Imaging"
2728
SCHEMA_MAPPER = Mapper
2829

2930
@classmethod

src/allotropy/parsers/agilent_openlab_cds/agilent_openlab_cds_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class AgilentOpenLabCDSParser(VendorParser[Data, Model]):
2323
DISPLAY_NAME = constants.DISPLAY_NAME
2424
RELEASE_STATE = ReleaseState.RECOMMENDED
2525
SUPPORTED_EXTENSIONS = "rslt"
26+
SUPPORTED_DETECTION_MODES = "Absorbance, Fluorescence"
2627
SCHEMA_MAPPER = Mapper
2728

2829
@classmethod

src/allotropy/parsers/agilent_tapestation_analysis/agilent_tapestation_analysis_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class AgilentTapestationAnalysisParser(VendorParser[Data, Model]):
2121
DISPLAY_NAME = "Agilent TapeStation Analysis"
2222
RELEASE_STATE = ReleaseState.RECOMMENDED
2323
SUPPORTED_EXTENSIONS = "xml"
24+
SUPPORTED_DETECTION_MODES = "Fluorescence"
2425
SCHEMA_MAPPER = Mapper
2526

2627
@classmethod

src/allotropy/parsers/appbio_absolute_q/appbio_absolute_q_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class AppbioAbsoluteQParser(VendorParser[Data, Model]):
2626
DISPLAY_NAME = "AppBio AbsoluteQ"
2727
RELEASE_STATE = ReleaseState.RECOMMENDED
2828
SUPPORTED_EXTENSIONS = AppbioAbsoluteQReader.SUPPORTED_EXTENSIONS
29+
SUPPORTED_DETECTION_MODES = "Fluorescence"
2930
SCHEMA_MAPPER = Mapper
3031

3132
@classmethod

0 commit comments

Comments
 (0)