API Reference

Complete API documentation for Apathetic Python Utils.

Quick Reference

Category Functions
File Loading load_jsonc(), load_toml()
Path Utilities normalize_path_string(), has_glob_chars(), get_glob_root(), shorten_path()
Pattern Matching fnmatchcase_portable(), is_excluded_raw()
Module Detection detect_packages_from_files(), find_all_packages_under_path()
System Detection is_ci(), if_ci(), is_running_under_pytest(), detect_runtime_mode(), capture_output(), get_sys_version_info()
Runtime Utilities find_python_command(), ensure_stitched_script_up_to_date(), ensure_zipapp_up_to_date(), runtime_swap()
Subprocess Utilities run_with_output(), run_with_separated_output()
Text Processing plural(), remove_path_in_error_message()
Type Utilities safe_isinstance(), literal_to_set(), cast_hint(), schema_from_typeddict()
Version Utilities create_version_info()
Testing Utilities detect_module_runtime_mode(), create_mock_superclass_test(), patch_everywhere()
Constants CI_ENV_VARS

File Loading

load_jsonc

load_jsonc(path: Path) -> dict[str, Any] | list[Any] | None

Load and parse a JSONC (JSON with Comments) file.

Supports:

Parameters:

Parameter Type Description
path Path Path to the JSONC file

Returns:

Raises:

Example:

from apathetic_utils import load_jsonc
from pathlib import Path

config = load_jsonc(Path("config.jsonc"))
# {
#   "name": "my_app",
#   "version": "1.0.0"
# }

load_toml

load_toml(path: Path, *, required: bool = False) -> dict[str, Any] | None

Load and parse a TOML file, supporting Python 3.10 and 3.11+.

Uses:

Parameters:

Parameter Type Description
path Path Path to the TOML file
required bool If True, raise RuntimeError when tomli is missing on Python 3.10. If False, return None when unavailable. Defaults to False.

Returns:

Raises:

Example:

from apathetic_utils import load_toml
from pathlib import Path

pyproject = load_toml(Path("pyproject.toml"), required=True)

Path Utilities

normalize_path_string

normalize_path_string(raw: str) -> str

Normalize a user-supplied path string for cross-platform use.

Industry-standard (Git/Node/Python) rules:

This is purely lexical — it normalizes syntax, not filesystem state.

Parameters:

Parameter Type Description
raw str Raw path string to normalize

Returns:

Example:

from apathetic_utils import normalize_path_string

path = normalize_path_string("src\\utils\\file.py")
# Returns: "src/utils/file.py"

path = normalize_path_string("src//utils///file.py")
# Returns: "src/utils/file.py"

has_glob_chars

has_glob_chars(s: str) -> bool

