Skip to content

Commit 6ca4733

Browse files
Technologicatclaude
andcommitted
add ruff linter alongside flake8, migrate CI
Add ruff with E/W/F/SIM rules to pyproject.toml, replace flake8 in CI workflow. Keep flake8 + flake8rc for Emacs. Per-site noqas for macro-injected names and continuation short-circuit tests. Fix: E714 not-is-test, E721 type comparisons, SIM201 negated equality, SIM110 loop-to-all/any, unused sys import. Noqa SIM401 on custom .get() implementations, E741 on Lisp-conventional `l` parameter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 37be782 commit 6ca4733

19 files changed

Lines changed: 83 additions & 66 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,14 @@ jobs:
2121
- name: Set up Python
2222
uses: actions/setup-python@v6
2323
with:
24-
# Use latest Python so flake8 can parse all syntax (e.g. except* requires 3.11+)
2524
python-version: "3.14"
26-
- name: Install flake8
25+
- name: Install ruff
2726
run: |
2827
python -m pip install --upgrade pip
29-
pip install flake8
30-
- name: Lint with flake8
28+
pip install ruff
29+
- name: Lint with ruff
3130
run: |
32-
# stop the build if there are Python syntax errors or undefined names
33-
flake8 . --config=flake8rc --count --select=E9,F63,F7,F82 --show-source --statistics
34-
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
35-
flake8 . --config=flake8rc --count --exit-zero --max-complexity=100 --max-line-length=127 --statistics
31+
ruff check .
3632
3733
test:
3834
needs: lint

