(
path: pathlib.Path,
*,
marker: str,
fields: Sequence[str],
)
| 122 | |
| 123 | |
| 124 | def parse_tsv( |
| 125 | path: pathlib.Path, |
| 126 | *, |
| 127 | marker: str, |
| 128 | fields: Sequence[str], |
| 129 | ) -> tuple[dict[str, str], list[dict[str, str]]]: |
| 130 | regular_status(path, ceiling=MAX_MANIFEST_BYTES, label="evidence manifest") |
| 131 | try: |
| 132 | text = path.read_text(encoding="utf-8") |
| 133 | except UnicodeError as error: |
| 134 | raise ContractError(f"evidence manifest is not UTF-8: {path}") from error |
| 135 | if "\x00" in text or "\r" in text: |
| 136 | raise ContractError(f"evidence manifest contains forbidden control bytes: {path}") |
| 137 | lines = text.splitlines() |
| 138 | if not lines or lines[0] != f"# {marker}": |
| 139 | raise ContractError(f"evidence marker is missing: {marker}") |
| 140 | metadata: dict[str, str] = {} |
| 141 | cursor = 1 |
| 142 | while cursor < len(lines) and lines[cursor].startswith("# "): |
| 143 | raw = lines[cursor][2:] |
| 144 | key, separator, value = raw.partition("=") |
| 145 | if ( |
| 146 | not separator |
| 147 | or not key |
| 148 | or not value |
| 149 | or key in metadata |
| 150 | or any(ord(character) < 32 for character in raw) |
| 151 | ): |
| 152 | raise ContractError(f"malformed or duplicate evidence metadata: {raw!r}") |
| 153 | metadata[key] = value |
| 154 | cursor += 1 |
| 155 | reader = csv.DictReader(lines[cursor:], delimiter="\t") |
| 156 | if tuple(reader.fieldnames or ()) != tuple(fields): |
| 157 | raise ContractError(f"evidence TSV header is malformed or unexpected: {path}") |
| 158 | rows = list(reader) |
| 159 | if any( |
| 160 | None in row |
| 161 | or any(row.get(field) is None for field in fields) |
| 162 | or any( |
| 163 | any(ord(character) < 32 for character in value) |
| 164 | for value in row.values() |
| 165 | if value |
| 166 | ) |
| 167 | for row in rows |
| 168 | ): |
| 169 | raise ContractError(f"evidence TSV has missing or surplus cells: {path}") |
| 170 | return metadata, rows |
| 171 | |
| 172 | |
| 173 | def expected_properties(target: str) -> tuple[str, str, str, str]: |
no test coverage detected