tmpkit 1.0.1__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tmpkit-1.0.1 → tmpkit-1.1.0}/.github/ISSUE_TEMPLATE/bug_report.yml +1 -1
- {tmpkit-1.0.1 → tmpkit-1.1.0}/.github/dependabot.yml +2 -2
- {tmpkit-1.0.1 → tmpkit-1.1.0}/.github/workflows/release.yml +14 -1
- {tmpkit-1.0.1 → tmpkit-1.1.0}/.pre-commit-config.yaml +2 -2
- {tmpkit-1.0.1 → tmpkit-1.1.0}/CHANGELOG.md +36 -14
- {tmpkit-1.0.1 → tmpkit-1.1.0}/CONTRIBUTING.md +4 -3
- {tmpkit-1.0.1 → tmpkit-1.1.0}/LICENSE +1 -1
- {tmpkit-1.0.1 → tmpkit-1.1.0}/PKG-INFO +33 -19
- {tmpkit-1.0.1 → tmpkit-1.1.0}/README.md +30 -16
- {tmpkit-1.0.1 → tmpkit-1.1.0}/SECURITY.md +1 -1
- {tmpkit-1.0.1 → tmpkit-1.1.0}/pyproject.toml +22 -3
- tmpkit-1.1.0/tests/__init__.py +1 -0
- tmpkit-1.1.0/tests/e2e/__init__.py +1 -0
- tmpkit-1.1.0/tests/integration/__init__.py +1 -0
- tmpkit-1.1.0/tests/unit/__init__.py +1 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_atomic.py +134 -5
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_decorators.py +47 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_error_paths.py +49 -6
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_keep.py +9 -8
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_registry.py +2 -3
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_smoke.py +1 -1
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_sync_dir.py +44 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_sync_file.py +34 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_windows.py +0 -40
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tmpkit/__init__.py +5 -2
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tmpkit/_async.py +24 -10
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tmpkit/_atomic.py +102 -34
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tmpkit/_decorators.py +38 -6
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tmpkit/_registry.py +4 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tmpkit/_sync.py +62 -34
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tmpkit/_types.py +2 -0
- tmpkit-1.0.1/tests/conftest.py +0 -26
- {tmpkit-1.0.1 → tmpkit-1.1.0}/.github/FUNDING.yml +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/.github/workflows/ci.yml +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/.gitignore +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/CODE_OF_CONDUCT.md +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/e2e/test_real_cleanup.py +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/integration/test_concurrent.py +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/integration/test_subprocess.py +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_async_dir.py +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_async_file.py +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_config.py +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tests/unit/test_types.py +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tmpkit/_config.py +0 -0
- {tmpkit-1.0.1 → tmpkit-1.1.0}/tmpkit/py.typed +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
version: 2
|
|
2
2
|
updates:
|
|
3
3
|
- package-ecosystem: pip
|
|
4
|
-
directory: /
|
|
4
|
+
directory: "/"
|
|
5
5
|
schedule:
|
|
6
6
|
interval: weekly
|
|
7
7
|
day: monday
|
|
@@ -14,7 +14,7 @@ updates:
|
|
|
14
14
|
include: scope
|
|
15
15
|
|
|
16
16
|
- package-ecosystem: github-actions
|
|
17
|
-
directory: /
|
|
17
|
+
directory: "/"
|
|
18
18
|
schedule:
|
|
19
19
|
interval: weekly
|
|
20
20
|
day: monday
|
|
@@ -94,6 +94,19 @@ jobs:
|
|
|
94
94
|
- name: Install from PyPI
|
|
95
95
|
run: |
|
|
96
96
|
VERSION="${GITHUB_REF_NAME#v}"
|
|
97
|
-
|
|
97
|
+
# PyPI indexing can lag behind publication; retry until available.
|
|
98
|
+
for i in $(seq 1 12); do
|
|
99
|
+
if pip install "tmpkit==${VERSION}" 2>/dev/null; then
|
|
100
|
+
echo "Installed tmpkit==${VERSION} after ${i} attempt(s)."
|
|
101
|
+
break
|
|
102
|
+
fi
|
|
103
|
+
echo "Attempt ${i}: tmpkit==${VERSION} not yet indexed on PyPI. Retrying in 15s..."
|
|
104
|
+
sleep 15
|
|
105
|
+
done
|
|
106
|
+
INSTALLED="$(python -c 'import importlib.metadata; print(importlib.metadata.version("tmpkit"))' 2>/dev/null)"
|
|
107
|
+
[ "$INSTALLED" = "$VERSION" ] || {
|
|
108
|
+
echo "ERROR: expected tmpkit==${VERSION}, got '${INSTALLED:-not installed}' after 3 minutes."
|
|
109
|
+
exit 1
|
|
110
|
+
}
|
|
98
111
|
- name: Smoke test
|
|
99
112
|
run: python -c "from tmpkit import temp_file, temp_dir; print('ok')"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
repos:
|
|
2
2
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
3
|
-
rev: v0.
|
|
3
|
+
rev: v0.16.1
|
|
4
4
|
hooks:
|
|
5
5
|
- id: ruff
|
|
6
6
|
args: [--fix]
|
|
7
7
|
- id: ruff-format
|
|
8
8
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
9
|
-
rev:
|
|
9
|
+
rev: v2.3.1
|
|
10
10
|
hooks:
|
|
11
11
|
- id: mypy
|
|
12
12
|
additional_dependencies: []
|
|
@@ -5,31 +5,53 @@ All notable changes to tmpkit will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [1.1.0] - 2026-10-07
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
-
|
|
12
|
+
- `keep=True` and `cleanup_hook=` parameters on `atomic_write()` (sync and async).
|
|
13
|
+
- `cleanup_hook=` parameter on `@temp_dir()` and `@temp_file()` decorators.
|
|
14
|
+
- `TempRecord` and `TempFileLike` are now exported from `tmpkit`.
|
|
15
|
+
- `atomic_write()` fsyncs the destination directory after `os.replace()`
|
|
16
|
+
for rename durability (best-effort, no-op on Windows).
|
|
13
17
|
|
|
14
18
|
### Changed
|
|
15
19
|
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
20
|
+
- `read`/`write`/`seek`/`tell`/`.path`/`__fspath__` before `__enter__` now
|
|
21
|
+
raise `RuntimeError` instead of `AssertionError`.
|
|
22
|
+
- `@temp_file()` on a class now raises `TypeError` instead of silently
|
|
23
|
+
producing a broken function.
|
|
24
|
+
- Class decoration with `@temp_dir()` now skips `staticmethod` and
|
|
25
|
+
`classmethod` members named `test_*` (they have no `self`).
|
|
26
|
+
- Tests are now type-checked under `mypy --strict` with relaxed per-module
|
|
27
|
+
rules (`[[tool.mypy.overrides]]`); `tests/` is now a package.
|
|
28
|
+
- Dev dependency floors bumped (`ruff>=0.16.0`, `mypy>=2.3.0`) and
|
|
29
|
+
pre-commit hook revisions aligned (`ruff v0.16.1`, `mypy v2.3.1`).
|
|
25
30
|
|
|
26
31
|
### Fixed
|
|
27
32
|
|
|
28
|
-
-
|
|
33
|
+
- Release workflow: the PyPI verify job used `pip show tmpkit==X.Y.Z`,
|
|
34
|
+
which always failed (`pip show` doesn't accept version specifiers).
|
|
35
|
+
- Body exceptions are no longer masked by cleanup failures: a failing
|
|
36
|
+
`close()`/`flush()`/`fsync()`/`rmtree` during exception propagation
|
|
37
|
+
no longer replaces the original error.
|
|
38
|
+
- `temp_dir(cwd=True)`: a non-`OSError` failure while restoring the
|
|
39
|
+
working directory no longer leaks the temp directory.
|
|
40
|
+
- `atomic_write()` raises `IsADirectoryError` early when `dest` is an
|
|
41
|
+
existing directory.
|
|
42
|
+
- `__repr__` now reports `kept` for temps kept via the close/flush
|
|
43
|
+
failure path.
|
|
44
|
+
- `__enter__` re-entry fully resets internal state (`_path`, `_file`).
|
|
45
|
+
- README: fixed the `temp_dir` `.keep()` example (it called `.keep()`
|
|
46
|
+
after the `with` block, which raises `RuntimeError`), corrected
|
|
47
|
+
`async_temp_file()` naming, and documented the process-global nature
|
|
48
|
+
of `cwd=True`.
|
|
29
49
|
|
|
30
|
-
###
|
|
50
|
+
### Removed
|
|
31
51
|
|
|
32
|
-
-
|
|
52
|
+
- `tests/conftest.py`: dropped the unused `assert_no_temps_left` fixture.
|
|
53
|
+
- `TestWindowsNameMock` tests, which mocked `os.name` to `"nt"` — a no-op
|
|
54
|
+
on Windows (the only platform they ran on).
|
|
33
55
|
|
|
34
56
|
## [1.0.1] - 2025-01-24
|
|
35
57
|
|
|
@@ -38,9 +38,11 @@ Thank you for your interest in contributing to tmpkit! This document describes t
|
|
|
38
38
|
- **Type checker**: [mypy](https://mypy-lang.org/) with `--strict`.
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
mypy --strict tmpkit/
|
|
41
|
+
mypy --strict tmpkit/ tests/
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
Tests are checked with relaxed rules (see `[[tool.mypy.overrides]]` in `pyproject.toml`) since they exercise decorator-injected arguments and private internals.
|
|
45
|
+
|
|
44
46
|
- **Python**: Target 3.11+ syntax. Use `from __future__ import annotations` in all modules.
|
|
45
47
|
- **Imports**: Sorted by ruff (isort-compatible). Stdlib first, then third-party, then local.
|
|
46
48
|
|
|
@@ -108,8 +110,7 @@ tmpkit/
|
|
|
108
110
|
│ ├── unit/ # Unit tests
|
|
109
111
|
│ ├── integration/ # Integration tests
|
|
110
112
|
│ └── e2e/ # End-to-end tests
|
|
111
|
-
|
|
112
|
-
└── ref/ # Internal design docs (not shipped)
|
|
113
|
+
└── pyproject.toml # Build config, tool config
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
## Reporting Issues
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tmpkit
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes, async support, and zero dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/MathiasPaulenko/tmpkit
|
|
6
6
|
Project-URL: Repository, https://github.com/MathiasPaulenko/tmpkit
|
|
@@ -24,11 +24,11 @@ Classifier: Typing :: Typed
|
|
|
24
24
|
Requires-Python: >=3.11
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: build>=1.0; extra == 'dev'
|
|
27
|
-
Requires-Dist: mypy>=
|
|
27
|
+
Requires-Dist: mypy>=2.3.0; extra == 'dev'
|
|
28
28
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
29
29
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
30
30
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
-
Requires-Dist: ruff>=0.
|
|
31
|
+
Requires-Dist: ruff>=0.16.0; extra == 'dev'
|
|
32
32
|
Description-Content-Type: text/markdown
|
|
33
33
|
|
|
34
34
|
# tmpkit
|
|
@@ -39,7 +39,8 @@ Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes,
|
|
|
39
39
|
[](https://pypi.org/project/tmpkit/)
|
|
40
40
|
[](https://pypi.org/project/tmpkit/)
|
|
41
41
|
[](https://github.com/MathiasPaulenko/tmpkit/blob/main/LICENSE)
|
|
42
|
-
[](https://codecov.io/gh/MathiasPaulenko/tmpkit)
|
|
43
|
+
[](https://pypi.org/project/tmpkit/)
|
|
43
44
|
|
|
44
45
|
---
|
|
45
46
|
|
|
@@ -70,7 +71,7 @@ Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes,
|
|
|
70
71
|
|
|
71
72
|
## Why tmpkit?
|
|
72
73
|
|
|
73
|
-
Python's `tempfile` gives you the pieces but forces you to write cleanup boilerplate every time. tmpkit wraps it in ergonomic context managers that **
|
|
74
|
+
Python's `tempfile` gives you the pieces but forces you to write cleanup boilerplate every time. tmpkit wraps it in ergonomic context managers that **always attempt cleanup** on exit — with features nobody else offers.
|
|
74
75
|
|
|
75
76
|
```python
|
|
76
77
|
# stdlib — verbose, easy to forget cleanup
|
|
@@ -82,7 +83,7 @@ try:
|
|
|
82
83
|
finally:
|
|
83
84
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
84
85
|
|
|
85
|
-
# tmpkit — one line,
|
|
86
|
+
# tmpkit — one line, cleans up on exit
|
|
86
87
|
from tmpkit import temp_file
|
|
87
88
|
with temp_file(suffix=".csv") as f:
|
|
88
89
|
f.write(data)
|
|
@@ -113,7 +114,7 @@ This installs `pytest`, `pytest-asyncio`, `pytest-cov`, `ruff`, `mypy`, and `bui
|
|
|
113
114
|
```python
|
|
114
115
|
from tmpkit import temp_file, temp_dir, atomic_write, async_temp_file
|
|
115
116
|
|
|
116
|
-
# Temp file
|
|
117
|
+
# Temp file (default mode is "w+b" — binary; use mode="w+" for str)
|
|
117
118
|
with temp_file(suffix=".csv", prefix="myapp_") as f:
|
|
118
119
|
f.write(data)
|
|
119
120
|
# deleted on exit
|
|
@@ -143,7 +144,7 @@ with temp_file(dest="output.csv") as f:
|
|
|
143
144
|
# Async
|
|
144
145
|
async def main() -> None:
|
|
145
146
|
async with async_temp_file(suffix=".json") as f:
|
|
146
|
-
await f.write(data)
|
|
147
|
+
await f.write(data) # bytes by default; mode="w+" for str
|
|
147
148
|
```
|
|
148
149
|
|
|
149
150
|
---
|
|
@@ -163,7 +164,7 @@ async def main() -> None:
|
|
|
163
164
|
- **`@temp_dir()` / `@temp_file()` decorators** — inject temps into functions and test classes.
|
|
164
165
|
- **Close without delete** — file survives `close()`, deleted on context exit (Windows subprocess friendly).
|
|
165
166
|
- **`.path` attribute** — `Path` object, no more `Path(f.name)` boilerplate.
|
|
166
|
-
- **Async support** — `async with
|
|
167
|
+
- **Async support** — `async with async_temp_file() as f:` with async I/O methods.
|
|
167
168
|
- **Windows-safe by default** — no `O_TEMPORARY` lock, `ignore_cleanup_errors=True`.
|
|
168
169
|
- **Zero dependencies** — stdlib only.
|
|
169
170
|
|
|
@@ -182,7 +183,7 @@ with temp_file(
|
|
|
182
183
|
dir: str | Path | None = None, # parent directory
|
|
183
184
|
mode: str = "w+b", # open mode
|
|
184
185
|
content: str | bytes | None = None, # pre-populate
|
|
185
|
-
dest: str | Path | None = None, # move here on success
|
|
186
|
+
dest: str | Path | None = None, # move here on success (parent must exist at exit)
|
|
186
187
|
keep: bool = False, # always keep
|
|
187
188
|
keep_on_error: bool = False, # keep only on exception
|
|
188
189
|
ignore_cleanup_errors: bool = True,
|
|
@@ -225,12 +226,12 @@ with temp_dir(
|
|
|
225
226
|
td = temp_dir()
|
|
226
227
|
with td as d:
|
|
227
228
|
(d / "file.txt").write_text("hello")
|
|
228
|
-
td.keep()
|
|
229
|
+
td.keep() # runtime decision to keep (must be inside the block)
|
|
229
230
|
```
|
|
230
231
|
|
|
231
|
-
**Returns:** A `Path` object (the temp directory path) with `/` operator support. To call `.keep()`, use the context manager object directly (see example above)
|
|
232
|
+
**Returns:** A `Path` object (the temp directory path) with `/` operator support. To call `.keep()`, use the context manager object directly (see example above) — it must be called **inside** the `with` block; calling it after exit raises `RuntimeError`.
|
|
232
233
|
|
|
233
|
-
**`cwd=True`:** Changes the working directory to the temp dir on `__enter__`, restores the original on `__exit__`.
|
|
234
|
+
**`cwd=True`:** Changes the working directory to the temp dir on `__enter__`, restores the original on `__exit__`. Warning: `os.chdir` is process-global — do not use `cwd=True` from multiple threads or concurrent async tasks.
|
|
234
235
|
|
|
235
236
|
### `atomic_write()`
|
|
236
237
|
|
|
@@ -245,8 +246,10 @@ with atomic_write(
|
|
|
245
246
|
prefix: str | None = None,
|
|
246
247
|
suffix: str = ".tmp",
|
|
247
248
|
fsync: bool = True, # fsync before rename
|
|
249
|
+
keep: bool = False, # keep temp, dest untouched
|
|
248
250
|
keep_on_error: bool = False,
|
|
249
251
|
ignore_cleanup_errors: bool = True,
|
|
252
|
+
cleanup_hook: Callable[[Path], None] | None = None,
|
|
250
253
|
) as f:
|
|
251
254
|
f.write(data)
|
|
252
255
|
# on success: atomically renamed to dest
|
|
@@ -255,6 +258,10 @@ with atomic_write(
|
|
|
255
258
|
|
|
256
259
|
Writes to a temp file in `dest`'s parent directory, then atomically replaces `dest` via `os.replace()` on success. On error, the temp is cleaned up and `dest` is left untouched.
|
|
257
260
|
|
|
261
|
+
**Note:** with `keep=True`, `.keep()`, `keep_on_error=True` + exception, or `DEBUG=1`, the temp file is kept and **the replace is skipped** — `dest` is left untouched even on success.
|
|
262
|
+
|
|
263
|
+
**Validation on enter:** raises `IsADirectoryError` if `dest` is an existing directory, `FileNotFoundError` if `dest`'s parent doesn't exist, and `NotADirectoryError` if the parent exists but isn't a directory — before writing anything.
|
|
264
|
+
|
|
258
265
|
### `@temp_dir()` Decorator
|
|
259
266
|
|
|
260
267
|
```python
|
|
@@ -281,9 +288,9 @@ class TestMyApp:
|
|
|
281
288
|
assert self.tmpdir.exists()
|
|
282
289
|
```
|
|
283
290
|
|
|
284
|
-
**Decorator defaults:** `cwd=True` (unlike the context manager where `cwd=False` by default).
|
|
291
|
+
**Decorator defaults:** `cwd=True` (unlike the context manager where `cwd=False` by default). Accepts the same parameters as `temp_dir()`, including `cleanup_hook`. Only plain functions and classes — decorating a single method injects the temp path in place of `self`.
|
|
285
292
|
|
|
286
|
-
**Class decoration:** Each method starting with `test_` is wrapped. The temp dir is available as `self.tmpdir`. Works with both sync and async test methods.
|
|
293
|
+
**Class decoration:** Each method starting with `test_` is wrapped (`staticmethod`/`classmethod` members are skipped). The temp dir is available as `self.tmpdir`. Works with both sync and async test methods.
|
|
287
294
|
|
|
288
295
|
### `@temp_file()` Decorator
|
|
289
296
|
|
|
@@ -304,7 +311,7 @@ async def process_async(f, data: str) -> None:
|
|
|
304
311
|
await f.write(data)
|
|
305
312
|
```
|
|
306
313
|
|
|
307
|
-
The temp file object is injected as the **first positional argument**.
|
|
314
|
+
The temp file object is injected as the **first positional argument**. Accepts the same parameters as `temp_file()`, including `cleanup_hook`. Plain functions only — decorating a class raises `TypeError`.
|
|
308
315
|
|
|
309
316
|
### `temp_registry`
|
|
310
317
|
|
|
@@ -340,7 +347,7 @@ temp_registry.clear_history()
|
|
|
340
347
|
temp_registry.disable()
|
|
341
348
|
```
|
|
342
349
|
|
|
343
|
-
**`TempRecord`
|
|
350
|
+
**`TempRecord`** is importable for type annotations: `from tmpkit import TempRecord`. Fields:
|
|
344
351
|
|
|
345
352
|
| Field | Type | Description |
|
|
346
353
|
| --- | --- | --- |
|
|
@@ -352,6 +359,8 @@ temp_registry.disable()
|
|
|
352
359
|
|
|
353
360
|
**Thread-safe:** All operations are protected by `threading.Lock`.
|
|
354
361
|
|
|
362
|
+
**Note:** records accumulate for the lifetime of the process. In long-running applications, call `temp_registry.clear_history()` periodically (or `reset()`) to bound memory usage.
|
|
363
|
+
|
|
355
364
|
### Async API
|
|
356
365
|
|
|
357
366
|
All sync APIs have async counterparts with identical parameters:
|
|
@@ -384,7 +393,7 @@ Async file objects support `await f.read()`, `await f.write()`, `await f.seek()`
|
|
|
384
393
|
| `DEBUG` | `1` | Same as `TMPKIT_DEBUG=1` (fallback) |
|
|
385
394
|
| `TMPKIT_REGISTRY` | `1` | Enable `temp_registry` at import time |
|
|
386
395
|
|
|
387
|
-
`TMPKIT_DEBUG` takes precedence over `DEBUG`.
|
|
396
|
+
`TMPKIT_DEBUG` takes precedence over `DEBUG`. Since `DEBUG` is a common variable name in other tools, you can set `TMPKIT_DEBUG=0` to explicitly disable keep-all even when `DEBUG=1` is present.
|
|
388
397
|
|
|
389
398
|
---
|
|
390
399
|
|
|
@@ -431,6 +440,11 @@ with temp_file(cleanup_hook=my_hook) as f:
|
|
|
431
440
|
f.write(data)
|
|
432
441
|
# hook is called, then standard cleanup runs
|
|
433
442
|
|
|
443
|
+
# Hook also runs when the temp is moved via dest=
|
|
444
|
+
with temp_file(dest="output.csv", cleanup_hook=my_hook) as f:
|
|
445
|
+
f.write(data)
|
|
446
|
+
# hook is called, then temp is moved to output.csv
|
|
447
|
+
|
|
434
448
|
# Hook is called even on exceptions
|
|
435
449
|
with temp_file(cleanup_hook=my_hook) as f:
|
|
436
450
|
raise RuntimeError("oops")
|
|
@@ -503,4 +517,4 @@ See [CHANGELOG.md](CHANGELOG.md) for a full list of changes.
|
|
|
503
517
|
|
|
504
518
|
## License
|
|
505
519
|
|
|
506
|
-
[MIT](LICENSE) — Copyright (c) 2025 Mathias Paulenko
|
|
520
|
+
[MIT](LICENSE) — Copyright (c) 2025-2026 Mathias Paulenko
|
|
@@ -6,7 +6,8 @@ Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes,
|
|
|
6
6
|
[](https://pypi.org/project/tmpkit/)
|
|
7
7
|
[](https://pypi.org/project/tmpkit/)
|
|
8
8
|
[](https://github.com/MathiasPaulenko/tmpkit/blob/main/LICENSE)
|
|
9
|
-
[](https://codecov.io/gh/MathiasPaulenko/tmpkit)
|
|
10
|
+
[](https://pypi.org/project/tmpkit/)
|
|
10
11
|
|
|
11
12
|
---
|
|
12
13
|
|
|
@@ -37,7 +38,7 @@ Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes,
|
|
|
37
38
|
|
|
38
39
|
## Why tmpkit?
|
|
39
40
|
|
|
40
|
-
Python's `tempfile` gives you the pieces but forces you to write cleanup boilerplate every time. tmpkit wraps it in ergonomic context managers that **
|
|
41
|
+
Python's `tempfile` gives you the pieces but forces you to write cleanup boilerplate every time. tmpkit wraps it in ergonomic context managers that **always attempt cleanup** on exit — with features nobody else offers.
|
|
41
42
|
|
|
42
43
|
```python
|
|
43
44
|
# stdlib — verbose, easy to forget cleanup
|
|
@@ -49,7 +50,7 @@ try:
|
|
|
49
50
|
finally:
|
|
50
51
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
51
52
|
|
|
52
|
-
# tmpkit — one line,
|
|
53
|
+
# tmpkit — one line, cleans up on exit
|
|
53
54
|
from tmpkit import temp_file
|
|
54
55
|
with temp_file(suffix=".csv") as f:
|
|
55
56
|
f.write(data)
|
|
@@ -80,7 +81,7 @@ This installs `pytest`, `pytest-asyncio`, `pytest-cov`, `ruff`, `mypy`, and `bui
|
|
|
80
81
|
```python
|
|
81
82
|
from tmpkit import temp_file, temp_dir, atomic_write, async_temp_file
|
|
82
83
|
|
|
83
|
-
# Temp file
|
|
84
|
+
# Temp file (default mode is "w+b" — binary; use mode="w+" for str)
|
|
84
85
|
with temp_file(suffix=".csv", prefix="myapp_") as f:
|
|
85
86
|
f.write(data)
|
|
86
87
|
# deleted on exit
|
|
@@ -110,7 +111,7 @@ with temp_file(dest="output.csv") as f:
|
|
|
110
111
|
# Async
|
|
111
112
|
async def main() -> None:
|
|
112
113
|
async with async_temp_file(suffix=".json") as f:
|
|
113
|
-
await f.write(data)
|
|
114
|
+
await f.write(data) # bytes by default; mode="w+" for str
|
|
114
115
|
```
|
|
115
116
|
|
|
116
117
|
---
|
|
@@ -130,7 +131,7 @@ async def main() -> None:
|
|
|
130
131
|
- **`@temp_dir()` / `@temp_file()` decorators** — inject temps into functions and test classes.
|
|
131
132
|
- **Close without delete** — file survives `close()`, deleted on context exit (Windows subprocess friendly).
|
|
132
133
|
- **`.path` attribute** — `Path` object, no more `Path(f.name)` boilerplate.
|
|
133
|
-
- **Async support** — `async with
|
|
134
|
+
- **Async support** — `async with async_temp_file() as f:` with async I/O methods.
|
|
134
135
|
- **Windows-safe by default** — no `O_TEMPORARY` lock, `ignore_cleanup_errors=True`.
|
|
135
136
|
- **Zero dependencies** — stdlib only.
|
|
136
137
|
|
|
@@ -149,7 +150,7 @@ with temp_file(
|
|
|
149
150
|
dir: str | Path | None = None, # parent directory
|
|
150
151
|
mode: str = "w+b", # open mode
|
|
151
152
|
content: str | bytes | None = None, # pre-populate
|
|
152
|
-
dest: str | Path | None = None, # move here on success
|
|
153
|
+
dest: str | Path | None = None, # move here on success (parent must exist at exit)
|
|
153
154
|
keep: bool = False, # always keep
|
|
154
155
|
keep_on_error: bool = False, # keep only on exception
|
|
155
156
|
ignore_cleanup_errors: bool = True,
|
|
@@ -192,12 +193,12 @@ with temp_dir(
|
|
|
192
193
|
td = temp_dir()
|
|
193
194
|
with td as d:
|
|
194
195
|
(d / "file.txt").write_text("hello")
|
|
195
|
-
td.keep()
|
|
196
|
+
td.keep() # runtime decision to keep (must be inside the block)
|
|
196
197
|
```
|
|
197
198
|
|
|
198
|
-
**Returns:** A `Path` object (the temp directory path) with `/` operator support. To call `.keep()`, use the context manager object directly (see example above)
|
|
199
|
+
**Returns:** A `Path` object (the temp directory path) with `/` operator support. To call `.keep()`, use the context manager object directly (see example above) — it must be called **inside** the `with` block; calling it after exit raises `RuntimeError`.
|
|
199
200
|
|
|
200
|
-
**`cwd=True`:** Changes the working directory to the temp dir on `__enter__`, restores the original on `__exit__`.
|
|
201
|
+
**`cwd=True`:** Changes the working directory to the temp dir on `__enter__`, restores the original on `__exit__`. Warning: `os.chdir` is process-global — do not use `cwd=True` from multiple threads or concurrent async tasks.
|
|
201
202
|
|
|
202
203
|
### `atomic_write()`
|
|
203
204
|
|
|
@@ -212,8 +213,10 @@ with atomic_write(
|
|
|
212
213
|
prefix: str | None = None,
|
|
213
214
|
suffix: str = ".tmp",
|
|
214
215
|
fsync: bool = True, # fsync before rename
|
|
216
|
+
keep: bool = False, # keep temp, dest untouched
|
|
215
217
|
keep_on_error: bool = False,
|
|
216
218
|
ignore_cleanup_errors: bool = True,
|
|
219
|
+
cleanup_hook: Callable[[Path], None] | None = None,
|
|
217
220
|
) as f:
|
|
218
221
|
f.write(data)
|
|
219
222
|
# on success: atomically renamed to dest
|
|
@@ -222,6 +225,10 @@ with atomic_write(
|
|
|
222
225
|
|
|
223
226
|
Writes to a temp file in `dest`'s parent directory, then atomically replaces `dest` via `os.replace()` on success. On error, the temp is cleaned up and `dest` is left untouched.
|
|
224
227
|
|
|
228
|
+
**Note:** with `keep=True`, `.keep()`, `keep_on_error=True` + exception, or `DEBUG=1`, the temp file is kept and **the replace is skipped** — `dest` is left untouched even on success.
|
|
229
|
+
|
|
230
|
+
**Validation on enter:** raises `IsADirectoryError` if `dest` is an existing directory, `FileNotFoundError` if `dest`'s parent doesn't exist, and `NotADirectoryError` if the parent exists but isn't a directory — before writing anything.
|
|
231
|
+
|
|
225
232
|
### `@temp_dir()` Decorator
|
|
226
233
|
|
|
227
234
|
```python
|
|
@@ -248,9 +255,9 @@ class TestMyApp:
|
|
|
248
255
|
assert self.tmpdir.exists()
|
|
249
256
|
```
|
|
250
257
|
|
|
251
|
-
**Decorator defaults:** `cwd=True` (unlike the context manager where `cwd=False` by default).
|
|
258
|
+
**Decorator defaults:** `cwd=True` (unlike the context manager where `cwd=False` by default). Accepts the same parameters as `temp_dir()`, including `cleanup_hook`. Only plain functions and classes — decorating a single method injects the temp path in place of `self`.
|
|
252
259
|
|
|
253
|
-
**Class decoration:** Each method starting with `test_` is wrapped. The temp dir is available as `self.tmpdir`. Works with both sync and async test methods.
|
|
260
|
+
**Class decoration:** Each method starting with `test_` is wrapped (`staticmethod`/`classmethod` members are skipped). The temp dir is available as `self.tmpdir`. Works with both sync and async test methods.
|
|
254
261
|
|
|
255
262
|
### `@temp_file()` Decorator
|
|
256
263
|
|
|
@@ -271,7 +278,7 @@ async def process_async(f, data: str) -> None:
|
|
|
271
278
|
await f.write(data)
|
|
272
279
|
```
|
|
273
280
|
|
|
274
|
-
The temp file object is injected as the **first positional argument**.
|
|
281
|
+
The temp file object is injected as the **first positional argument**. Accepts the same parameters as `temp_file()`, including `cleanup_hook`. Plain functions only — decorating a class raises `TypeError`.
|
|
275
282
|
|
|
276
283
|
### `temp_registry`
|
|
277
284
|
|
|
@@ -307,7 +314,7 @@ temp_registry.clear_history()
|
|
|
307
314
|
temp_registry.disable()
|
|
308
315
|
```
|
|
309
316
|
|
|
310
|
-
**`TempRecord`
|
|
317
|
+
**`TempRecord`** is importable for type annotations: `from tmpkit import TempRecord`. Fields:
|
|
311
318
|
|
|
312
319
|
| Field | Type | Description |
|
|
313
320
|
| --- | --- | --- |
|
|
@@ -319,6 +326,8 @@ temp_registry.disable()
|
|
|
319
326
|
|
|
320
327
|
**Thread-safe:** All operations are protected by `threading.Lock`.
|
|
321
328
|
|
|
329
|
+
**Note:** records accumulate for the lifetime of the process. In long-running applications, call `temp_registry.clear_history()` periodically (or `reset()`) to bound memory usage.
|
|
330
|
+
|
|
322
331
|
### Async API
|
|
323
332
|
|
|
324
333
|
All sync APIs have async counterparts with identical parameters:
|
|
@@ -351,7 +360,7 @@ Async file objects support `await f.read()`, `await f.write()`, `await f.seek()`
|
|
|
351
360
|
| `DEBUG` | `1` | Same as `TMPKIT_DEBUG=1` (fallback) |
|
|
352
361
|
| `TMPKIT_REGISTRY` | `1` | Enable `temp_registry` at import time |
|
|
353
362
|
|
|
354
|
-
`TMPKIT_DEBUG` takes precedence over `DEBUG`.
|
|
363
|
+
`TMPKIT_DEBUG` takes precedence over `DEBUG`. Since `DEBUG` is a common variable name in other tools, you can set `TMPKIT_DEBUG=0` to explicitly disable keep-all even when `DEBUG=1` is present.
|
|
355
364
|
|
|
356
365
|
---
|
|
357
366
|
|
|
@@ -398,6 +407,11 @@ with temp_file(cleanup_hook=my_hook) as f:
|
|
|
398
407
|
f.write(data)
|
|
399
408
|
# hook is called, then standard cleanup runs
|
|
400
409
|
|
|
410
|
+
# Hook also runs when the temp is moved via dest=
|
|
411
|
+
with temp_file(dest="output.csv", cleanup_hook=my_hook) as f:
|
|
412
|
+
f.write(data)
|
|
413
|
+
# hook is called, then temp is moved to output.csv
|
|
414
|
+
|
|
401
415
|
# Hook is called even on exceptions
|
|
402
416
|
with temp_file(cleanup_hook=my_hook) as f:
|
|
403
417
|
raise RuntimeError("oops")
|
|
@@ -470,4 +484,4 @@ See [CHANGELOG.md](CHANGELOG.md) for a full list of changes.
|
|
|
470
484
|
|
|
471
485
|
## License
|
|
472
486
|
|
|
473
|
-
[MIT](LICENSE) — Copyright (c) 2025 Mathias Paulenko
|
|
487
|
+
[MIT](LICENSE) — Copyright (c) 2025-2026 Mathias Paulenko
|
|
@@ -27,5 +27,5 @@ If you discover a security vulnerability in tmpkit, please report it responsibly
|
|
|
27
27
|
tmpkit is a zero-dependency library that wraps Python's `tempfile` module. The main security considerations are:
|
|
28
28
|
|
|
29
29
|
- **Temp file permissions**: Uses `tempfile.mkstemp()` which creates files with restrictive permissions (0600 on Unix).
|
|
30
|
-
- **Symlink attacks**: Uses `shutil.rmtree()` for directory cleanup
|
|
30
|
+
- **Symlink attacks**: Uses `shutil.rmtree()` for directory cleanup, which includes partial protections against symlink-based races on Python 3.11+. Avoid pointing `dir` at world-writable locations you don't control.
|
|
31
31
|
- **Path traversal**: Temp paths are generated by the stdlib and are not user-controlled beyond `suffix`, `prefix`, and `dir` parameters. If you pass a user-controlled `dir`, validate it.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tmpkit"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "Ergonomic tempfile & tempdir context managers with auto-cleanup, atomic writes, async support, and zero dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -30,8 +30,8 @@ dev = [
|
|
|
30
30
|
"pytest>=8.0",
|
|
31
31
|
"pytest-asyncio>=0.23",
|
|
32
32
|
"pytest-cov>=5.0",
|
|
33
|
-
"ruff>=0.
|
|
34
|
-
"mypy>=
|
|
33
|
+
"ruff>=0.16.0",
|
|
34
|
+
"mypy>=2.3.0",
|
|
35
35
|
"build>=1.0",
|
|
36
36
|
]
|
|
37
37
|
|
|
@@ -80,6 +80,25 @@ warn_unused_ignores = true
|
|
|
80
80
|
warn_no_return = true
|
|
81
81
|
warn_unreachable = true
|
|
82
82
|
|
|
83
|
+
# Tests exercise decorator-injected args and private internals; strict
|
|
84
|
+
# signature/attr checks don't apply there.
|
|
85
|
+
[[tool.mypy.overrides]]
|
|
86
|
+
module = "tests.*"
|
|
87
|
+
disallow_untyped_defs = false
|
|
88
|
+
disallow_incomplete_defs = false
|
|
89
|
+
warn_return_any = false
|
|
90
|
+
warn_unused_ignores = false
|
|
91
|
+
disable_error_code = [
|
|
92
|
+
"arg-type",
|
|
93
|
+
"attr-defined",
|
|
94
|
+
"call-arg",
|
|
95
|
+
"call-overload",
|
|
96
|
+
"comparison-overlap",
|
|
97
|
+
"func-returns-value",
|
|
98
|
+
"method-assign",
|
|
99
|
+
"union-attr",
|
|
100
|
+
]
|
|
101
|
+
|
|
83
102
|
[tool.pytest.ini_options]
|
|
84
103
|
asyncio_mode = "auto"
|
|
85
104
|
testpaths = ["tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for tmpkit."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for tmpkit."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for tmpkit."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for tmpkit."""
|