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
27 changes: 27 additions & 0 deletions .github/workflows/test-python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Python tests

on:
pull_request:
branches:
- master

permissions: {}

jobs:
pytest:
permissions:
contents: read
runs-on: ubuntu-24.04
env:
CI: true
steps:
- name: Setup Action
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '3.12'
- name: Install dev dependencies
run: python -m pip install --user -r requirements-dev.txt
- name: Run pytest
run: python -m pytest
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ venv
.claude/settings.local.json
.claude/settings.json
.claude/worktrees/
# Python bytecode
__pycache__/
*.pyc
10 changes: 10 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short --strict-markers
filterwarnings =
error
# The repo's scripts predate py3.10; tolerate missing annotations.
ignore::DeprecationWarning
6 changes: 6 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Test-only dependencies. Install with:
# pip install -r requirements-dev.txt
#
# Kept separate from requirements.txt so the runtime image for mkdocs/feedgen
# does not pull in pytest.
pytest>=7.0
170 changes: 111 additions & 59 deletions scripts/Update_CheatSheets_Index.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,69 @@
and is named "Index.md".
"""
import os
import sys
from collections import OrderedDict
from typing import Dict, Iterable, List

# Define utility functions
def extract_languages_snippet_provided(cheatsheet):
languages = []
markers = ["javascript", "java", "csharp", "c", "cpp", "html", "xml", "python",
"ruby", "php", "json", "sql", "bash", "shell", "coldfusion", "perl",
"vbnet"]
with open("../cheatsheets/" + cheatsheet, encoding="utf8") as cs_file:
cs_content = cs_file.read().lower().replace(" ","")
for marker in markers:
_LANGUAGE_MARKERS = [
"javascript", "java", "csharp", "c", "cpp", "html", "xml", "python",
"ruby", "php", "json", "sql", "bash", "shell", "coldfusion", "perl",
"vbnet",
]


def extract_languages_snippet_provided(
cheatsheet: str,
cheatsheets_dir: str = "../cheatsheets",
) -> List[str]:
"""Detect the languages of code snippets in the given cheatsheet.