CLAUDE.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,11 @@ Each test module exports a `runtests()` function. Tests are grouped with `testse
6666
## Linting
6767

6868
```bash
69-
# As in CI — hard errors (syntax errors, undefined names)
70-
flake8 . --config=flake8rc --select=E9,F63,F7,F82 --show-source
71-
72-
# Soft warnings
73-
flake8 . --config=flake8rc --exit-zero --max-line-length=127
69+
ruff check <changed .py files> # primary linter (config in pyproject.toml)
7470
```
7571

72+
Legacy `flake8rc` also present (used by Emacs flycheck, not by CI or CC).
73+
7674
## Code structure and conventions
7775

7876
- **Regular code** in `unpythonic/`, **macros** in `unpythonic/syntax/`, **REPL networking** in `unpythonic/net/`, **dialects** in `unpythonic/dialects/`.

pyproject.toml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Repository = "https://github.com/Technologicat/unpythonic"
4949

5050
[dependency-groups]
5151
dev = [
52+
"ruff>=0.14.0",
5253
"flake8",
5354
"autopep8",
5455
"importmagic",
@@ -81,5 +82,45 @@ excludes = ["**/tests", "**/__pycache__"]
8182

8283
# most python tools at this point, including mypy, have support for sourcing configuration from pyproject.toml
8384
# making the setup.cfg file unnecessary
85+
[tool.ruff]
86+
line-length = 130
87+
target-version = "py310"
88+
exclude = [
89+
".git",
90+
"__pycache__",
91+
"build",
92+
"dist",
93+
".venv",
94+
"unpythonic/syntax/tests/test_scopeanalyzer_3_11.py", # except* syntax requires 3.11+
95+
]
96+
97+
[tool.ruff.lint]
98+
select = ["E", "W", "F", "SIM"]
99+
ignore = [
100+
# pycodestyle
101+
"E203", # whitespace before ':' — needed for slice alignment
102+
"E265", # block comment should start with '# ' — commented-out code, markers
103+
"E301", # expected 1 blank line — blank lines are semantic paragraph breaks
104+
"E302", # expected 2 blank lines before def — same
105+
"E305", # expected 2 blank lines after end — same
106+
"E306", # expected blank line before nested def — same
107+
"E402", # module level import not at top — conditional/deferred imports
108+
"E501", # line too long — advisory, not enforced
109+
"E731", # lambda assignment — closures are idiomatic in this codebase
110+
# flake8-simplify
111+
"SIM102", # collapsible if — nested ifs often represent distinct semantic guards
112+
"SIM105", # contextlib.suppress — try/except/pass is more flexible and explicit
113+
"SIM108", # ternary instead of if/else — often less readable, no real gain
114+
"SIM114", # combine if branches — match-casing style; autofix would damage semantics
115+
"SIM117", # combine with statements — nesting shows parent/child; also mcpyrate AST differences
116+
"SIM118", # in-dict-keys — explicit .keys() marks the variable as a dictlike
117+
"SIM300", # yoda conditions — natural reading order preferred
118+
"SIM910", # dict.get with None default — explicit None documents programmer intent
119+
"SIM103", # return condition directly — multi-guard patterns; autofix breaks visual consistency
120+
]
121+
122+
[tool.ruff.lint.per-file-ignores]
123+
"__init__.py" = ["F401", "F403"] # re-exports via star-import
124+
84125
[tool.mypy]
85126
show_error_codes = true

unpythonic/collections.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -571,10 +571,7 @@ def __eq__(self, other):
571571
return True
572572
if len(self) != len(other):
573573
return False
574-
for v1, v2 in zip(self, other):
575-
if v1 != v2:
576-
return False
577-
return True
574+
return all(v1 == v2 for v1, v2 in zip(self, other))
578575

579576
class roview(SequenceView, _StrReprEqMixin):
580577
"""Read-only live view into a sequence.

unpythonic/dialects/listhell.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
class Listhell(Dialect):
1717
def transform_ast(self, tree): # tree is an ast.Module
18-
with q as template:
18+
with q as template: # noqa: F823 -- `q` is a macro-injected name
1919
__lang__ = "Listhell" # noqa: F841, just provide it to user code.
2020
from unpythonic.syntax import macros, prefix, q, u, kw, autocurry # noqa: F401, F811
2121
# Auxiliary syntax elements for the macros

unpythonic/dynassign.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ def keys(self):
245245
def values(self):
246246
return self.asdict().values()
247247
def get(self, k, default=None):
248-
return self[k] if k in self else default
248+
return self[k] if k in self else default # noqa: SIM401 -- this IS the .get() implementation
249249
def __eq__(self, other): # dyn is a singleton, but its contents can be compared to another mapping.
250250
return other == self.asdict()
251251

unpythonic/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def keys(self):
122122
def values(self):
123123
return self._env.values()
124124
def get(self, k, default=None):
125-
return self[k] if k in self else default
125+
return self[k] if k in self else default # noqa: SIM401 -- this IS the .get() implementation
126126
def __eq__(self, other):
127127
return other == self._env
128128

unpythonic/funutil.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ def values(self):
360360
return self.kwrets.values()
361361
def get(self, k, default=None):
362362
"""Dict-like `get` for the named part."""
363-
return self[k] if k in self else default
363+
return self[k] if k in self else default # noqa: SIM401 -- this IS the .get() implementation
364364

365365
# comparison
366366
def __eq__(self, other):

unpythonic/it.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,4 @@ def allsame(iterable):
933933
x0 = next(it)
934934
except StopIteration:
935935
return True # like all(()) is True
936-
for x in it:
937-
if x != x0:
938-
return False
939-
return True
936+
return all(x == x0 for x in it)

unpythonic/llist.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,10 +255,7 @@ def __eq__(self, other):
255255
try: # duck test linked lists
256256
ia, ib = (LinkedListIterator(x) for x in (self, other))
257257
fill = object() # gensym("fill"), but object() is much faster, and we don't need a label, or pickle support.
258-
for a, b in zip_longest(ia, ib, fillvalue=fill):
259-
if a != b:
260-
return False
261-
return True
258+
return all(a == b for a, b in zip_longest(ia, ib, fillvalue=fill))
262259
except TypeError:
263260
return self.car == other.car and self.cdr == other.cdr
264261
return False
@@ -374,7 +371,7 @@ def lappend_two(l1, l2):
374371
return foldr(cons, l2, l1)
375372
return foldr(lappend_two, nil, ls)
376373

377-
def member(x, l):
374+
def member(x, l): # noqa: E741 -- standard Lisp name for a linked list
378375
"""Walk linked list l and check if item x is in it.
379376
380377
Returns:

0 commit comments

Comments
 (0)