Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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
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
5 changes: 5 additions & 0 deletions cmd_mox/command_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def run(self, invocation: Invocation, extra_env: dict[str, str]) -> Response:
* ``126`` - command found but not executable or execution failed
(e.g., permission denied)
* ``124`` - execution timed out

Returns
-------
Response
The command result, including the applied environment overrides.
"""
env = self._prepare_environment(extra_env, invocation.env)
merged_path = env.get(
Expand Down
22 changes: 17 additions & 5 deletions cmd_mox/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
from collections import deque
from pathlib import Path

from . import _path_utils
from .command_runner import CommandRunner
from .environment import (
IS_WINDOWS,
EnvironmentManager,
ensure_dir_exists,
temporary_env,
Expand Down Expand Up @@ -84,6 +84,11 @@ def __init__(
environment:
Optional :class:`EnvironmentManager` instance used to prepare shim and
PATH state. When omitted a fresh manager is created automatically.

Raises
------
ValueError
If ``max_journal_entries`` is not positive.
"""
self.environment = (
environment if environment is not None else EnvironmentManager()
Expand Down Expand Up @@ -193,7 +198,7 @@ def _handle_auto_verify(self, exc_type: type[BaseException] | None) -> bool:
verify_error: Exception | None = None
try:
self.verify()
except Exception as err: # noqa: BLE001
except Exception as err: # noqa: BLE001 - this boundary intentionally converts arbitrary callback failures
# pragma: no cover - verification failed
verify_error = err
if exc_type is None and verify_error is not None:
Expand Down Expand Up @@ -316,11 +321,11 @@ def verify(self) -> None:
first_error: BaseException | None = None
try:
self._finalize_verification()
except BaseException as exc: # noqa: BLE001
except BaseException as exc: # noqa: BLE001 - this boundary intentionally converts arbitrary callback failures
first_error = exc
try:
self._finalize_recording_sessions()
except BaseException as exc: # noqa: BLE001
except BaseException as exc: # noqa: BLE001 - this boundary intentionally converts arbitrary callback failures
if first_error is None:
first_error = exc
if first_error is not None:
Expand Down Expand Up @@ -514,6 +519,11 @@ def _prepare_passthrough(
final merging into an effective execution environment happens
downstream when the shim consumes the resulting
:class:`~cmd_mox.ipc.PassthroughRequest`.

Returns
-------
Response
Instructions that make the shim execute the passthrough command.
"""
overrides = self._apply_expectation_env(double, invocation)
lookup_path = self.environment.original_environment.get(
Expand Down Expand Up @@ -640,7 +650,9 @@ def _start_ipc_server(self) -> None:
raise MissingEnvironmentError(msg)
shim_dir, socket_path = self._validate_replay_environment()
create_shim_symlinks(shim_dir, self._commands)
server_factory = CallbackNamedPipeServer if IS_WINDOWS else CallbackIPCServer
server_factory = (
CallbackNamedPipeServer if _path_utils.IS_WINDOWS else CallbackIPCServer
)
self._server = server_factory(
socket_path,
self._handle_invocation,
Expand Down
25 changes: 22 additions & 3 deletions cmd_mox/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from ._validators import validate_positive_finite_timeout
from .fs_retry import robust_rmtree

IS_WINDOWS = path_utils.IS_WINDOWS
_MAX_PATH_THRESHOLD: typ.Final[int] = 240

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -55,11 +54,11 @@ class _Kernel32(typ.Protocol):
class _CtypesModule(typ.Protocol):
"""Typed subset of ``ctypes`` used by the Windows path-shortening helper."""

def WinDLL(self, name: str, *, use_last_error: bool) -> _Kernel32: ... # noqa: N802
def WinDLL(self, name: str, *, use_last_error: bool) -> _Kernel32: ... # noqa: N802 - the protocol mirrors the external Windows API casing

def get_last_error(self) -> int: ...

def FormatError(self, code: int) -> str: ... # noqa: N802
def FormatError(self, code: int) -> str: ... # noqa: N802 - the protocol mirrors the external Windows API casing

def create_unicode_buffer(self, init_or_size: int) -> _UnicodeBuffer: ...

Expand Down Expand Up @@ -164,6 +163,11 @@ def ensure_dir_exists(

Normalises path validation so callers raise consistent, descriptive errors
when environment directories disappear or are misconfigured.

Returns
-------
Path
The resolved existing directory.
"""
if path is None:
msg = missing_message or f"{name} is missing"
Expand Down Expand Up @@ -201,6 +205,11 @@ def _collect_os_error(
The decorated function is expected to take ``(self, cleanup_errors)`` and
should raise ``OSError`` on failure. Any such exception is captured and the
formatted message appended to ``cleanup_errors``.

Returns
-------
collections.abc.Callable
A decorator that converts ``OSError`` failures into cleanup records.
"""

def decorator(
Expand Down Expand Up @@ -393,6 +402,16 @@ def _resolve_effective_timeout(self, timeout: float | object) -> float | None:
The helper isolates the branching necessary to honour explicit
overrides, fall back to the previously configured value, and surface
invalid types consistently with other validation paths.

Returns
-------
float | None
The configured timeout, or ``None`` when IPC timeouts are disabled.

Raises
------
TypeError
If ``timeout`` is neither a real number nor the unset sentinel.
"""
if timeout is _UNSET_TIMEOUT:
return self.ipc_timeout
Expand Down
25 changes: 18 additions & 7 deletions cmd_mox/expectations.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def is_sensitive_recording_env_key(key: str) -> bool:
Combines the substring-based check from :func:`_is_sensitive_env_key` with
a regex that catches word-segment patterns such as ``GITHUB_KEY`` or
``DB_PWD``.

Returns
-------
bool
Whether recordings should redact the environment variable's value.
"""
return _is_sensitive_env_key(key) or bool(_SECRET_ENV_KEY_RE.search(key))

Expand Down Expand Up @@ -73,6 +78,11 @@ def with_stdin(self, data: str | cabc.Callable[[str], object]) -> Expectation:
"""Expect ``stdin`` to equal ``data`` or satisfy a predicate.

The predicate's return value will be coerced to bool.

Returns
-------
Expectation
This expectation, allowing further fluent configuration.
"""
self.stdin = data
return self
Expand Down Expand Up @@ -102,7 +112,8 @@ def times_called(self, count: int) -> Expectation:

def times(self, count: int) -> Expectation:
"""Alias for :meth:`times_called` matching the fluent DSL."""
return self.times_called(count)
self.count = count
return self

def in_order(self) -> Expectation:
"""Mark this expectation as ordered relative to others."""
Expand Down Expand Up @@ -145,11 +156,11 @@ def _validate_matchers(self, args: list[str]) -> bool:
return False
if len(args) != len(matchers):
return False
for arg, matcher in zip(args, matchers): # noqa: B905
for arg, matcher in zip(args, matchers): # noqa: B905 - paired values have already been validated for matching arity
try:
if not matcher(arg):
return False
except Exception: # noqa: BLE001
except Exception: # noqa: BLE001 - this boundary intentionally converts arbitrary callback failures
return False
return True

Expand Down Expand Up @@ -188,11 +199,11 @@ def _explain_match_args_mismatch(self, invocation: Invocation) -> str | None:
f"expected {len(self.match_args)} args but got {len(invocation.args)}"
)
for i, (arg, matcher) in enumerate(
zip(invocation.args, self.match_args), # noqa: B905
zip(invocation.args, self.match_args), # noqa: B905 - paired values have already been validated for matching arity
):
try:
ok = bool(matcher(arg))
except Exception as exc: # noqa: BLE001
except Exception as exc: # noqa: BLE001 - this boundary intentionally converts arbitrary callback failures
return (
f"arg[{i}] predicate {matcher!r} raised "
f"{exc.__class__.__name__}: {exc}"
Expand All @@ -213,7 +224,7 @@ def _explain_stdin_mismatch(self, invocation: Invocation) -> str | None:
return f"stdin expectation {self.stdin!r} is not str or callable"
try:
ok = bool(self.stdin(invocation.stdin))
except Exception as exc: # noqa: BLE001
except Exception as exc: # noqa: BLE001 - this boundary intentionally converts arbitrary callback failures
return (
f"stdin predicate {self.stdin!r} raised {exc.__class__.__name__}: {exc}"
)
Expand Down Expand Up @@ -243,7 +254,7 @@ def _matches_stdin(self, invocation: Invocation) -> bool:
if callable(self.stdin):
try:
return bool(self.stdin(invocation.stdin))
except Exception: # noqa: BLE001
except Exception: # noqa: BLE001 - this boundary intentionally converts arbitrary callback failures
return False
return False

Expand Down
Loading
Loading