Looks for fenced code blocks (```` ```language ````) whose language
tag is in the recognized list. The file is read in lowercase and with
spaces stripped so detection is case- and spacing-insensitive.

Args:
cheatsheet: Filename of the cheatsheet within ``cheatsheets_dir``.
cheatsheets_dir: Directory containing the cheatsheet file.

Returns:
A list of recognized language names with their first letter
capitalized, in the order they were detected.
"""
languages: List[str] = []
with open(
os.path.join(cheatsheets_dir, cheatsheet), encoding="utf8"
) as cs_file:
cs_content = cs_file.read().lower().replace(" ", "")
for marker in _LANGUAGE_MARKERS:
if "```" + marker + "\n" in cs_content:
languages.append(marker.capitalize())
return languages


def group_by_letter(cheatsheets: Iterable[str]) -> "OrderedDict[str, List[str]]":
"""Group cheatsheet filenames by their first letter (uppercased).

Filenames are grouped by the uppercase form of their first character.
The result is an :class:`OrderedDict` sorted by letter, preserving
the input order of filenames within each letter group.
"""
index: Dict[str, List[str]] = {}
for cheatsheet in cheatsheets:
letter = cheatsheet[0].upper()
index.setdefault(letter, []).append(cheatsheet)
return OrderedDict(sorted(index.items()))


def clean_trailing_whitespace(file_path: str) -> None:
"""Strip trailing whitespace from each line in the file (in place)."""
with open(file_path, "r", encoding="utf-8") as file:
cleaned_lines = [line.rstrip() + "\n" for line in file]
with open(file_path, "w", encoding="utf-8") as file:
file.writelines(cleaned_lines)


# Define templates
cs_md_link_template = "[%s](cheatsheets/%s)"
language_md_link_template = "![%s](assets/Index_%s.svg)"
Expand All @@ -31,59 +79,63 @@ def extract_languages_snippet_provided(cheatsheet):
cs_count_template = "**%s** cheat sheets available."
cs_index_title_template = "# Index Alphabetical\n\n"

# Scan all CS files
index = {}
cs_count = 0
cheatsheets = [f.name for f in os.scandir("../cheatsheets") if f.is_file()]
for cheatsheet in cheatsheets:
letter = cheatsheet[0].upper()
if letter not in index:
index[letter] = [cheatsheet]
else:
index[letter].append(cheatsheet)
cs_count += 1
index = OrderedDict(sorted(index.items()))

# Generate the index file
with open("../Index.md", "w", encoding="utf-8") as index_file:
index_file.write(cs_index_title_template)
index_count = len(index)
index_file.write(cs_count_template % cs_count)
index_file.write("\n\n*Icons beside the cheat sheet name indicate in which language(s) code snippet(s) are provided.*")
index_file.write("\n\n")
# Generate the top menu
for letter in index:
index_file.write(top_menu_template % (letter, letter.lower()))
index_file.write(" ")
index_file.write("\n\n")
# Generate letter sections
j = 0
for letter in index:
cs_count = len(index[letter])
index_file.write(header_template % letter)
i = 0
for cs_file in index[letter]:
cs_name = cs_file.replace("_", " ").replace(".md", "").strip()
index_file.write(cs_md_link_template % (cs_name, cs_file))
languages = extract_languages_snippet_provided(cs_file)
if len(languages) > 0:
index_file.write(" ")
for language in languages:
index_file.write(language_md_link_template % (language, language))

def main(
cheatsheets_dir: str = "../cheatsheets",
output_file: str = "../Index.md",
) -> int:
"""Regenerate the alphabetical index from the cheatsheets directory.

Scans ``cheatsheets_dir`` for files, groups them by first letter,
detects code-snippet languages, and writes the index to
``output_file``. Returns 0 on success.
"""
cheatsheets = [f.name for f in os.scandir(cheatsheets_dir) if f.is_file()]
index = group_by_letter(cheatsheets)
cs_count = len(cheatsheets)

with open(output_file, "w", encoding="utf-8") as index_file:
index_file.write(cs_index_title_template)
index_file.write(cs_count_template % cs_count)
index_file.write(
"\n\n*Icons beside the cheat sheet name indicate in which "
"language(s) code snippet(s) are provided.*"
)
index_file.write("\n\n")
# Generate the top menu
for letter in index:
index_file.write(top_menu_template % (letter, letter.lower()))
index_file.write(" ")
index_file.write("\n\n")
# Generate letter sections
index_count = len(index)
for j, letter in enumerate(index):
group = index[letter]
group_count = len(group)
index_file.write(header_template % letter)
for i, cs_file in enumerate(group):
cs_name = cs_file.replace("_", " ").replace(".md", "").strip()
index_file.write(cs_md_link_template % (cs_name, cs_file))
languages = extract_languages_snippet_provided(
cs_file, cheatsheets_dir=cheatsheets_dir
)
if languages:
index_file.write(" ")
i += 1
index_file.write("\n")
if i != cs_count:
for language in languages:
index_file.write(
language_md_link_template % (language, language)
)
index_file.write(" ")
index_file.write("\n")
if i + 1 != group_count:
index_file.write("\n")
if j + 1 != index_count:
index_file.write("\n")
j += 1
if j != index_count:
index_file.write("\n")

# Clean trailing whitespaces
with open("../Index.md", "r", encoding="utf-8") as file:
cleaned_lines = [line.rstrip() + "\n" for line in file]
clean_trailing_whitespace(output_file)
print("Index updated.")
return 0

with open("../Index.md", "w", encoding="utf-8") as file:
file.writelines(cleaned_lines)

print("Index updated.")
if __name__ == "__main__":
sys.exit(main())
Empty file added tests/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Shared pytest fixtures for the scripts/ test suite."""
from __future__ import annotations

from pathlib import Path

import pytest


@pytest.fixture
def cheatsheets_dir(tmp_path: Path) -> Path:
"""Return a fresh, empty cheatsheets/ directory inside a temp path.

Tests that need sample cheatsheet files should write them into this
directory before invoking the script under test.
"""
d = tmp_path / "cheatsheets"
d.mkdir()
return d


@pytest.fixture
def write_cheatsheet(cheatsheets_dir: Path):
"""Return a callable that writes a cheatsheet file with optional content.

Usage:
write_cheatsheet("Foo.md", "# Foo\\n\\n```python\\nprint('x')\\n```")
"""
def _write(name: str, content: str = "") -> Path:
path = cheatsheets_dir / name
path.write_text(content, encoding="utf-8")
return path
return _write
Loading