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
2 changes: 2 additions & 0 deletions .rules/python-00.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,12 @@ def login_user(username: str, password: str) -> bool:
"""Return True if the user is authenticated."""
...


# login_flow_test.py
def test_login_success():
assert login_user("alice", "correct-password") is True


def test_login_failure():
assert not login_user("alice", "wrong-password")
```
Expand Down
3 changes: 3 additions & 0 deletions .rules/python-context-managers.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Use this for straightforward procedural setup/teardown:
```python
from contextlib import contextmanager


@contextmanager
def managed_file(path: str, mode: str):
f = open(path, mode)
Expand All @@ -31,6 +32,7 @@ def managed_file(path: str, mode: str):
finally:
f.close()


# Usage:
with managed_file("/tmp/data.txt", "w") as f:
f.write("hello")
Expand All @@ -53,6 +55,7 @@ class Resource:
def __exit__(self, exc_type, exc_val, exc_tb):
self.conn.close()


# Usage:
with Resource() as conn:
conn.send("ping")
Expand Down
17 changes: 11 additions & 6 deletions .rules/python-exception-design-raising-handling-and-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vendor leakage.
class PaymentsError(Exception):
"""All payment-layer errors."""


class CardDeclinedError(PaymentsError): # ✅ ends with Error (pep8-naming N818)
def __init__(self, code: str, *, retry_after: int | None = None):
super().__init__(f"Card declined ({code})")
Expand Down Expand Up @@ -50,7 +51,9 @@ types, `ValueError` for bad values, and so on), or domain-specific classes.
try:
token = decode_jwt(payload)
except jwt.InvalidTokenError as exc:
raise AuthenticationError("Invalid session token") from exc # ✅ Ruff Tryceratops rule TRY201
raise AuthenticationError(
"Invalid session token"
) from exc # ✅ Ruff Tryceratops rule TRY201
```

When transforming low‑level failures into domain errors, `raise … from …`
Expand Down Expand Up @@ -115,13 +118,14 @@ duplication and clarifies intent.

```python
import logging

logger = logging.getLogger(__name__)

# ❌ flake8-logging issues
logging.warning(f"failed for {user_id}") # f-string (flake8-logging LOG004/LOG014)
logging.warning("failed for %s" % user_id) # %-formatting (flake8-logging LOG007)
logging.warn("deprecated") # warn() (flake8-logging LOG009)
logging.error("bad root logger") # root logger usage (flake8-logging LOG015)
logging.warning(f"failed for {user_id}") # f-string (flake8-logging LOG004/LOG014)
logging.warning("failed for %s" % user_id) # %-formatting (flake8-logging LOG007)
logging.warn("deprecated") # warn() (flake8-logging LOG009)
logging.error("bad root logger") # root logger usage (flake8-logging LOG015)

# ✅ Correct
logger.warning("Failed for user_id=%s", user_id) # lazy interpolation
Expand Down Expand Up @@ -205,7 +209,7 @@ def charge(amount_pennies: int, card_token: str) -> str:
try:
return gateway.charge(amount_pennies, card_token)
except gateway.Timeout as exc:
raise PaymentsError("Gateway timeout") from exc # ✅ Tryceratops rule TRY201
raise PaymentsError("Gateway timeout") from exc # ✅ Tryceratops rule TRY201
except gateway.CardDeclined as exc:
raise CardDeclinedError(exc.code, retry_after=60) from exc
```
Expand All @@ -229,6 +233,7 @@ def must_have_key(d: dict, key: str) -> None:
msg = f"Missing required key: {key!r}"
raise KeyError(msg)


logger.info("Dispatching order_id=%s to shop_id=%s", order_id, shop_id) # structured
```

Expand Down
6 changes: 3 additions & 3 deletions .rules/python-generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def iter_user_names(users):
if user.active and user.name:
yield user.name.upper()


def get_names(users):
return list(iter_user_names(users))
```
Expand All @@ -50,11 +51,10 @@ def get_names(users):
```python
from itertools import islice


def top_active_emails(users):
emails = (
user.email.lower()
for user in users
if user.active and user.email is not None
user.email.lower() for user in users if user.active and user.email is not None
)
return list(islice(emails, 10))
```
Expand Down
33 changes: 17 additions & 16 deletions .rules/python-pyproject.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Published "extras" that an *end user* opts into to enable an optional feature
of the package, requested with `package[extra]` syntax (for example,
`pandas[excel]`). Reach for this only when the extra dependency powers
user-facing functionality that not everyone needs — never for development
tooling. Add them with `uv add --optional <extra>`:
tooling. Add them with `uv add --optional <extra> <package>`:

```toml
[project.optional-dependencies]
Expand All @@ -122,8 +122,9 @@ Tooling only contributors need: test frameworks, linters, type checkers,
documentation builders, and property or mutation testers. These are
**local-only** — PEP 735 dependency groups are *not* included in published
package metadata (they are not part of the wheel), so they must live here rather
than in `project.optional-dependencies`. Add them with `uv add --dev` (the
`dev` group) or `uv add --group <name>`:
than in `project.optional-dependencies`. Add them with
`uv add --dev <package>` (the `dev` group) or
`uv add --group <name> <package>`:

```toml
[dependency-groups]
Expand All @@ -138,8 +139,9 @@ dev = [
`uv sync` include the `dev` group with no extra flags, so a bare `uv sync`
gives a contributor the full toolchain. Adjust this with:

- `--no-dev` or `--no-default-groups` to exclude development dependencies (for
example, when building a wheel or a production install).
- `--no-dev` to exclude only the `dev` group.
- `--no-default-groups` to disable configured default groups while still
permitting explicit selection of other groups.
- `--group <name>` or `--only-group <name>` to include or isolate a
non-default group.
- `[tool.uv].default-groups` to change which groups sync by default:
Expand Down Expand Up @@ -190,19 +192,19 @@ ______________________________________________________________________
## 5. Declaring a Build System

PEP 517/518 require a `[build-system]` table to tell tools how to build and
install your project. A "modern" convention is to specify `setuptools>=61.0`
install your project. A "modern" convention is to specify `setuptools>=64.0`
(for editable installs without `setup.py`) or a lighter alternative like
`flit_core`. Below is the typical setup using setuptools:

```toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
requires = ["setuptools>=64.0"]
build-backend = "setuptools.build_meta"
```

- **`requires`:** A list of packages needed at build time. For editable installs
in `uv`, you need at least `setuptools>=61.0` and `wheel`. (Python
Packaging[^4], Astral Docs[^7])
in `uv`, `setuptools>=64.0` supplies PEP 660 editable-install support.
(Python Packaging[^4], Astral Docs[^7])
- **`build-backend`:** The entry point for your build backend.
`setuptools.build_meta` is the PEP 517-compliant backend for setuptools.
(Python Packaging[^4], Astral Docs[^7])
Expand Down Expand Up @@ -282,7 +284,7 @@ docs = [
mycli = "my_project.cli:main"

[build-system]
requires = ["setuptools>=61.0", "wheel"]
requires = ["setuptools>=64.0"]
build-backend = "setuptools.build_meta"

[tool.uv]
Expand Down Expand Up @@ -318,9 +320,8 @@ package = true

4. **Build System:**

- `setuptools>=61.0` plus `wheel` ensures both legacy and editable installs
work. ✱ Newer versions of setuptools support PEP 660 editable installs
without a `setup.py` stub. (Python Packaging[^4], Astral Docs[^7])
- `setuptools>=64.0` supplies PEP 660 editable-install support without a
`setup.py` stub. (Python Packaging[^4], Astral Docs[^7])
- `build-backend = "setuptools.build_meta"` tells `uv` how to compile your
package. (Python Packaging[^4], Astral Docs[^7])

Expand Down Expand Up @@ -372,9 +373,9 @@ A "modern" `pyproject.toml` for an Astral `uv` project should:
`[dependency-groups]` (the `dev` group installs by default).
- Define any CLI or GUI entry points under `[project.scripts]` or
`[project.gui-scripts]`.
- Declare a PEP 517 `[build-system]` (e.g. `setuptools>=61.0`, `wheel`,
`setuptools.build_meta`) to support editable installs, or omit it and rely on
`tool.uv.package = true`.
- Declare a PEP 517 `[build-system]` (e.g. `setuptools>=64.0` and
`setuptools.build_meta`) to support editable installs, or omit it and rely
on `tool.uv.package = true`.
- Include a `[tool.uv]` section, at minimum `package = true` if you want `uv` to
build and install your own package.

Expand Down
32 changes: 23 additions & 9 deletions .rules/python-return.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# flake8-return Style Guide (Python 3.13)

The `flake8-return` rules ensure consistent and explicit return behaviour while
keeping functions clear in intent and free from unnecessary control flow.
Follow these rules:
The `flake8-return` rules ensure consistent and explicit return behaviour. They
keep functions clear in intent and free from unnecessary control flow. Follow
these rules:

## R501 — Avoid Explicit `return None` if It's the Only Return

Expand All @@ -11,6 +11,7 @@ Follow these rules:
def func():
return None


# GOOD:
def func():
return
Expand All @@ -30,6 +31,7 @@ def func(x):
return x
# implicitly returns None (bad)


# GOOD:
def func(x):
if x > 0:
Expand All @@ -41,14 +43,15 @@ Ensure all branches explicitly return a value if any branch does.

______________________________________________________________________

## R503 — Add an Explicit Return at the End
## R503 — Add an Explicit Return at the End When a Function May Return a Value

```python
# BAD:
def func(x):
if x > 0:
return x
# no return (bad)
# missing terminal return (bad)


# GOOD:
def func(x):
Expand All @@ -57,7 +60,17 @@ def func(x):
return -1
```

Don't rely on implicit `None`—always return something at the end.
Don't rely on implicit `None` if the function may return a value elsewhere—always
return something at the end.

Functions whose only possible result is `None` do not need a final bare `return`:

```python
# GOOD:
def func():
do_something()
# implicit None is fine here
```

______________________________________________________________________

Expand All @@ -69,6 +82,7 @@ def func():
result = compute()
return result


# GOOD:
def func():
return compute()
Expand Down Expand Up @@ -114,9 +128,9 @@ for x in xs:
log()
```

