File: README.md

package info (click to toggle)
python-acres 0.5.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 296 kB
  • sloc: python: 194; makefile: 11
file content (156 lines) | stat: -rw-r--r-- 5,237 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# Acres: Access resources on your terms

[![PyPI release](https://img.shields.io/pypi/v/acres.svg)](https://pypi.python.org/project/acres/)
[![Codecov Coverage report](https://codecov.io/github/nipreps/acres/graph/badge.svg?token=jVfxERJR5k)](https://codecov.io/github/nipreps/acres)

This module provides simple, consistent access to package resources.

```python
from acres import Loader

loader = Loader(somepkg)

res_text = loader.readable('data/some_resource.txt').read_text()
res_bytes = loader.readable('data/another_resource.bin').read_bytes()

with loader.as_path('data') as data_dir:
    # data_dir is a pathlib.Path until the "with" scope closes

resource_path = loader.cached('data/another_resource.bin')
# The path pointed to by resource_path will exist until interpreter exit
```

## Module loader

The primary use case for `acres.Loader` is to allow a directory module to provide access
to its resources.

Suppose you have a module structure:

```
src/
  mypkg/
    data/
      resourceDir/
        ...
      __init__.py
      resource.ext
    __init__.py
    ...
```

In `src/mypkg/data/__init__.py`, add:

```python
'''Data package

.. autofunction:: load_resource

.. automethod:: load_resource.readable

.. automethod:: load_resource.as_path

.. automethod:: load_resource.cached
'''

from acres import Loader

load_resource = Loader(__spec__.name)
```

`mypkg.data.load_resource()` is now a function that will return a `Path` to a
resource that is guaranteed to exist until interpreter exit:

```python
from mypkg.data import load_resource

resource_file: Path = load_resource('resource.ext')
```

For additional control, you can use `load_resource.readable()` to return a `Path`-like
object that implements `.read_text()` and `.read_bytes()`:

```python
resource_contents: bytes = load_resource.readable('resource.ext').read_bytes()
```

Or a context manager with a limited lifetime:

```python
with load_resource.as_path('resourceDir') as resource_dir:
    # Work with the contents of `resource_dir` as a `Path`

# Outside the `with` block, `resource_dir` may no longer point to an existing path.
```

Note that `load_resource()` is a shorthand for `load_resource.cached()`,
whose explicitness might be more to your taste.

### The `__spec__.name` anchor

Previous versions recommended using `Loader(__package__)`.
Before Python 3.10, `__package__` might be `None` during a [zipimport][],
and `__package__` has been deprecated in Python 3.13, to be removed in 3.15.

[`__spec__.parent`][ModuleSpec.parent] is an exact equivalent for `__package__`,
but for `__init__.py` files, [`__spec__.name`][ModuleSpec.name] is equivalent.
`__spec__.name` is also guaranteed to be a string and not `None`,
which lets it play nicely with type checkers.

## Interpreter-scoped resources, locally scoped loaders

`Loader.cached` uses a global cache. This ensures that cached files do not get
unloaded if a `Loader()` instance is garbage collected, and it also ensures that
instances can be created cheaply and garbage collected when out of scope.

## Why acres?

`importlib.resources` provides a simple, composable interface, using the
`files()` and `as_file()` functions:

```python
from importlib.resources import files, as_file

with as_file(files(my_module) / 'data' / 'resource.ext') as resource_path:
    # Interact with resource_path as a pathlib.Path

# resource_path *may* no longer exist
```

`files()` returns a [Traversable][] object, which is similar to a [pathlib.Path][],
except that the object may not actually exist, so `os` functions may not work
correctly on it. `as_files()` turns a `Traversable` into a `Path` that exists on
the filesystem for the duration of the `with` block.

To make matters more complicated, if a package is unpacked on the filesystem,
`files()` returns an actual `Path` object. It is therefore easy to miss bugs
where a true `Path` is always needed but a `Traversable` may be returned when
the package is zipped.

Finally, the scoping of `as_files()` is frequently inconvenient. If you pass a
`Path` to an object inside an `as_files()` context, but the resource access is
deferred, the `Path` may point to a nonexistent file or directory. Again,
this bug will only exhibit when the package is zipped.

`acres.Loader` aims to clearly delineate the scopes and capabilities of
the accessed resources, including providing an interpreter-lifetime scope.

## Type annotations

`acres` is fully type-annotated, and checked with both `mypy` and `pyright`.
The `Loader.readable` method returns a [Traversable][], which moved from
`importlib.abc` in Python 3.10 to `importlib.resources.abc` in Python 3.11.
To simplify the use of this annotation in functions that accept readable
resources, use:

```python
import acres.typ as at

def myfunc(resource: at.Traversable) -> ReturnType:
    ...
```

[Traversable]: https://docs.python.org/3/library/importlib.resources.abc.html#importlib.resources.abc.Traversable
[pathlib.Path]: https://docs.python.org/3/library/pathlib.html#pathlib.Path
[ModuleSpec.name]: https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.name
[ModuleSpec.parent]: https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.parent