#!/usr/bin/env python3 """ Check each guide URL in guides.csv for WCAG 2.1 AA accessibility issues using the pa11y CLI and report results. The script: 1. Reads guides.csv in the current directory. 2. Runs `pa11y` for each URL with the WCAG2AA standard, requesting JSON. 3. Collects the issues for every guide. 4. Removes any issue codes that appear in *every* guide. 5. Prints the filtered results to the console and writes them to output.txt. Prerequisites: npm install -g pa11y Usage: python check_wcag.py """ from __future__ import annotations import csv import json import subprocess import sys from pathlib import Path from typing import Dict, List, Set GUIDES_CSV = Path(__file__).with_name("guides.csv") OUTPUT_FILE = Path("output.txt") def run_pa11y(url: str) -> List[dict]: """ Run pa11y on a URL and return the parsed JSON list of issues. Returns an empty list if pa11y fails. """ try: result = subprocess.run( ["pa11y", url, "--standard", "WCAG2AA", "--reporter", "json"], capture_output=True, text=True, check=False, ) if result.returncode != 0: print(f"[WARN] pa11y returned code {result.returncode} for {url}", file=sys.stderr) return [] return json.loads(result.stdout) except FileNotFoundError: print("Error: pa11y not found. Install it with `npm install -g pa11y`.", file=sys.stderr) sys.exit(1) except json.JSONDecodeError: print(f"[WARN] Could not parse pa11y JSON output for {url}", file=sys.stderr) return [] def read_guides(csv_path: Path) -> List[Dict[str, str]]: """Load all guides from the CSV into a list of dicts.""" with csv_path.open(newline="", encoding="utf-8-sig") as fh: reader = csv.DictReader(fh) return list(reader) def compute_common_codes(issues_by_guide: Dict[str, List[dict]]) -> Set[str]: """Return the set of issue codes that appear in every guide.""" all_codes_iter = ( {issue["code"] for issue in issues} for issues in issues_by_guide.values() ) common_codes: Set[str] | None = None for codes in all_codes_iter: if common_codes is None: common_codes = codes else: common_codes &= codes return common_codes or set() def format_issue(issue: dict) -> str: """Return a human-readable one-line representation of a pa11y issue.""" selector = issue.get("selector", "").strip() or "N/A" return f"[{issue['type']}] {issue['code']}: {issue['message']} (selector: {selector})" def main() -> None: guides = read_guides(GUIDES_CSV) issues_by_guide: Dict[str, List[dict]] = {} print("Auditing guides with pa11y…\n") for guide in guides: name = guide["Name"] url = guide["URL"] print(f"• {name} — {url}") issues = run_pa11y(url) issues_by_guide[name] = issues common_codes = compute_common_codes(issues_by_guide) lines_out: List[str] = [] lines_out.append("\nAccessibility report (WCAG 2.1 AA)") lines_out.append("=" * 40) for name, issues in issues_by_guide.items(): filtered = [i for i in issues if i["code"] not in common_codes] lines_out.append(f"\nGuide: {name}") if not filtered: lines_out.append(" ✔ No unique issues found.") continue for issue in filtered: lines_out.append(f" • {format_issue(issue)}") report_text = "\n".join(lines_out) print("\n" + report_text) OUTPUT_FILE.write_text(report_text, encoding="utf-8") print(f"\nReport written to {OUTPUT_FILE.resolve()}") if __name__ == "__main__": main()