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
22 changes: 22 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"config": {
"MD004": { "style": "dash" },
"MD010": { "code_blocks": false },
"MD013": {
"line_length": 80,
"code_block_line_length": 120,
"tables": false,
"headings": false
},
"MD029": { "style": "ordered" }
},
"ignores": [
"**/.venv/**",
".node_modules/**",
"**/node_modules/**",
"**/target/**",
".terraform/**",
".uv-cache/**",
"CRUSH.md"
]
}
39 changes: 20 additions & 19 deletions template/docs/complexity-antipatterns-and-refactoring-strategies.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ the number of edges, N is the number of nodes, and P is the number of connected
components (typically 1 for a single program or method).[^3] A simpler
formulation applies to a single subroutine:

M = number of decision points + 1, where decision points include constructs
like `if` statements and conditional loops.[^3]
M = number of decision points + 1, where decision points include constructs like
`if` statements and conditional loops.[^3]

Thresholds and Implications:

Expand Down Expand Up @@ -198,14 +198,14 @@ attention, much like a physical bumpy road slows down driving.[^9]
### B. How it forms and its impact

The Bumpy Road antipattern, like many software antipatterns, often emerges from
development practices that prioritize short-term speed over long-term
structural integrity.[^2] Rushed development cycles, lack of clear design, or
cutting corners on maintenance can lead to the gradual accumulation of
conditional logic within a single function.[^2] As new requirements emerge
alongside additional edge cases, developers might add conditional branches to
an existing method. Examples include an `if` statement, a loop, or a deeply
nested match added in haste when a team could instead step back to refactor and
create appropriate abstractions.
development practices that prioritize short-term speed over long-term structural
integrity.[^2] Rushed development cycles, lack of clear design, or cutting
corners on maintenance can lead to the gradual accumulation of conditional
logic within a single function.[^2] As new requirements emerge alongside
additional edge cases, developers might add conditional branches to an existing
method. Examples include an `if` statement, a loop, or a deeply nested match
added in haste when a team could instead step back to refactor and create
appropriate abstractions.

The impact of this antipattern is significant:

Expand Down Expand Up @@ -393,10 +393,10 @@ maintainable systems.
1\. Separation of Concerns (SoC)

Separation of Concerns is a design principle that advocates for dividing a
computer program into distinct sections, where each section addresses a
separate concern.[^13] A "concern" is a set of information that affects the
code of a computer program. Modularity is achieved by encapsulating information
within a section of code that has a well-defined interface.[^13]
computer program into distinct sections, where each section addresses a separate
concern.[^13] A "concern" is a set of information that affects the code of a
computer program. Modularity is achieved by encapsulating information within a
section of code that has a well-defined interface.[^13]

The Bumpy Road antipattern is a direct violation of SoC. Each "bump" in the
code often represents a distinct concern, or responsibility, that has been
Expand Down Expand Up @@ -471,10 +471,10 @@ Command Query Responsibility Segregation promotes a clear separation that can
prevent the kind of tangled logic that forms Bumpy Roads. By isolating write
operations (commands) from read operations (queries), and by encouraging
task-based commands, the system naturally tends towards smaller, more cohesive
units of behaviour, thus reducing overall cognitive complexity within
individual components.[^14] The separation allows for independent optimization
and scaling of read and write sides, but more importantly for this discussion,
it enforces a structural discipline that discourages methods from accumulating
units of behaviour, thus reducing overall cognitive complexity within individual
components.[^14] The separation allows for independent optimization and
scaling of read and write sides, but more importantly for this discussion, it
enforces a structural discipline that discourages methods from accumulating
diverse responsibilities.[^14]

### B. Avoiding spaghetti code turning into ravioli code
Expand Down Expand Up @@ -653,7 +653,7 @@ behaviour—common culprits for bugs and increased cognitive load in imperative
code.[^26]

