Metadata-Version: 2.5
Name: importpatch
Version: 0.2.0
Summary: Post-import hooks: run a function on a module right after it is imported, registered by entry point
Project-URL: Homepage, https://github.com/jose-pr/importpatch/
Project-URL: Documentation, https://jose-pr.github.io/importpatch/
Project-URL: Issues, https://github.com/jose-pr/importpatch/issues
Author-email: jose-pr <jose-pr@coqui.dev>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: black; extra == 'dev'
Requires-Dist: build; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs; extra == 'docs'
Requires-Dist: mkdocs-material; extra == 'docs'
Requires-Dist: mkdocstrings[python]; extra == 'docs'
Description-Content-Type: text/markdown

# importpatch

[![Version](https://img.shields.io/pypi/v/importpatch.svg)](https://pypi.org/project/importpatch/)
[![Python versions](https://img.shields.io/pypi/pyversions/importpatch.svg)](https://pypi.org/project/importpatch/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jose-pr/importpatch/blob/master/LICENSE)
[![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/importpatch/)
[![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/importpatch/test.yml)](https://github.com/jose-pr/importpatch/actions/workflows/test.yml)

**Post-import hooks that a package registers just by being installed.** A
distribution declares "when `requests` is imported, call `mypkg.hooks.patch`"
as an entry point, and every program in that environment gets the patch the
moment it imports `requests`, without importing `mypkg` itself. **A hook that
fails is logged and skipped; it never breaks the import it patches.**
Documentation: <https://jose-pr.github.io/importpatch/>.

## Features

- **Registration by entry point** — the `importpatch` group: name = the module to
  patch, value = the patcher's dotted name. Uninstalling the package removes its hooks.
- **Active on install** — the wheel ships `importpatch.pth`, so the hook runs at the
  start of every interpreter in the environment; `IMPORTPATCH_DISABLE=1` turns it off.
- **Works with any loader** — the module is found by Python's own finders and its loader
  is wrapped, so packages, zip archives and anything else importable are patched.
- **Already imported is fine** — a module imported before its hook was registered is
  patched at registration.
- **Fail-safe** — a hook that is missing or raises is logged on the `importpatch` logger
  and skipped.
- **Cheap** — no dependencies, and entry points are read without `importlib.metadata`,
  which would cost every interpreter start tens of milliseconds.

## Installation

```bash
pip install importpatch
```

There are no extras.

## Quick start

A package that gives every `requests` call a default timeout declares, in its
`pyproject.toml`:

```toml
[project.entry-points.importpatch]
requests = "mypkg.hooks.patch_requests"
```

and in `mypkg/hooks.py`:

```python
def patch_requests(requests):
    original = requests.Session.request

    def request(self, method, url, **kwargs):
        kwargs.setdefault("timeout", 30)
        return original(self, method, url, **kwargs)

    requests.Session.request = request
```

With both installed, every program in the environment that imports `requests`
gets the patched `Session.request`. Registering in-process needs no packaging:

```python
import importpatch

importpatch.add("json", "mypkg.hooks.patch_json")  # runs at once if json is loaded
```

## API overview

| Name | Purpose |
| --- | --- |
| `importpatch.add(target, patcher)` | run `patcher` on `target` after it is imported (at once, if it is) |
| `importpatch.remove(target, patcher=None)` | stop running one or every patcher on future imports |
| `importpatch.patches()` | the registry: target → patchers |
| `importpatch.bootstrap()` | install the hook and read entry points and the configuration file (`importpatch.pth` calls it) |

## Configuration file

`pyimportpatch.cfg` adds hooks that do not come from a package, and switches
hooks off. It has one section per module to patch, then one patcher per line:

```ini
# lines starting with # or ; are comments
[json]
mypkg.hooks.patch_json

[requests]
otherpkg.hooks.patch_requests = false
```

A patcher alone, or `= true`, enables it; `= false` disables it, whether a package's
entry point, an earlier file or the same file registered it (booleans: `1`/`0`,
`true`/`false`, `yes`/`no`, `on`/`off`).

A patcher is a dotted name. When it names a function, the function is called with
the imported module: `patch_json(module)`. When it names a module, that module's
`importpatch(name, module)` is called with the imported module's name and the
module, so one module can patch several targets. The entry-point spelling
`pkg.mod:function` is the same patcher as `pkg.mod.function`.

Two files are read and merged, after the entry points, each overriding what came
before it:

| File | Windows | Elsewhere | Override |
| --- | --- | --- | --- |
| system | `%PROGRAMDATA%\pyimportpatch.cfg` | `/etc/pyimportpatch.cfg` | `IMPORTPATCH_SYSTEM_CONFIG` |
| user | `%APPDATA%\pyimportpatch.cfg` | `$XDG_CONFIG_HOME/pyimportpatch.cfg` (default `~/.config`) | `IMPORTPATCH_CONFIG` |

Each variable may list several files separated by `os.pathsep` (`:`, or `;` on
Windows), applied in order. The user files are not read under `python -I` or `-s`; nothing is read from the
working directory. A line that cannot be read is logged with the file and line
number, and skipped.

## Migrating from 0.1

- There is no `python -m importpatch enable` / `disable`: installing activates the hook,
  uninstalling removes it, and `IMPORTPATCH_DISABLE=1` turns it off. Installing 0.2
  replaces the `importpatch.pth` that `enable` wrote.
- `.pymodpatch.json` in site-packages, `~` or the working directory is no longer read;
  move its entries to `pyimportpatch.cfg` (one `[module]` section each), or declare
  entry points.
- `rm()` is `remove()`; `MODULES_TO_PATCH` is private (use `patches()`).
- A hook that raises or cannot be imported is logged, instead of being ignored silently
  or, for a missing package, failing the import.

## Development

In a virtual environment on Python 3.9 or later:

```bash
python -m pip install -e ".[dev]"
python -m pytest -q -W error
```

The install tests build the wheel and run real interpreters in a temporary
virtual environment.

### Releasing

This project follows [Semantic Versioning](https://semver.org/) and keeps a
[`CHANGELOG.md`](https://github.com/jose-pr/importpatch/blob/master/CHANGELOG.md).
Pushing a tag matching `v*` triggers the release workflow: test gate → build (checking
the tag names the version built) → a strict docs build as a gate → GitHub release →
publish. The release workflow never deploys the docs site itself: for a final release
its last job dispatches the docs workflow at the tag, which owns every Pages deploy.

## License

MIT — see [LICENSE](https://github.com/jose-pr/importpatch/blob/master/LICENSE).