Check if a string contains glob pattern characters (*, ?, [).

Parameters:

Parameter Type Description
s str String to check

Returns:

Example:

from apathetic_utils import has_glob_chars

has_glob_chars("src/**/*.py")  # True
has_glob_chars("src/file.py")  # False

get_glob_root

get_glob_root(pattern: str) -> Path

Return the non-glob portion of a path pattern like src/**/*.txt.

Normalizes paths to cross-platform format.

Parameters:

Parameter Type Description
pattern str Glob pattern string

Returns:

Example:

from apathetic_utils import get_glob_root

root = get_glob_root("src/**/*.txt")
# Returns: Path("src")

root = get_glob_root("tests/unit/**/*.py")
# Returns: Path("tests/unit")

shorten_path

shorten_path(
    path: str | Path,
    bases: str | Path | list[str | Path]
) -> str

Return the shortest path relative to any base’s common prefix.

Finds the longest shared prefix between path and each base path by comparing their path parts, and returns the shortest remaining portion of path. This works with any paths (files, directories, etc.) and does not require one path to be under the other.

When the common prefix is only root ("/"), returns the absolute path since a relative path from root is not useful.

Parameters:

Parameter Type Description
path str \| Path Path to shorten
bases str \| Path \| list[str \| Path] Single base path or list of base paths to find common prefix with

Returns:

Examples:

from apathetic_utils import shorten_path

# Single base
result = shorten_path(
    "/home/user/code/serger/src/serger/logs.py",
    "/home/user/code/serger/tests/utils/patch_everywhere.py"
)
# Returns: "src/serger/logs.py"

# Multiple bases - returns shortest
result = shorten_path(
    "/home/user/code/serger/src/logs.py",
    ["/home/user/code/serger/tests/utils/patch.py",
     "/home/user/code/serger/src"]
)
# Returns: "logs.py" (shortest: common prefix with src/)

# Returns absolute path when only root in common
result = shorten_path(
    "/var/lib/file.py",
    ["/home/user", "/tmp"]
)
# Returns: "/var/lib/file.py" (absolute path)

Pattern Matching

fnmatchcase_portable

fnmatchcase_portable(path: str, pattern: str) -> bool

Case-sensitive glob pattern matching with Python 3.10 ** backport.

Uses fnmatchcase (case-sensitive) as the base, with backported support for recursive ** patterns on Python 3.10.

Parameters:

Parameter Type Description
path str The path to match against the pattern
pattern str The glob pattern to match

Returns:

Example:

from apathetic_utils import fnmatchcase_portable

fnmatchcase_portable("src/utils/file.py", "src/**/*.py")  # True
fnmatchcase_portable("deep/nested/file.py", "**/*.py")    # True
fnmatchcase_portable("src/file.py", "src/*.py")            # True

is_excluded_raw

is_excluded_raw(
    path: Path | str,
    exclude_patterns: list[str],
    root: Path | str
) -> bool

Smart matcher for normalized inputs to check if a path matches exclusion patterns.

Parameters:

Parameter Type Description
path Path \| str Path to check against exclusion patterns
exclude_patterns list[str] List of glob patterns to exclude
root Path \| str Root directory for relative path resolution

Returns:

Example:

from apathetic_utils import is_excluded_raw
from pathlib import Path

patterns = ["**/__pycache__/**", "*.pyc", "tests/**"]
root = Path(".")

is_excluded_raw("src/__pycache__/file.pyc", patterns, root)  # True
is_excluded_raw("src/utils/file.py", patterns, root)         # False

Module Detection

detect_packages_from_files

detect_packages_from_files(
    file_paths: list[Path],
    package_name: str,
    *,
    source_bases: list[str] | None = None,
    _config_dir: Path | None = None
) -> tuple[set[str], list[str]]

Detect packages from file paths.

If files are under source_bases directories, treats everything after the matching base prefix as a package structure (regardless of __init__.py). Otherwise, follows Python’s import rules: only detects regular packages (with __init__.py files). Falls back to configured package_name if none detected.

Parameters:

Parameter Type Description
file_paths list[Path] List of file paths to check
package_name str Configured package name (used as fallback)
source_bases list[str] \| None Optional list of module base directories (absolute paths)
_config_dir Path \| None Optional config directory (unused, kept for compatibility)

Returns:

Example:

from apathetic_utils import detect_packages_from_files
from pathlib import Path

files = [Path("src/mypkg/module.py"), Path("src/otherpkg/file.py")]
packages, parents = detect_packages_from_files(
    files,
    "mypkg",
    source_bases=["/path/to/src"]
)
# packages: {"mypkg", "otherpkg", "mypkg"} (includes configured name)
# parents: ["/path/to/src"]

find_all_packages_under_path

find_all_packages_under_path(root_path: Path) -> set[str]

Find all package names under a directory by scanning for __init__.py files.

Parameters:

Parameter Type Description
root_path Path Path to the root directory to scan

Returns:

Example:

from apathetic_utils import find_all_packages_under_path
from pathlib import Path

packages = find_all_packages_under_path(Path("src"))
# Returns: {"mypkg", "otherpkg"} (all top-level packages with __init__.py)

System Detection

is_ci

is_ci() -> bool

Check if running in a CI environment.

Returns True if any of the following environment variables are set:

Returns:

Example:

from apathetic_utils import is_ci

if is_ci():
    print("Running in CI environment")
    # Adjust behavior for CI

if_ci

if_ci(ci_value: T, local_value: T) -> T

Return different values based on CI environment.

Useful for tests that need different behavior or expectations in CI vs local development environments.

Parameters:

Parameter Type Description
ci_value T Value to return when running in CI
local_value T Value to return when running locally

Returns:

Example:

from apathetic_utils import if_ci

# Different regex patterns for commit hashes
commit_pattern = if_ci(
    r"[0-9a-f]{4,}",  # CI: expect actual commit hash
    r"unknown \\(local build\\)"  # Local: expect placeholder
)

is_running_under_pytest

is_running_under_pytest() -> bool

Detect if code is running under pytest.

Checks multiple indicators:

Returns:

Example:

from apathetic_utils import is_running_under_pytest

if is_running_under_pytest():
    # Use test-specific configuration
    pass

detect_runtime_mode

detect_runtime_mode(package_name: str) -> str

Detect how the package is being executed.

Parameters:

Parameter Type Description
package_name str Name of the package to check

Returns:

Example:

from apathetic_utils import detect_runtime_mode

mode = detect_runtime_mode("my_package")
print(f"Running in {mode} mode")

capture_output

capture_output() -> ContextManager[CapturedOutput]

Temporarily capture stdout and stderr.

Any exception raised inside the block is re-raised with the captured output attached as exc.captured_output.

Returns:

CapturedOutput Attributes:

Attribute Type Description
stdout StringIO Captured stdout
stderr StringIO Captured stderr
merged StringIO Merged stdout and stderr

CapturedOutput Methods:

Method Description
as_dict() -> dict[str, str] Return contents as plain strings for serialization

Example:

from apathetic_utils import capture_output
import sys

with capture_output() as cap:
    print("Hello, world!")
    print("Error message", file=sys.stderr)

print(f"stdout: {cap.stdout.getvalue()}")
print(f"stderr: {cap.stderr.getvalue()}")
print(f"merged: {cap.merged.getvalue()}")

# Or convert to dict
output = cap.as_dict()

get_sys_version_info

get_sys_version_info() -> tuple[int, int, int] | tuple[int, int, int, str, int]

Get Python version information.

Wrapper for sys.version_info.

Returns:

Example:

from apathetic_utils import get_sys_version_info

version = get_sys_version_info()
print(f"Python {version[0]}.{version[1]}.{version[2]}")

Runtime Utilities

ensure_stitched_script_up_to_date

ensure_stitched_script_up_to_date(
    *,
    root: Path,
    script_name: str | None = None,
    package_name: str,
    command_path: str | None = None,
    log_level: str | None = None
) -> Path

Rebuild stitched script if missing or outdated.

Checks if the stitched script exists and is newer than all source files. If not, rebuilds it using either the provided bundler script or the Poetry-installed serger module.

Parameters:

Parameter Type Description
root Path Project root directory
script_name str \| None Optional name of the stitched script (without .py extension). If None, defaults to package_name.
package_name str Name of the package (e.g., “apathetic_utils”)
bundler_script str \| None Optional path to bundler script (relative to root). If provided and exists, uses python {bundler_script}. Otherwise, uses python -m serger --config .serger.jsonc.
log_level str \| None Optional log level to pass to serger. If provided, adds --log-level=<log_level> to the serger command.

Returns:

Raises:

Example:

from apathetic_utils import ensure_stitched_script_up_to_date
from pathlib import Path

# Using package_name as script_name (default)
script_path = ensure_stitched_script_up_to_date(
    root=Path("."),
    package_name="my_package"
)

# Using a custom script name
script_path = ensure_stitched_script_up_to_date(
    root=Path("."),
    script_name="my_script",
    package_name="my_package"
)

# Using a local bundler script
script_path = ensure_stitched_script_up_to_date(
    root=Path("."),
    script_name="my_script",
    package_name="my_package",
    command_path="bin/serger.py"
)

# Using a custom log level
script_path = ensure_stitched_script_up_to_date(
    root=Path("."),
    package_name="my_package",
    log_level="debug"
)

ensure_zipapp_up_to_date

ensure_zipapp_up_to_date(
    *,
    root: Path,
    script_name: str | None = None,
    package_name: str,
    command_path: str | None = None,
    log_level: str | None = None
) -> Path

Rebuild zipapp if missing or outdated.

Checks if the zipapp exists and is newer than all source files. If not, rebuilds it using zipbundler.

Parameters:

Parameter Type Description
root Path Project root directory
script_name str \| None Optional name of the zipapp (without .pyz extension). If None, defaults to package_name.
package_name str Name of the package (e.g., “apathetic_utils”)
command_path str \| None Optional path to bundler script (relative to root). If provided and exists, uses python {command_path}. Otherwise, uses zipbundler.
log_level str \| None Optional log level to pass to zipbundler. If provided, adds --log-level=<log_level> to the zipbundler command.

Returns:

Raises:

Example:

from apathetic_utils import ensure_zipapp_up_to_date
from pathlib import Path

# Using package_name as script_name (default)
zipapp_path = ensure_zipapp_up_to_date(
    root=Path("."),
    package_name="my_package"
)

# Using a custom script name
zipapp_path = ensure_zipapp_up_to_date(
    root=Path("."),
    script_name="my_script",
    package_name="my_package"
)

# Using a custom log level
zipapp_path = ensure_zipapp_up_to_date(
    root=Path("."),
    package_name="my_package",
    log_level="debug"
)

# Using a local bundler script
zipapp_path = ensure_zipapp_up_to_date(
    root=Path("."),
    package_name="my_package",
    command_path="bin/zipapp_builder.py"
)

runtime_swap

runtime_swap(
    *,
    root: Path,
    package_name: str,
    script_name: str | None = None,
    stitch_command: str | None = None,
    zipapp_command: str | None = None,
    mode: str | None = None,
    log_level: str | None = None
) -> bool

Pre-import hook — runs before any tests or plugins are imported.

Swaps in the appropriate runtime module based on RUNTIME_MODE:

This ensures all test imports work transparently regardless of runtime mode.

Parameters:

Parameter Type Description
root Path Project root directory
package_name str Name of the package (e.g., “apathetic_utils”)
script_name str \| None Optional name of the stitched script (without extension). If None, defaults to package_name.
stitch_command str \| None Optional path to bundler script for stitched mode (relative to root). If provided and exists, uses python {stitch_command}. Otherwise, uses python -m serger --config .serger.jsonc.
zipapp_command str \| None Optional path to bundler script for zipapp mode (relative to root). If provided and exists, uses python {zipapp_command}. Otherwise, uses zipbundler.
mode str \| None Runtime mode override. If None, reads from RUNTIME_MODE env var.
log_level str \| None Optional log level to pass to serger and zipbundler. If provided, adds --log-level=<log_level> to their commands.

Returns:

Raises:

Example:

from apathetic_utils import runtime_swap
from pathlib import Path

# In pytest conftest.py or similar
# Using package_name as script_name (default), Poetry-installed serger
runtime_swap(
    root=Path(__file__).parent.parent,
    package_name="my_package",
    mode="stitched"  # or None to read from RUNTIME_MODE env var
)

# Using a custom script name
runtime_swap(
    root=Path(__file__).parent.parent,
    package_name="my_package",
    script_name="my_script",
    mode="stitched"
)

# Using a local bundler script for stitched mode
runtime_swap(
    root=Path(__file__).parent.parent,
    package_name="my_package",
    script_name="my_script",
    stitch_command="bin/serger.py",
    mode="stitched"
)

# Using a local bundler script for zipapp mode
runtime_swap(
    root=Path(__file__).parent.parent,
    package_name="my_package",
    script_name="my_script",
    zipapp_command="bin/zipapp_builder.py",
    mode="zipapp"
)

# Using a custom log level
runtime_swap(
    root=Path(__file__).parent.parent,
    package_name="my_package",
    mode="stitched",
    log_level="debug"
)

Subprocess Utilities

run_with_output

run_with_output(
    args: list[str],
    *,
    cwd: Path | str | None = None,
    initial_env: dict[str, str] | None = None,
    env: dict[str, str] | None = None,
    forward_to: str | None = "normal",
    check: bool = False,
    **kwargs: Any
) -> SubprocessResult

Run subprocess and capture all output with optional forwarding.

This helper captures subprocess output and can optionally forward it to different destinations. It ensures captured output is available for error messages and can be displayed in real-time if desired.

Parameters:

Parameter Type Description
args list[str] Command and arguments to run
cwd Path \| str \| None Working directory
initial_env dict[str, str] \| None Initial environment state. If None, uses os.environ.copy(). If provided, starts with this environment (can be empty dict for blank environment).
env dict[str, str] \| None Additional environment variables to add/override
forward_to str \| None Where to forward captured output. Options: "bypass" (forward to sys.__stdout__/sys.__stderr__), "normal" (forward to sys.stdout/sys.stderr), None (don’t forward). Defaults to "normal".
check bool If True, raise CalledProcessError on non-zero exit
**kwargs Any Additional arguments passed to subprocess.run()

Returns:

SubprocessResult Attributes:

Attribute Type Description
stdout str Captured stdout
stderr str Captured stderr
returncode int Return code from subprocess
all_output str All output combined: stdout + stderr

Example:

from apathetic_utils import run_with_output
import sys

# Use current environment with additional vars
result = run_with_output(
    [sys.executable, "-m", "serger", "--config", "config.json"],
    cwd=tmp_path,
    env={"LOG_LEVEL": "test"},
)

# Forward output to bypass (visible in real-time, bypasses capsys)
result = run_with_output(
    [sys.executable, "-m", "serger", "--config", "config.json"],
    cwd=tmp_path,
    env={"LOG_LEVEL": "test"},
    forward_to="bypass",
)

# On test failure, output will be included
assert result.returncode == 0, f"Failed: {result.all_output}"

run_with_separated_output

run_with_separated_output(
    args: list[str],
    *,
    cwd: Path | str | None = None,
    initial_env: dict[str, str] | None = None,
    env: dict[str, str] | None = None,
    check: bool = False,
    **kwargs: Any
) -> SubprocessResultWithBypass

Run subprocess with stdout and __stdout__ captured separately.

This uses a Python wrapper to modify sys.__stdout__ before the command runs, allowing code to write to stdout and __stdout__ normally without any changes. Normal output (stdout) is captured, while bypass output (__stdout__) goes to the parent’s stdout.

Parameters:

Parameter Type Description
args list[str] Command and arguments to run (must be a Python command)
cwd Path \| str \| None Working directory
initial_env dict[str, str] \| None Initial environment state. If None, uses os.environ.copy(). If provided, starts with this environment (can be empty dict for blank environment).
env dict[str, str] \| None Additional environment variables to add/override
check bool If True, raise CalledProcessError on non-zero exit
**kwargs Any Additional arguments passed to subprocess.run()

Returns:

SubprocessResultWithBypass Attributes:

Attribute Type Description
stdout str Captured stdout (normal output, excluding bypass)
stderr str Captured stderr
bypass_output str Bypass output (written to sys.__stdout__)
returncode int Return code from subprocess
all_output str All output combined: stdout + stderr + bypass

Example:

from apathetic_utils import run_with_separated_output
import sys

result = run_with_separated_output(
    [sys.executable, "-m", "serger", "--config", "config.json"],
    cwd=tmp_path,
    env={"LOG_LEVEL": "test"},
)
# stdout contains normal output (captured)
# bypass_output contains output written to __stdout__
assert result.returncode == 0, f"Failed: {result.all_output}"

Text Processing

plural

plural(obj: Any) -> str

Return 's' if obj represents a plural count.

Accepts ints, floats, and any object implementing __len__(). Returns '' for singular or zero.

Parameters:

Parameter Type Description
obj Any Object to check (int, float, or object with __len__())

Returns:

Example:

from apathetic_utils import plural

count = 5
print(f"{count} file{plural(count)}")  # "5 files"

count = 1
print(f"{count} file{plural(count)}")  # "1 file"

items = [1, 2, 3]
print(f"{len(items)} item{plural(items)}")  # "3 items"

remove_path_in_error_message

remove_path_in_error_message(inner_msg: str, path: Path) -> str

Remove redundant file path mentions (and nearby filler) from error messages.

Useful when wrapping a lower-level exception that already embeds its own file reference, so the higher-level message can use its own path without duplication.

Parameters:

Parameter Type Description
inner_msg str Error message that may contain path references
path Path Path to remove from the message

Returns:

Example:

from apathetic_utils import remove_path_in_error_message
from pathlib import Path

error_msg = "Invalid JSONC syntax in /path/to/config.jsonc: Expecting value"
path = Path("/path/to/config.jsonc")

clean_msg = remove_path_in_error_message(error_msg, path)
# Returns: "Invalid JSONC syntax: Expecting value"

Type Utilities

safe_isinstance

safe_isinstance(value: Any, expected_type: Any) -> bool

Like isinstance(), but safe for TypedDicts and typing generics.

Handles:

Parameters:

Parameter Type Description
value Any Value to check
expected_type Any Type to check against

Returns:

Example:

from apathetic_utils import safe_isinstance
from typing import TypedDict, Optional

class Config(TypedDict):
    name: str
    value: int

data = {"name": "test", "value": 42}

# Works with TypedDict
safe_isinstance(data, Config)  # True

# Works with generics
safe_isinstance([1, 2, 3], list[int])  # True
safe_isinstance({"a": 1, "b": 2}, dict[str, int])  # True

# Works with Optional
safe_isinstance(None, Optional[str])  # True
safe_isinstance("test", Optional[str])  # True

literal_to_set

literal_to_set(literal_type: Any) -> set[Any]

Extract values from a Literal type as a set.

Parameters:

Parameter Type Description
literal_type Any A Literal type (e.g., Literal["a", "b"])

Returns:

Raises:

Example:

from apathetic_utils import literal_to_set
from typing import Literal

Mode = Literal["dev", "prod", "test"]
valid_modes = literal_to_set(Mode)
# Returns: {"dev", "prod", "test"}

if "dev" in valid_modes:
    print("Valid mode")

cast_hint

cast_hint(typ: type[T], value: Any) -> T

Explicit cast that documents intent but is purely for type hinting.

A drop-in replacement for typing.cast, meant for places where:

Does not handle Union, Optional, or nested generics: stick to cast() for those, because unions almost always represent a meaningful type narrowing.

This function performs no runtime checks.

Parameters:

Parameter Type Description
typ type[T] Target type
value Any Value to cast

Returns:

Example:

from apathetic_utils import cast_hint

# Type narrowing for type checkers
value: Any = get_data()
typed_value = cast_hint(dict[str, int], value)
# typed_value is now inferred as dict[str, int]

schema_from_typeddict

schema_from_typeddict(td: type[Any]) -> dict[str, Any]

Extract field names and their annotated types from a TypedDict.

Parameters:

Parameter Type Description
td type[Any] TypedDict class

Returns:

Example:

from apathetic_utils import schema_from_typeddict
from typing import TypedDict

class Config(TypedDict):
    name: str
    value: int

schema = schema_from_typeddict(Config)
# Returns: {"name": str, "value": int}

Version Utilities

create_version_info

create_version_info(major: int, minor: int, micro: int = 0) -> Any

Create a mock sys.version_info object with major and minor attributes.

This properly mocks sys.version_info so it can be used with attribute access (.major, .minor) and tuple comparison, matching the behavior of the real sys.version_info object (which is a named tuple).

Useful for testing or when you need to simulate different Python versions.

Parameters:

Parameter Type Description
major int Major version number (e.g., 3)
minor int Minor version number (e.g., 11)
micro int Micro version number (default: 0)

Returns:

Example:

from apathetic_utils import create_version_info

version = create_version_info(3, 11)
assert version.major == 3
assert version.minor == 11
assert version >= (3, 10)

Testing Utilities

detect_module_runtime_mode

detect_module_runtime_mode(
    mod: ModuleType,
    *,
    stitch_hints: set[str] | None = None
) -> str

Detect the runtime mode of a specific module.

Determines whether a module was built as part of a stitched single-file script, zipapp archive, or standard package by checking for markers and file path attributes.

This check prioritizes marker-based detection (__STITCHED__ and __STANDALONE__ attributes) as the most reliable method, but falls back to path heuristics when markers are not present.

Parameters:

Parameter Type Description
mod ModuleType Module to check
stitch_hints set[str] \| None Set of path hints to identify stitched modules. Defaults to {"/dist/", "stitched"}. Used as fallback when markers are not present.

Returns:

Raises:

Example:

from apathetic_utils import detect_module_runtime_mode
import apathetic_utils

# Detect module runtime mode
mode = detect_module_runtime_mode(apathetic_utils)
print(f"apathetic_utils is running in {mode} mode")

# With custom stitch hints
mode = detect_module_runtime_mode(
    apathetic_utils,
    stitch_hints={"/dist/", "bundled", "embedded"}
)

create_mock_superclass_test

create_mock_superclass_test(
    mixin_class: type,
    parent_class: type,
    method_name: str,
    camel_case_method_name: str,
    args: tuple[Any, ...],
    kwargs: dict[str, Any],
    monkeypatch: pytest.MonkeyPatch
) -> None

Test that a mixin’s snake_case method calls parent’s camelCase via super().

Creates a test class with controlled MRO:

Parameters:

Parameter Type Description
mixin_class type The mixin class containing the snake_case method
parent_class type The parent class with the camelCase method (e.g., logging.Logger)
method_name str Name of the snake_case method to test (e.g., “add_filter”)
camel_case_method_name str Name of the camelCase method to mock (e.g., “addFilter”)
args tuple[Any, ...] Arguments to pass to the snake_case method
kwargs dict[str, Any] Keyword arguments to pass to the snake_case method
monkeypatch pytest.MonkeyPatch pytest.MonkeyPatch fixture for patching

Raises:

Example:

from apathetic_utils import create_mock_superclass_test
import logging
import pytest

def test_mixin_calls_parent(monkeypatch):
    create_mock_superclass_test(
        MyMixin,
        logging.Logger,
        "add_filter",
        "addFilter",
        (logging.Filter(),),
        {},
        monkeypatch
    )

patch_everywhere

patch_everywhere(
    mp: pytest.MonkeyPatch,
    mod_env: ModuleType | Any,
    func_name: str,
    replacement_func: Callable[..., object],
    *,
    package_prefix: str | Sequence[str],
    stitch_hints: set[str] | None = None,
    create_if_missing: bool = False,
    caller_func_name: str | None = None
) -> None

Replace a function everywhere it was imported.

Works in both package and stitched single-file runtimes. Walks sys.modules once and handles:

Parameters:

Parameter Type Description
mp pytest.MonkeyPatch pytest.MonkeyPatch instance to use for patching
mod_env ModuleType \| Any Module or object containing the function to patch
func_name str Name of the function to patch
replacement_func Callable[..., object] Function to replace the original with
package_prefix str \| Sequence[str] Package name prefix(es) to filter modules. Can be a single string (e.g., “apathetic_utils”) or a sequence of strings (e.g., [“apathetic_utils”, “my_package”]) to patch across multiple packages.
stitch_hints set[str] \| None Set of path hints to identify stitched modules. Defaults to {"/dist/", "stitched"}. When providing custom hints, you must be certain of the path attributes of your stitched file, as this uses substring matching on the module’s __file__ path. This is a heuristic fallback when identity checks fail (e.g., when modules are reloaded).
create_if_missing bool If True, create the attribute if it doesn’t exist. If False (default), raise TypeError if the function doesn’t exist.
caller_func_name str \| None If provided, only patch __globals__ for this specific function to handle direct calls. If None (default), patch __globals__ for all functions in stitched modules that reference the original function.

Raises:

Example:

from apathetic_utils import patch_everywhere
import pytest
import my_module

def test_patch_function(monkeypatch):
    def mock_func():
        return "mocked"
    
    patch_everywhere(
        monkeypatch,
        my_module,
        "original_func",
        mock_func,
        package_prefix="my_module"
    )

Constants

CI_ENV_VARS

CI_ENV_VARS: tuple[str, ...]

Tuple of CI environment variable names that indicate a CI environment.

Default values:

Example:

from apathetic_utils import CI_ENV_VARS

print(CI_ENV_VARS)  # ("CI", "GITHUB_ACTIONS", "GIT_TAG", "GITHUB_REF")

Namespace Class

All utilities are also available through the apathetic_utils namespace class:

from apathetic_utils import apathetic_utils

# Use via namespace
config = apathetic_utils.load_jsonc(Path("config.jsonc"))
is_ci = apathetic_utils.is_ci()
mode = apathetic_utils.detect_runtime_mode("my_package")

This is useful when embedding the library as a single-file script to minimize global namespace pollution.