Examples include using Structured Query Language for database queries—
specifying the desired dataset rather than the retrieval algorithm[^26]—or
specifying the desired dataset rather than the retrieval algorithm[^33]—or
employing functional programming constructs like `map`, `filter`, and `reduce`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
on collections instead of writing explicit loops. Refactoring imperative code
to a declarative style can start small, perhaps by converting a loop that
Expand Down Expand Up @@ -885,3 +885,4 @@ maintain.
[^32]: Refactor `if-else` Statements to `match-case` for Improved Readability
and Maintainability in Python 3.10+ · Issue #453 — GitHub,
<https://github.com/sourcery-ai/sourcery/issues/453>
[^33]: SQL — Wikipedia, <https://en.wikipedia.org/wiki/SQL>
22 changes: 12 additions & 10 deletions template/docs/scripting-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,16 @@ def default(
# Required parameters
bin_name: Annotated[str, Parameter(required=True)],
version: Annotated[str, Parameter(required=True)],

# Optional scalars
package_name: Optional[str] = None,
target: Optional[str] = None,
outdir: Optional[Path] = None,
dry_run: bool = False,

# Lists (whitespace/newline separated by default)
formats: list[str] | None = None,
man_paths: Annotated[list[Path] | None, Parameter(env_var="INPUT_MAN_PATHS")] = None,
man_paths: Annotated[
list[Path] | None, Parameter(env_var="INPUT_MAN_PATHS")
] = None,
deb_depends: list[str] | None = None,
rpm_depends: list[str] | None = None,
):
Expand Down Expand Up @@ -338,15 +338,15 @@ The exceptions raised are those from the Python event loop itself, such as

A `Catalogue` instance is safe to share across concurrent tasks because it is
read-only after construction. `sh.scoped(CATALOGUE)` is a context manager that
binds the catalogue for the current execution scope. Authors must not mutate the
catalogue inside a concurrent task. Construct the catalogue once at module level
and re-use it.
binds the catalogue for the current execution scope. Authors must not mutate
the catalogue inside a concurrent task. Construct the catalogue once at module
level and re-use it.

#### Concurrent testing patterns with cmd-mox

Concurrent async script paths use the same catalogue and scoped context in tests
as they do in production code. `cmd-mox` intercepts at the catalogue boundary
regardless of whether `run()` or `run_sync()` is used.
Concurrent async script paths use the same catalogue and scoped context in
tests as they do in production code. `cmd-mox` intercepts at the catalogue
boundary regardless of whether `run()` or `run_sync()` is used.

