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

on:
push:
branches: [main]
pull_request:

# Lint only — ruff is a standalone binary, so this job skips the project
# install (PyPy 3.10 + SDL2 headers + Cython) entirely.
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: ruff check
run: uvx ruff@0.16.2 check --output-format=github .
- name: ruff format --check
run: uvx ruff@0.16.2 format --check .
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build clean run install test benchmark docs
.PHONY: build clean run install test benchmark docs lint format

PY ?= --python pypy@3.10

Expand Down Expand Up @@ -38,3 +38,13 @@ profile: build
tests: build
@echo "Running tests..."
uv run ${PY} pytest

lint:
@echo "Linting PySNES..."
uv run ${PY} ruff check .
uv run ${PY} ruff format --check .

format:
@echo "Formatting PySNES..."
uv run ${PY} ruff check --fix .
uv run ${PY} ruff format .
41 changes: 41 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,44 @@ dependencies = [

[project.scripts]
pysnes = "pysnes.pysnes:main"

[dependency-groups]
dev = [
# Pinned exactly: a floating formatter version makes CI and local
# checkouts disagree about what "formatted" means.
"ruff==0.16.2",
]

[tool.ruff]
line-length = 80
target-version = "py310"
# Third-party checkouts (Mesen2, the SingleStepTests suites). Markdown is
# excluded too: ruff formats Python blocks inside .md, which would rewrite
# the hand-tuned examples in AGENTS.md.
extend-exclude = ["submodules", "*.md"]

[tool.ruff.lint]
# E/W (pycodestyle), F (pyflakes), I (isort), UP (pyupgrade), B (bugbear),
# SIM (flake8-simplify), C4 (comprehensions), PT (pytest-style),
# PIE/RET/G (misc + return + logging), ISC/LOG (ratchets, today clean).
# TID is deliberately out: pysnes is a single package and relative imports
# between its modules are the house style.
select = [
"E", "W", "F", "I", "UP", "B",
"SIM", "C4", "PT", "PIE", "RET", "G", "ISC", "LOG",
]

# SIM108 pushes if/else blocks into ternaries, but a ternary long enough to
# exceed 80 columns gets wrapped by the formatter into a multi-line ternary,
# which is harder to read than the if/else it replaced. House rule wins.
ignore = ["SIM108"]

[tool.ruff.lint.per-file-ignores]
# Package __init__ files exist to re-export their module's public names.
"**/__init__.py" = ["F401", "F403"]
# Hand-aligned hardware tables (the 512-entry Gaussian interpolation table,
# the 256-entry SPC700 opcode table). They are kept in their reference
# layout inside `# fmt: off` blocks, which stops the formatter reflowing
# them but does not exempt them from the line-length rule.
"pysnes/apu/dsp.py" = ["E501"]
"pysnes/apu/spc700/instructions_spc700.py" = ["E501"]
12 changes: 7 additions & 5 deletions pysnes/_ss_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import os
import pickle


CACHE_DIR = ".pytest_cache"


Expand All @@ -31,7 +30,9 @@ def _dir_fingerprint(tests_path):


def _cache_key(suite_name, tests_path, filter_args):
blob = repr((suite_name, filter_args, _dir_fingerprint(tests_path))).encode()
blob = repr(
(suite_name, filter_args, _dir_fingerprint(tests_path))
).encode()
return hashlib.sha1(blob).hexdigest()[:16]


Expand Down Expand Up @@ -62,10 +63,11 @@ def _atomic_write(path, key, params, ids):


def get_or_build(suite_name, tests_path, filter_args, parse_fn):
"""Return (params, ids). params is [(file_path, index), ...]; ids are pytest IDs.
"""Return (params, ids), where params is [(file_path, index), ...] and
ids are pytest IDs.

On cache hit: read pickle, return.
On cache miss: acquire exclusive flock, re-check, call parse_fn(), write, return.
On cache hit: read pickle, return. On cache miss: acquire exclusive flock,
re-check, call parse_fn(), write, return.
"""
os.makedirs(CACHE_DIR, exist_ok=True)
key = _cache_key(suite_name, tests_path, filter_args)
Expand Down
Loading
Loading