Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.12
3.13
3 changes: 1 addition & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -493,8 +493,7 @@ python-sync: _ensure-uv
uv sync --group dev

python-typecheck: python-sync
uv run ty check scripts/
uv run mypy scripts/archive_changelog.py scripts/bench_compare.py scripts/check_docs_version_sync.py scripts/check_semgrep_fixtures.py scripts/criterion_dim_plot.py scripts/tag_release.py scripts/postprocess_changelog.py scripts/subprocess_utils.py
uv run ty check scripts/ --error all
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Repository-owned Semgrep rules for project-specific diagnostics.
semgrep: _ensure-uv
Expand Down
20 changes: 2 additions & 18 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "la-stack-scripts"
version = "0.4.2"
description = "Python utility scripts for the la-stack Rust library"
readme = "README.md"
requires-python = ">=3.12"
requires-python = ">=3.13"
license = { text = "BSD-3-Clause" }
authors = [
{ name = "Adam Getchell", email = "adam@adamgetchell.org" },
Expand All @@ -20,7 +20,6 @@ classifiers = [
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Mathematics",
"Topic :: System :: Benchmarking",
Expand Down Expand Up @@ -50,7 +49,7 @@ py-modules = [ "archive_changelog", "bench_compare", "check_docs_version_sync",

[tool.ruff]
line-length = 160
target-version = "py312"
target-version = "py313"
src = [ "scripts" ]

[tool.ruff.lint]
Expand Down Expand Up @@ -122,27 +121,12 @@ python_files = [ "test_*.py", "*_test.py" ]
python_classes = [ "Test*" ]
python_functions = [ "test_*" ]

[tool.mypy]
python_version = "3.12"
mypy_path = "scripts"
warn_unused_configs = true
no_implicit_optional = true
strict_equality = true
warn_redundant_casts = true
warn_no_return = true
show_error_codes = true
show_column_numbers = true
pretty = true
allow_untyped_calls = true
allow_incomplete_defs = true

[tool.uv]
package = true

[dependency-groups]
dev = [
"actionlint-py==1.7.12.24",
"mypy>=1.19.0",
"pytest==9.0.3",
"ruff>=0.15.14",
"semgrep==1.164.0",
Expand Down
30 changes: 23 additions & 7 deletions scripts/check_docs_version_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import tomllib
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from typing import TypeGuard

SKIP_DIRS = frozenset(
{
Expand All @@ -23,15 +23,30 @@
)


@dataclass(frozen=True)
type ParsedObject = dict[str, object]


def _is_parsed_object(value: object) -> TypeGuard[ParsedObject]:
"""Return true when a parsed TOML value is an object with string keys."""
return isinstance(value, dict) and all(isinstance(key, str) for key in value)


def _require_parsed_object(value: object, context: str) -> ParsedObject:
if not _is_parsed_object(value):
msg = f"{context} is not a TOML object"
raise TypeError(msg)
return value


@dataclass(frozen=True, slots=True)
class PackageInfo:
"""Cargo package identity used in documented dependency snippets."""

name: str
version: str


@dataclass(frozen=True)
@dataclass(frozen=True, slots=True)
class DependencySnippet:
"""A documented dependency version snippet for the current package."""

Expand All @@ -41,7 +56,7 @@ class DependencySnippet:
text: str


@dataclass(frozen=True)
@dataclass(frozen=True, slots=True)
class VersionMismatch:
"""A dependency snippet whose version does not match Cargo.toml."""

Expand All @@ -50,9 +65,10 @@ class VersionMismatch:


def _read_cargo_package_info(cargo_toml: Path) -> PackageInfo:
data: dict[str, Any] = tomllib.loads(cargo_toml.read_text(encoding="utf-8"))
package = data.get("package")
if not isinstance(package, dict):
data: object = tomllib.loads(cargo_toml.read_text(encoding="utf-8"))
cargo = _require_parsed_object(data, str(cargo_toml))
package = cargo.get("package")
if not _is_parsed_object(package):
msg = f"{cargo_toml} is missing a [package] table"
raise TypeError(msg)

Expand Down
54 changes: 40 additions & 14 deletions scripts/check_semgrep_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,27 @@
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from typing import TypeGuard

RULE_ANNOTATION = re.compile(r"\bruleid:\s*([A-Za-z0-9_.-]+(?:\s*,\s*[A-Za-z0-9_.-]+)*)")


type ParsedObject = dict[str, object]


@dataclass(frozen=True, slots=True)
class SemgrepResults:
"""Validated subset of Semgrep JSON needed by fixture checks."""

results: tuple[ParsedObject, ...]


def _is_parsed_object(value: object) -> TypeGuard[ParsedObject]:
return isinstance(value, dict) and all(isinstance(key, str) for key in value)


def _path_argument(argv: list[str]) -> Path | None:
if len(argv) <= 1:
print("Missing required path argument: sys.argv[1]", file=sys.stderr)
Expand All @@ -31,24 +46,40 @@ def _path_argument(argv: list[str]) -> Path | None:
return path


def _semgrep_results() -> dict[str, Any] | None:
def _semgrep_results() -> SemgrepResults | None:
semgrep_json = os.environ.get("SEMGREP_JSON")
if semgrep_json is None:
print("Missing required SEMGREP_JSON environment variable", file=sys.stderr)
return None
try:
data = json.loads(semgrep_json)
data: object = json.loads(semgrep_json)
except json.JSONDecodeError as error:
print(f"Invalid JSON in SEMGREP_JSON: {error}", file=sys.stderr)
return None

if not isinstance(data, dict):
if not _is_parsed_object(data):
print("Invalid SEMGREP_JSON shape: expected a JSON object", file=sys.stderr)
return None
if not isinstance(data.get("results"), list):
results = data.get("results")
if not isinstance(results, list):
print("Invalid SEMGREP_JSON shape: expected 'results' to be a list", file=sys.stderr)
return None
return data

parsed_results: list[ParsedObject] = []
malformed_results: list[str] = []
for index, result in enumerate(results):
if _is_parsed_object(result):
parsed_results.append(result)
else:
malformed_results.append(f"result {index} is not an object")

if malformed_results:
print("Invalid SEMGREP_JSON shape:", file=sys.stderr)
for malformed in malformed_results:
print(f" {malformed}", file=sys.stderr)
return None

return SemgrepResults(results=tuple(parsed_results))


def main() -> int:
Expand All @@ -61,18 +92,13 @@ def main() -> int:
for match in RULE_ANNOTATION.finditer(line):
expected.update(rule_id.strip() for rule_id in match.group(1).split(",") if rule_id.strip())

data = _semgrep_results()
if data is None:
semgrep = _semgrep_results()
if semgrep is None:
return 1

results = data["results"]
actual: collections.Counter[str] = collections.Counter()
malformed_results: list[str] = []
for index, result in enumerate(results):
if not isinstance(result, dict):
malformed_results.append(f"result {index} is not an object")
continue

for index, result in enumerate(semgrep.results):
check_id = result.get("check_id")
if not isinstance(check_id, str):
malformed_results.append(f"result {index} is missing string field 'check_id'")
Expand Down
Loading
Loading