Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
29 changes: 15 additions & 14 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`, you need at least `setuptools>=64.0`. (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` supports 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
package. (Python Packaging[^4], Astral Docs[^7])

Expand Down Expand Up @@ -372,7 +373,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`,
`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
20 changes: 17 additions & 3 deletions .rules/python-return.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ flow. 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
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ PYLINT_PYPY_SHIM_REF ?= 726d09f968b4d729ee4b29c71fc732e744854f3b
PYLINT_PYPY_SHIM = git+https://github.com/leynos/pylint-pypy-shim.git@$(PYLINT_PYPY_SHIM_REF)
PYLINT_BASELINE_DISABLE = no-else-return,unnecessary-ellipsis,too-many-lines,too-many-arguments,too-many-positional-arguments,subprocess-run-check,use-implicit-booleaness-not-comparison-to-string,unnecessary-dunder-call,use-implicit-booleaness-not-comparison
PYLINT = $(UV_ENV) $(UV) tool run --python $(PYLINT_PYTHON) --from '$(PYLINT_PYPY_SHIM)' pylint-pypy --disable=$(PYLINT_BASELINE_DISABLE)
DF12_PYTHON ?= 3.14
DF12_PYTHON_LINTS_REF ?= 9c835f35b0f1690597ade799c9c6a30bc5922959
DF12_PYTHON_LINTS = git+https://github.com/leynos/df12-python-lints.git@$(DF12_PYTHON_LINTS_REF)
DF12_PYLINT = $(UV_ENV) UV_PYTHON_PREFERENCE=only-managed $(UV) run --isolated --python $(DF12_PYTHON) --with '$(DF12_PYTHON_LINTS)' pylint
AMBRLEAKS = $(UV_ENV) UV_PYTHON_PREFERENCE=only-managed $(UV) run --isolated --python $(DF12_PYTHON) --with '$(DF12_PYTHON_LINTS)' ambrleaks
WINDOWS_SMOKE_ARGS = tests/test_windows_environment.py \
tests/test_windows_support_bdd.py \
--log-file=windows-ipc.log \
Expand Down Expand Up @@ -89,6 +94,8 @@ markdownlint-run: ## Run markdownlint-cli2 with pinned fallback
lint: build ## Run linters
$(RUFF) check
$(PYLINT) $(PYLINT_TARGETS)
$(DF12_PYLINT) --rcfile=pylintrc-df12.toml $(PYLINT_TARGETS)
$(AMBRLEAKS) tests
+$(MAKE) spelling

typecheck: build ## Run typechecking
Expand Down
25 changes: 24 additions & 1 deletion cmd_mox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,30 @@ def __getattr__(name: str) -> _ModuleType: ...


def __getattr__(name: str) -> _ModuleType | cabc.Callable[..., object]:
"""Lazily import optional dependencies when requested."""
"""Lazily import optional dependencies when requested.

Parameters
----------
name : str
Module attribute to resolve.

Returns
-------
types.ModuleType or collections.abc.Callable[..., object]
Imported submodule, or the ``cmd_mox`` pytest fixture callable when
``name`` is ``"cmd_mox_fixture"``.

Raises
------
AttributeError
If ``name`` does not identify a submodule in :mod:`cmd_mox`.
RuntimeError
If resolving ``"cmd_mox_fixture"`` fails because an optional
dependency of the pytest plugin is unavailable.
ModuleNotFoundError
If importing a requested submodule fails because a dependency other
than the requested module is unavailable.
"""
if name == "cmd_mox_fixture":
try:
from .pytest_plugin import cmd_mox as _cmd_mox_fixture
Expand Down
16 changes: 14 additions & 2 deletions cmd_mox/_path_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@


def normalize_path_string(path: str) -> str:
"""Return a normalized string path using platform rules."""
"""Return a normalized string path using platform rules.

Returns
-------
str
The normalised path, using case-folding on Windows.
"""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
module = ntpath if IS_WINDOWS else os.path
normalized = module.normpath(path)
if IS_WINDOWS:
Expand All @@ -18,5 +24,11 @@ def normalize_path_string(path: str) -> str:


def normalize_path(path: os.PathLike[str] | str) -> str:
"""Normalize *path* regardless of whether it is a string or Path."""
"""Normalise *path* regardless of whether it is a string or Path.

Returns
-------
str
The normalised path string.
"""
return normalize_path_string(os.fspath(path))
32 changes: 28 additions & 4 deletions cmd_mox/_shim_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@


