Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 25 additions & 27 deletions git-hooks/pre-commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,51 +138,49 @@ def iter_csv(repo_root: Path) -> Iterable[Path]:
yield csv_file

def sort_csv_by_slug(repo_root: Path, delimiter: str = ",") -> None:
"""
Sort CSV files by slug column without modifying quoting.
Uses the same naive delimiter parsing as rewrite_urls().
"""
"""Sort CSV files by their parsed slug field without rewriting records."""
print("Sorting CSV files by slug")

for csv_file in iter_csv(repo_root):
newline_style = detect_newline(csv_file)

with csv_file.open("r", newline="") as f:
lines = f.readlines()
text = f.read()
physical_lines = text.splitlines(keepends=True)

if len(lines) <= 1:
if not physical_lines:
continue

header_line = lines[0].rstrip("\r\n")
header_parts = [h.strip().strip('"') for h in header_line.split(delimiter)]
records = []
reader = csv.reader(io.StringIO(text, newline=""), delimiter=delimiter)
start_line = 0
for fields in reader:
end_line = reader.line_num
raw_record = "".join(physical_lines[start_line:end_line])
records.append((fields, raw_record))
start_line = end_line

if len(records) <= 1:
continue

header_fields, header_record = records[0]
header_parts = [field.strip().strip('"') for field in header_fields]
if "slug" not in header_parts:
continue

slug_idx = header_parts.index("slug")

data_lines = lines[1:]

def slug_key(raw_line: str) -> str:
parts = raw_line.rstrip("\r\n").split(delimiter)

if slug_idx >= len(parts):
def slug_key(record: tuple[list[str], str]) -> str:
fields = record[0]
if slug_idx >= len(fields):
return ""
return fields[slug_idx].strip().strip('"')

cell = parts[slug_idx].strip()

# normalize quoted slug for sorting only
if cell.startswith('"') and cell.endswith('"'):
cell = cell[1:-1]

return cell

rows_sorted = sorted(data_lines, key=slug_key)
rows_sorted = sorted(records[1:], key=slug_key)

with csv_file.open("w", newline="") as f:
f.write(header_line + newline_style)
for row in rows_sorted:
f.write(row.rstrip("\r\n") + newline_style)
f.write(header_record.rstrip("\r\n") + newline_style)
for _, raw_record in rows_sorted:
f.write(raw_record.rstrip("\r\n") + newline_style)

subprocess.run(["git", "add", str(csv_file)], check=True)
print(f" sorted: {csv_file}")
Expand Down
42 changes: 42 additions & 0 deletions tests/test_offer_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import csv
import unittest
from pathlib import Path


class OfferSchemaTests(unittest.TestCase):
"""Guard the common columns shared by every canonical offer category."""

OFFER_DIRECTORY = Path(__file__).parents[1] / "references" / "offers"

def test_every_category_declares_tag_once(self):
category_files = sorted(self.OFFER_DIRECTORY.glob("*.csv"))
self.assertEqual(
{path.stem for path in category_files},
{
"analytics",
"apis",
"bridges",
"explorers",
"faucets",
"mcpservers",
"oracles",
"platforms",
"ramps",
"sdks",
"security",
"services",
"storages",
"wallets",
},
)

for category_file in category_files:
with category_file.open(encoding="utf-8-sig", newline="") as stream:
header = next(csv.reader(stream))
columns = [column.strip() for column in header]
with self.subTest(category=category_file.stem):
self.assertEqual(columns.count("tag"), 1)


if __name__ == "__main__":
unittest.main()
57 changes: 57 additions & 0 deletions tests/test_pre_commit_sort.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import importlib.util
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch


MODULE_PATH = Path(__file__).parents[1] / "git-hooks" / "pre-commit.py"
SPEC = importlib.util.spec_from_file_location("pre_commit", MODULE_PATH)
pre_commit = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(pre_commit)


class SortCsvBySlugTests(unittest.TestCase):
def sort_bytes(self, source: bytes) -> bytes:
with tempfile.TemporaryDirectory() as directory:
csv_file = Path(directory) / "records.csv"
csv_file.write_bytes(source)
with patch.object(pre_commit, "iter_csv", return_value=[csv_file]), \
patch.object(pre_commit.subprocess, "run"):
pre_commit.sort_csv_by_slug(Path(directory))
return csv_file.read_bytes()

def test_preserves_multiline_quoted_record(self):
source = (
b"slug,notes\n"
b"zulu,\"line one\ncontinuation\"\n"
b"alpha,single\n"
)
self.assertEqual(
self.sort_bytes(source),
b"slug,notes\nalpha,single\nzulu,\"line one\ncontinuation\"\n",
)

def test_parses_quoted_comma_before_slug(self):
source = b"notes,slug\n\"has,comma\",zulu\nplain,alpha\n"
self.assertEqual(
self.sort_bytes(source),
b"notes,slug\nplain,alpha\n\"has,comma\",zulu\n",
)

def test_keeps_duplicate_slug_order_and_crlf(self):
source = b"slug,value\r\nz,one\r\na,two\r\na,three\r\n"
self.assertEqual(
self.sort_bytes(source),
b"slug,value\r\na,two\r\na,three\r\nz,one\r\n",
)

def test_keeps_missing_final_newline(self):
source = b"slug,value\nz,one\na,two"
self.assertEqual(
self.sort_bytes(source), b"slug,value\na,two\nz,one\n"
)


if __name__ == "__main__":
unittest.main()