Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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