def _try_get_stdlib_path() -> str | None:
"""Return the configured stdlib path, or ``None`` if config lookup fails."""
"""Return the configured stdlib path, or ``None`` if lookup fails.

Returns
-------
str or None
The configured standard-library directory when available.
"""
try:
return sysconfig.get_path("stdlib")
except (AttributeError, ImportError, KeyError, OSError, ValueError, TypeError):
Expand All @@ -43,7 +49,13 @@ def _temporary_sys_path(entries: tuple[str, ...]) -> cabc.Iterator[None]:


def _get_stdlib_path() -> str | None:
"""Return the stdlib path, guarding against missing or invalid configs."""
"""Return the stdlib path, guarding against invalid configuration.

Returns
-------
str or None
The standard-library directory, if it can be resolved safely.
"""
stdlib_path = _try_get_stdlib_path()
if stdlib_path is not None:
return stdlib_path
Expand All @@ -53,7 +65,13 @@ def _get_stdlib_path() -> str | None:


def _create_module_from_file(module_name: str, file_path: Path) -> ModuleType | None:
"""Load and return a module from *file_path*, or ``None`` on failure."""
"""Load a module from *file_path*, or return ``None`` on failure.

Returns
-------
ModuleType or None
The loaded module when the file has a usable import specification.
"""
try:
if not file_path.is_file():
return None
Expand All @@ -72,7 +90,13 @@ def _create_module_from_file(module_name: str, file_path: Path) -> ModuleType |


def _load_stdlib_platform() -> ModuleType:
"""Return the stdlib platform module, falling back to regular import."""
"""Return the stdlib platform module, with a regular-import fallback.

Returns
-------
ModuleType
The platform module loaded from the standard library.
"""
importlib.invalidate_caches()
stdlib_path = _get_stdlib_path()

Expand Down
51 changes: 46 additions & 5 deletions cmd_mox/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@


def validate_positive_finite_timeout(timeout: float) -> None:
"""Ensure *timeout* represents a usable IPC timeout value."""
"""Ensure *timeout* represents a usable IPC timeout value.

Raises
------
TypeError
If *timeout* is not a real number.
ValueError
If *timeout* is not finite and strictly positive.
"""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if isinstance(timeout, bool):
msg = "timeout must be a real number"
raise TypeError(msg)
Expand All @@ -17,7 +25,15 @@ def validate_positive_finite_timeout(timeout: float) -> None:


def validate_optional_timeout(timeout: float | None, *, name: str) -> None:
"""Validate optional timeout values passed to IPC helpers."""
"""Validate optional timeout values passed to IPC helpers.

Raises
------
TypeError
If *timeout* is not ``None`` or a real number.
ValueError
If a supplied timeout is not finite and strictly positive.
"""
if timeout is None:
return

Expand All @@ -32,7 +48,15 @@ def validate_optional_timeout(timeout: float | None, *, name: str) -> None:


def validate_retry_attempts(retries: int) -> None:
"""Ensure retry attempt counts are sensible."""
"""Ensure retry attempt counts are sensible.

Raises
------
TypeError
If *retries* is not an integer.
ValueError
If *retries* is less than one.
"""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if isinstance(retries, bool):
msg = "retries must be an integer"
raise TypeError(msg)
Expand All @@ -43,7 +67,15 @@ def validate_retry_attempts(retries: int) -> None:


def validate_retry_backoff(backoff: float) -> None:
"""Ensure retry backoff configuration is valid."""
"""Ensure retry backoff configuration is valid.

Raises
------
TypeError
If *backoff* is not a real number.
ValueError
If *backoff* is negative or not finite.
"""
if isinstance(backoff, bool):
msg = "backoff must be a real number"
raise TypeError(msg)
Expand All @@ -54,7 +86,16 @@ def validate_retry_backoff(backoff: float) -> None:


def validate_retry_jitter(jitter: float) -> None:
"""Ensure retry jitter configuration stays within safe bounds."""
"""Ensure retry jitter configuration stays within safe bounds.

Raises
------
TypeError
If *jitter* is not a real number.
ValueError
If *jitter* is outside the inclusive range from zero to one or is not
finite.
"""
if isinstance(jitter, bool):
msg = "jitter must be a real number"
raise TypeError(msg)
Expand Down
Loading
Loading