These rules apply to regular, and `async def` functions alike.
These rules apply to regular and `async def` functions alike.

______________________________________________________________________

Use the `flake8-return` rules to enforce predictable return logic. Doing so
enhances readability and correctness.
Use the `flake8-return` rules to enforce predictable and clean return logic,
enhancing readability and correctness.
11 changes: 9 additions & 2 deletions .rules/python-typing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ with integers or strings is required (e.g. for database or JSON serialization).
```python
import enum


class Status(enum.Enum):
PENDING = enum.auto()
COMPLETE = enum.auto()


class ErrorCode(enum.IntEnum):
OK = 0
NOT_FOUND = 404


class Role(enum.StrEnum):
ADMIN = enum.auto()
GUEST = enum.auto()
Expand Down Expand Up @@ -68,6 +71,7 @@ returns the same instance.
```python
import typing


class Builder:
def add(self, value: int) -> typing.Self:
self.values.append(value)
Expand All @@ -84,9 +88,10 @@ enables static analysis tools to detect typos and signature mismatches.
```python
import typing


class Base:
def run(self) -> None:
...
def run(self) -> None: ...


class Child(Base):
@typing.override
Expand All @@ -104,6 +109,7 @@ checkers.
```python
import typing


def is_str_list(val: list[object]) -> typing.TypeIs[list[str]]:
return all(isinstance(x, str) for x in val)
```
Expand All @@ -119,6 +125,7 @@ type is provided.
```python
T = typing.TypeVar("T", default=int)


class Box[T]:
def __init__(self, value: T | None = None):
# Fallback to the TypeVar default (int in this example)
Expand Down
24 changes: 23 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ UV_ENV = UV_CACHE_DIR=.uv-cache UV_TOOL_DIR=.uv-tools
RUFF := $(UV_ENV) uv run ruff
TYPOS_VERSION ?= 1.48.0
TYPOS := uv tool run typos@$(TYPOS_VERSION)
# Keep Pylint independent from the project virtual environment. The PyPy shim
# makes the baseline Pylint policy available on every supported host.
PYLINT_PYTHON ?= pypy
PYLINT_TARGETS ?= concordat scripts tests
PYLINT_PYPY_SHIM_REF ?= 726d09f968b4d729ee4b29c71fc732e744854f3b
PYLINT_PYPY_SHIM = git+https://github.com/leynos/pylint-pypy-shim.git@$(PYLINT_PYPY_SHIM_REF)
PYLINT = $(UV_ENV) uv tool run --python $(PYLINT_PYTHON) --from '$(PYLINT_PYPY_SHIM)' pylint-pypy
# Run the df12 plugin in a separate CPython 3.14 process. Keeping its
# dependency out of the PyPy shim avoids interpreter and plugin version skew.
DF12_PYTHON_LINTS_REF ?= 9c835f35b0f1690597ade799c9c6a30bc5922959
DF12_PYTHON_LINTS = git+https://github.com/leynos/df12-python-lints.git@$(DF12_PYTHON_LINTS_REF)
DF12_PYTHON ?= 3.14
DF12_PYLINT_TARGETS ?= concordat scripts
DF12_PYLINT_MESSAGES = R9101,C9102,R9103,R9104,C9105,C9106,C9107,R9108,R9109,R9110,R9111,R9112,C9112
DF12_PYLINT = $(UV_ENV) uv run --isolated --python $(DF12_PYTHON) --with '$(DF12_PYTHON_LINTS)' pylint \
--disable=all --load-plugins=df12_python_lints --py-version=3.13 \
--enable=$(DF12_PYLINT_MESSAGES)
AMBRLEAKS = $(UV_ENV) uv tool run --python $(DF12_PYTHON) \
--from '$(DF12_PYTHON_LINTS)' ambrleaks
# Pinned so `make typecheck` reports the same diagnostics locally and in
# CI. An unpinned `ty` drifts between machines and hides real findings.
TY_VERSION ?= 0.0.65
Expand Down Expand Up @@ -74,6 +93,9 @@ check-fmt: build ## Verify formatting

lint: build ## Run linters
$(RUFF) check
$(PYLINT) $(PYLINT_TARGETS)
$(DF12_PYLINT) $(DF12_PYLINT_TARGETS)
$(AMBRLEAKS) tests
+$(MAKE) spelling

typecheck: build uv ## Run typechecking
Expand All @@ -98,7 +120,7 @@ vale: $(VALE) $(ACRONYM_SCRIPT) ## Check prose
uv run --with "git+https://github.com/leynos/concordat-vale.git" $(ACRONYM_SCRIPT)
$(VALE) --no-global .

test: build uv $(VENV_TOOLS) ## Run tests
test: build spelling uv $(VENV_TOOLS) ## Run tests
$(UV_ENV) uv run pytest -v -n auto

help: ## Show available targets
Expand Down
Loading
Loading