-
Notifications
You must be signed in to change notification settings - Fork 0
Python
lanekeep checks .py and .pyi files. Rules are TypeScript programs matching a Python syntax
tree — see Home for why.
pip install lanekeep
lanekeep checkFour platform wheels, no launcher — a wheel names its platform in its own filename and pip picks by that tag. Nothing is pulled in as a dependency, and Python is not required to run it: the wheel carries a static binary.
Works the same with uv, poetry or pipx:
uv add --dev lanekeep
poetry add --group dev lanekeep
pipx install lanekeepThe Linux wheels are built against glibc 2.17, so they install and run on anything from RHEL 7 onwards.
lanekeep init in a directory with a pyproject.toml writes this for you:
{
"$schema": "https://raw.githubusercontent.com/fmsouza/lanekeep/main/schema/lanekeep.schema.json",
"include": ["**/*.py"],
"exclude": ["**/test_*.py"],
"rules": [
"lanekeep/no-broad-except",
"./lanekeep/rules/no-print.ts"
]
}Virtual environments and build output are worth excluding explicitly:
{ "exclude": ["**/test_*.py", ".venv/**", "**/migrations/**", "build/**"] }.pyi stubs are the same language with the bodies removed, so a rule about imports or
signatures applies there unchanged.
# bad
try:
parse(raw)
except Exception:
return None
# good
try:
parse(raw)
except ValueError:
return NoneA bare except: also catches KeyboardInterrupt and SystemExit, so it swallows Ctrl-C.
except Exception: still catches every bug in the block — a typo'd attribute, a None where
an object was expected — and reports whatever the handler decided the failure was.
A project defining or importing its own Exception is not reported: bindingKind says the
name is local, so it is not the builtin.
# bad
def add(item, into=[]):
into.append(item)
# good
def add(item, into=None):
into = [] if into is None else intoThe default is evaluated once, at definition, so every call without the argument shares one
list. A project defining its own list, dict or set is not reported.
import { defineRule } from 'lanekeep'
export default defineRule({
id: 'local/no-requests-in-handlers',
language: 'python', // ← required; the default is TypeScript
severity: 'error',
card: {
message: 'requests called directly from a handler',
remediation: 'go through the client in services/http.py, which carries timeouts and retries',
examples: {
bad: 'requests.get(url)',
good: 'http_client.get(url)',
},
},
gates: { fileContains: ['requests'] },
query: `
(call
function: (attribute
object: (identifier) @pkg
attribute: (identifier) @method)) @call
`,
check(ctx, m) {
if (ctx.text(m.pkg) !== 'requests') return
// A local name that happens to read `requests` is not the library.
if (ctx.bindingKind(m.pkg) !== 'import') return
ctx.report(m.call)
},
})language: 'python' is not optional. The default is ['typescript', 'tsx'], and a rule
does not run on a file whose language it does not name — so omitting it means the rule silently
never fires.
ctx.bindingKind understands:
| Form | Kind |
|---|---|
import os, from a import b
|
import |
x = 1, x += 1, x := 1
|
assignment |
def f(), class C
|
function, class
|
| parameters | param |
for x in xs |
loop |
with open(p) as f |
context-manager |
except E as e |
catch-param |
[x for x in xs] |
comprehension |
Two Python-specific behaviors worth knowing:
There is no block scope. A name bound anywhere in a function body is bound throughout it, so resolution walks a scope's whole body rather than its direct children.
A class body is opaque to functions nested inside it. def m(self): return LIMIT does not
see a class-level LIMIT, and resolution reflects that rather than reporting a binding Python
would not find.
Comprehension targets are scoped to the comprehension. The x in [x for x in xs] does
not leak, so an outer x is still what an outer use resolves to.
Import shapes. import a.b.c binds a — the first segment. from a.b import c as d binds
d, and resolves to module a.b, name c. ctx.resolvesToImport(node, { module, name })
handles the aliasing for you.
Languages
In the repository