```python
import pytest
Expand Down Expand Up @@ -407,7 +407,9 @@ f.write_text("1.2.3\n", encoding="utf-8")
version = f.read_text(encoding="utf-8").strip()

# Atomic write pattern (tmp → replace)
with tempfile.NamedTemporaryFile("w", delete=False, dir=f.parent, encoding="utf-8") as tmp:
with tempfile.NamedTemporaryFile(
"w", delete=False, dir=f.parent, encoding="utf-8"
) as tmp:
tmp.write("new-contents\n")
tmp_path = Path(tmp.name)

Expand Down
13 changes: 13 additions & 0 deletions template/tests/test_public_api.py.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Tests for the public package API."""

from __future__ import annotations

import {{ package_name }}


def test_hello_uses_configured_backend() -> None:
"""Verify the expected greeting for this generated project."""
expected_greeting = "{% if use_rust %}hello from Rust{% else %}hello from Python{% endif %}"
assert {{ package_name }}.hello() == expected_greeting, (
"expected public hello() to return the configured backend greeting"
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name = "_{{ package_name }}_rs"
crate-type = ["cdylib", "rlib"]

[dependencies]
pyo3 = { version = "0.28.3", features = ["extension-module"] }
pyo3 = { version = "0.29.0", features = ["extension-module"] }

[lints.clippy]
pedantic = { level = "warn", priority = -1 }
Expand Down
26 changes: 13 additions & 13 deletions template/{{ package_name }}/__init__.py.jinja
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"""{{ project_name }} package."""
"""Public package entry point for {{ project_name }}.

from __future__ import annotations

import importlib
import typing as typ
The package exposes the supported public API for generated projects. Import
from this module when application code needs the package's main functionality;
the package selects the configured backend internally and exports `hello` as
the stable greeting function.

if typ.TYPE_CHECKING:
import collections.abc as cabc
Examples
--------
>>> import {{ package_name }}
>>> {{ package_name }}.hello()
"{% if use_rust %}hello from Rust{% else %}hello from Python{% endif %}"
"""

PACKAGE_NAME = "{{ package_name }}"
from __future__ import annotations

try: # pragma: no cover - Rust optional
rust = importlib.import_module(f"._{PACKAGE_NAME}_rs", package=__name__)
hello = typ.cast("cabc.Callable[[], str]", rust.hello)
except ModuleNotFoundError: # pragma: no cover - Python fallback
from .pure import hello
from ._runtime import hello

__all__ = ["hello"]
37 changes: 37 additions & 0 deletions template/{{ package_name }}/_runtime.py.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Select the generated package implementation backend.

This module keeps `__init__.py` as a stable public re-export while isolating
the logic that chooses between the optional Rust extension and the pure Python
implementation. External callers should import the package root and call
`{{ package_name }}.hello()`. Internal generated code may import `hello` from
`._runtime` when it needs the already-selected backend function.

Examples
--------
>>> import {{ package_name }}
>>> {{ package_name }}.hello()
"{% if use_rust %}hello from Rust{% else %}hello from Python{% endif %}"
"""

from __future__ import annotations
{% if use_rust %}
import importlib
import typing as typ

if typ.TYPE_CHECKING:
import collections.abc as cabc

PACKAGE_NAME = "{{ package_name }}"
RUST_MODULE_NAME = f"_{PACKAGE_NAME}_rs"
EXPECTED_RUST_MODULE_NAME = f"{PACKAGE_NAME}.{RUST_MODULE_NAME}"

try: # pragma: no cover - Rust optional
rust = importlib.import_module(f".{RUST_MODULE_NAME}", package=__package__)
hello = typ.cast("cabc.Callable[[], str]", rust.hello)
except ModuleNotFoundError as exc: # pragma: no cover - Python fallback
if exc.name != EXPECTED_RUST_MODULE_NAME:
raise
from .pure import hello as hello
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{%- else %}
from .pure import hello as hello
{%- endif %}
90 changes: 90 additions & 0 deletions tests/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
from __future__ import annotations

import ast
import os
import shutil
import subprocess
import sys
from pathlib import Path

import pytest
Expand Down Expand Up @@ -157,6 +159,91 @@ def test_pure_module_snapshot(
assert pure_module == snapshot


def test_runtime_module_documents_and_limits_fallback(
copier: CopierFixture, tmp_path: Path
) -> None:
"""Validate generated runtime backend-selection safeguards.

Parameters
----------
copier
``pytest-copier`` fixture used to render the template.
tmp_path
Temporary directory where the rendered project is created.

Returns
-------
None
The test passes when the generated runtime module documents its role,
falls back only for the missing Rust extension, and re-raises unrelated
missing-module failures.
"""
project = copier.copy(
tmp_path / "runtime",
project_name="Runtime",
package_name="runtime_pkg",
use_rust=True,
)

runtime_module = read_generated_file(project, "runtime_pkg/_runtime.py")

assert runtime_module.startswith(
'"""Select the generated package implementation backend.'
), "expected rendered _runtime.py to start with its module docstring"
assert "except ModuleNotFoundError as exc:" in runtime_module, (
"expected _runtime.py to inspect ModuleNotFoundError details"
)
assert 'EXPECTED_RUST_MODULE_NAME = f"{PACKAGE_NAME}.{RUST_MODULE_NAME}"' in (
runtime_module
), "expected _runtime.py to define the fully qualified Rust module name"
assert "if exc.name != EXPECTED_RUST_MODULE_NAME:" in runtime_module, (
"expected _runtime.py to fall back only for the missing Rust module"
)
assert "raise" in runtime_module, (
"expected _runtime.py to re-raise unrelated ModuleNotFoundError cases"
)

python_env = os.environ | {"PYTHONPATH": str(project.path)}
fallback = subprocess.run(
[
sys.executable,
"-c",
(
"import runtime_pkg; "
"import runtime_pkg._runtime as runtime; "
"assert runtime_pkg.hello() == 'hello from Python'; "
"assert runtime.__doc__ is not None"
),
],
cwd=project.path,
env=python_env,
check=False,
capture_output=True,
text=True,
)
assert fallback.returncode == 0, fallback.stderr

(project / "runtime_pkg" / "_runtime_pkg_rs.py").write_text(
'raise ModuleNotFoundError("missing dependency", name="transitive_dependency")\n',
encoding="utf-8",
)
unexpected_missing_module = subprocess.run(
[sys.executable, "-c", "import runtime_pkg"],
cwd=project.path,
env=python_env,
check=False,
capture_output=True,
text=True,
)

assert unexpected_missing_module.returncode != 0, (
"expected runtime import to re-raise unrelated ModuleNotFoundError"
)
assert "transitive_dependency" in unexpected_missing_module.stderr, (
"expected runtime import failure to preserve the original missing module"
)


def test_python_only_template(copier: CopierFixture, tmp_path: Path) -> None:
"""Validate the Python-only rendered project variant.

Expand Down Expand Up @@ -190,6 +277,9 @@ def test_python_only_template(copier: CopierFixture, tmp_path: Path) -> None:
assert not (proj / "docs" / "rust-extension.md").exists(), (
"Rust documentation should not be generated for Python-only template"
)
assert (proj / "docs" / "documentation-style-guide.md").exists(), (
"Python-only template should include shared documentation guidance"
)
assert "maturin" not in (proj / "pyproject.toml").read_text(encoding="utf-8"), (
"maturin should not be in pyproject.toml for Python-only template"
)
Expand Down
Loading