compat.py
Overview
The `compat.py` file serves as a compatibility layer to facilitate smooth interaction with the `pluggy` plugin system, particularly when dealing with path-related hooks that historically accepted legacy path arguments. It provides a wrapper class, `PathAwareHookProxy`, that transparently manages the coexistence of modern `pathlib.Path` objects and legacy path representations (`LEGACY_PATH`), ensuring backward compatibility while encouraging usage of modern APIs.
This module addresses the problem where certain plugin hooks accept two arguments representing the same filesystem location: one as a `pathlib.Path` object and another as a legacy string/path representation. The compatibility logic enforces consistency between these arguments and provides warnings when legacy arguments are used, helping plugin authors migrate to the new style.
Detailed Documentation
Constants and Mappings
imply_paths_hooks: Mapping[str, tuple[str, str]]
Type:
Mapping[str, tuple[str, str]]Description:
A mapping that defines plugin hook names to pairs of argument names representing the same filesystem path in two formats:The first element is the argument name that expects a modern
pathlib.Pathinstance.The second element is the argument name that accepts the legacy path argument (
LEGACY_PATH).
Use case:
This dictionary is used internally by thePathAwareHookProxyto determine which hooks require special compatibility handling for path arguments.Example entry:
"pytest_ignore_collect": ("collection_path", "path")Here, the hook
pytest_ignore_collectexpects two arguments that represent the path:collection_path(modern) andpath(legacy).
Functions
_check_path(path: Path, fspath: LEGACY_PATH) -> None
Parameters:
path(Path): The modernpathlib.Pathobject.fspath(LEGACY_PATH): The legacy path representation (usually a string orpy.path.local).
Return value:
NoneRaises:
ValueErrorifpathandfspathdo not represent the same filesystem location.
Description:
Validates that the modernpathlib.Pathand legacy path arguments are equivalent. If not, it raises aValueErrorindicating the mismatch. This ensures consistency when both arguments are provided to a hook.Usage example:
_check_path(Path("/tmp/example"), "/tmp/example") # No exception raised, paths are equivalent _check_path(Path("/tmp/example"), "/tmp/different") # Raises ValueError
Classes
PathAwareHookProxy
A proxy wrapper around `pluggy.HookRelay` to support legacy and modern path argument compatibility in plugin hooks.
Purpose
`PathAwareHookProxy` intercepts calls to plugin hooks that accept both legacy and modern path arguments, enforcing consistency and issuing deprecation warnings when legacy arguments are used. It allows existing plugins that use legacy path arguments to continue functioning while new plugins can use modern `pathlib.Path` arguments.
Initialization
PathAwareHookProxy(hook_relay: pluggy.HookRelay) -> None
Parameters:
hook_relay(pluggy.HookRelay): The original pluggy hook relay object to wrap.
Behavior:
Stores the providedhook_relayand will proxy attribute access to it, wrapping specific hooks as needed.
Methods
__dir__(self) -> list[str]Returns the list of attributes available on the underlying
hook_relay.
__getattr__(self, key: str) -> pluggy.HookCallerIntercepts attribute access for hook callers by name.
If the hook name
keyis inimply_paths_hooks, it returns a wrapped version of the hook caller that:Extracts legacy and modern path arguments from the call.
Issues a deprecation warning if the legacy argument is used.
Checks consistency between legacy and modern paths.
Converts one argument to the other if only one is provided.
Calls the original hook with both arguments correctly set.
For hooks not in
imply_paths_hooks, returns the original hook caller unchanged.
Usage example:
import pluggy
from compat import PathAwareHookProxy
hook_relay = pluggy.HookRelay("pytest")
proxy = PathAwareHookProxy(hook_relay)
# Accessing a wrapped hook
result = proxy.pytest_ignore_collect(collection_path=Path("/tmp"), path="/tmp")
# This will check and warn if legacy 'path' is used, ensure consistency, and call the original hook.
Important Implementation Details
Uses
functools.wrapsto preserve metadata of the original hook caller when wrapping.Stores wrapped hooks in the instance dictionary to avoid repeated wrapping.
Relies on the
imply_paths_hooksdictionary to determine which hooks require special path argument handling.Emits deprecation warnings with a stack level of 2 to point plugin authors to their call site.
Implementation Details and Algorithms
Compatibility Logic:
The core algorithm is in the__getattr__method ofPathAwareHookProxy. When a hook is accessed that supports both modern and legacy path arguments, it dynamically creates a wrapper function (fixed_hook) that:Pops out modern and legacy path arguments from the keyword arguments.
Warns if the legacy argument is used.
Checks for consistency if both arguments are provided.
Converts one argument to the other if only one is provided.
Re-inserts both arguments into the keyword arguments.
Invokes the original hook with the updated arguments.
This ensures that downstream plugins receive both representations, preserving backward compatibility while encouraging migration.
Interactions with Other System Components
Pluggy Integration:
PathAwareHookProxywrapspluggy.HookRelayobjects from thepluggyplugin management system. Pluggy is used for dynamically managing plugin hooks and hook calls.Compatibility Modules:
The file imports constants and helpers from sibling modules:LEGACY_PATHandlegacy_pathfrom..compat: Represent and convert legacy path types.HOOK_LEGACY_PATH_ARGfrom..deprecated: A deprecation warning message template.
Plugin Hooks:
This module is specifically designed for Pytest hooks (e.g.,pytest_ignore_collect,pytest_collect_file), which historically passed paths in two forms. It helps Pytest maintain compatibility with older plugins while moving towards modern APIs.
Visual Diagram
classDiagram
class PathAwareHookProxy {
- _hook_relay: pluggy.HookRelay
+ __init__(hook_relay)
+ __dir__() list~str~
+ __getattr__(key) pluggy.HookCaller
}
class pluggy.HookRelay {
+ __getattr__(key) pluggy.HookCaller
}
class pluggy.HookCaller {
+ __call__(**kwargs) Any
}
PathAwareHookProxy --> pluggy.HookRelay : wraps
PathAwareHookProxy --> pluggy.HookCaller : returns wrapped or original
Summary
The `compat.py` file provides a crucial compatibility layer that bridges old and new path argument conventions in plugin hooks managed by pluggy. Its `PathAwareHookProxy` enables smooth coexistence of legacy plugins with modern Python path APIs, maintaining system stability during API evolution. This module is key in easing migration while enforcing argument correctness and encouraging adoption of `pathlib.Path`.
Example Usage Snippet
import pluggy
from compat import PathAwareHookProxy
from pathlib import Path
# Assume hook_relay is obtained from pluggy PluginManager or similar
hook_relay = pluggy.HookRelay("pytest")
# Wrap with compatibility proxy
hook_proxy = PathAwareHookProxy(hook_relay)
# Call a hook that expects path arguments
results = hook_proxy.pytest_collect_file(
file_path=Path("/path/to/file"),
path="/path/to/file" # legacy argument, will emit warning
)
This will check that both paths are equivalent, warn about the deprecated legacy argument, and call the underlying hook with consistent arguments.