Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
24 changes: 13 additions & 11 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,18 +192,18 @@ ______________________________________________________________________
## 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", "wheel"]
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
in `uv`, you need at least `setuptools>=64.0` and `wheel`. (Python
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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.
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", "wheel"]
build-backend = "setuptools.build_meta"

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

4. **Build System:**

- `setuptools>=61.0` plus `wheel` ensures both legacy and editable installs
- `setuptools>=64.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])
- `build-backend = "setuptools.build_meta"` tells `uv` how to compile your
Expand Down Expand Up @@ -372,7 +374,7 @@ 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`,
- Declare a PEP 517 `[build-system]` (e.g. `setuptools>=64.0`, `wheel`,
`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
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,
Ensuring your functions are clear in intent and free from unnecessary control
flow. Follow these rules:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

## 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 ?= v0.2.0
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) pylint \
--disable=all --load-plugins=df12_python_lints --py-version=3.13 \
--enable=$(DF12_PYLINT_MESSAGES)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
AMBRLEAKS = $(UV_ENV) uv tool run --python $(DF12_PYTHON) \
--from '$(DF12_PYTHON_LINTS)' ambrleaks
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
# 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
10 changes: 1 addition & 9 deletions concordat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
"""concordat package."""

from __future__ import annotations

PACKAGE_NAME = "concordat"

try: # pragma: no cover - Rust optional
rust = __import__(f"_{PACKAGE_NAME}_rs")
hello = rust.hello # type: ignore[attr-defined]
except ModuleNotFoundError: # pragma: no cover - Python fallback
from .pure import hello
from .runtime import hello

__all__ = ["hello"]
Loading
Loading