Skip to content

Commit

Permalink
Merge pull request #8 from effigies/doc/recommend-spec-name
Browse files Browse the repository at this point in the history
test: Run construct a zip module and verify acres functionality
  • Loading branch information
effigies authored Dec 9, 2024
2 parents ea065c0 + 8bc80b6 commit 9cee2b6
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 18 deletions.
24 changes: 11 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ In `src/mypkg/data/__init__.py`, add:

from acres import Loader

load_resource = Loader(__package__)
load_resource = Loader(__spec__.name)
```

`mypkg.data.load_resource()` is now a function that will return a `Path` to a
Expand Down Expand Up @@ -86,20 +86,16 @@ with load_resource.as_path('resourceDir') as resource_dir:
Note that `load_resource()` is a shorthand for `load_resource.cached()`,
whose explicitness might be more to your taste.

### Type checking
### The `__spec__.name` anchor

Some type checkers may complain on `Loader(__package__)` because `__package__` may be `None`.
To resolve this, add `assert __package__` before the call, for example:
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.

```python
from acres import Loader

assert __package__
load_resource = Loader(__package__)
```

This does have a runtime cost, so `# type: ignore[reportArgumentType,unused-ignore]`
can also be used to avoid incurring that, if import times are a concern.
[`__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

Expand Down Expand Up @@ -141,3 +137,5 @@ the accessed resources, including providing an interpreter-lifetime scope.

[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
46 changes: 46 additions & 0 deletions changelog.d/20241209_154150_effigies_recommend_spec_name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Added
- A bullet item for the Added category.
-->
### Changed

- Update recommended usage from `Loader(__package__)` to `Loader(__spec__.name)`.

<!--
### Fixed
- A bullet item for the Fixed category.
-->
<!--
### Deprecated
- A bullet item for the Deprecated category.
-->
<!--
### Removed
- A bullet item for the Removed category.
-->
<!--
### Security
- A bullet item for the Security category.
-->
<!--
### Infrastructure
- A bullet item for the Infrastructure category.
-->
44 changes: 44 additions & 0 deletions changelog.d/20241209_154445_effigies_recommend_spec_name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

### Added

- Tests exercise and demonstrate the usage of acres on zipped modules.

<!--
### Changed
- A bullet item for the Changed category.
-->
### Fixed

- Resolve cache misses when caching the same file from different loaders.

<!--
### Deprecated
- A bullet item for the Deprecated category.
-->
<!--
### Removed
- A bullet item for the Removed category.
-->
<!--
### Security
- A bullet item for the Security category.
-->
<!--
### Infrastructure
- A bullet item for the Infrastructure category.
-->
5 changes: 5 additions & 0 deletions changelog.d/scriv.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[scriv]
format = md
main_branches = main
version = literal: deno.json: version
categories = Added, Changed, Fixed, Deprecated, Removed, Security, Infrastructure
9 changes: 5 additions & 4 deletions src/acres/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@


@cache
def _cache_resource(resource: Traversable) -> Path:
def _cache_resource(anchor: str | ModuleType, segments: tuple[str]) -> Path:
# PY310(importlib_resources): no-any-return, PY311+(importlib.resources): unused-ignore
return EXIT_STACK.enter_context(as_file(resource)) # type: ignore[no-any-return,unused-ignore]
return EXIT_STACK.enter_context(as_file(files(anchor).joinpath(*segments))) # type: ignore[no-any-return,unused-ignore]


class Loader:
Expand Down Expand Up @@ -97,7 +97,7 @@ class Loader:
from acres import Loader
load_data = Loader(__package__)
load_data = Loader(__spec__.name)
:class:`~Loader` objects implement the :func:`callable` interface
and generate a docstring, and are intended to be treated and documented
Expand Down Expand Up @@ -205,7 +205,8 @@ def cached(self, *segments: str) -> Path:
data multiple times, but directories and their contents being
requested separately may result in some duplication.
"""
# Use self._anchor and segments to ensure the cache does not depend on id(self.files)
# PY310(importlib_resources): unused-ignore, PY311+(importlib.resources) arg-type
return _cache_resource(self.files.joinpath(*segments)) # type: ignore[arg-type,unused-ignore]
return _cache_resource(self._anchor, segments) # type: ignore[arg-type,unused-ignore]

__call__ = cached
2 changes: 1 addition & 1 deletion tests/data/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from acres import Loader

load_resource = Loader(__package__) # type: ignore[reportArgumentType,unused-ignore]
load_resource = Loader(__spec__.name)
54 changes: 54 additions & 0 deletions tests/test_zipmodule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import os
import sys
import zipfile
from pathlib import Path

from acres import Loader


def test_zipimport(tmp_path: Path) -> None:
# Setup... no need for a fixture for a single test
target_file = tmp_path / 'mymodule.zip'
with zipfile.ZipFile(target_file, mode='w') as mymod:
mymod.writestr(
'mypkg/__init__.py',
'from . import data\n',
)
mymod.writestr(
'mypkg/data/__init__.py',
'from acres import Loader\nload_resource = Loader(__spec__.name)\n',
)
mymod.writestr(
'mypkg/data/resource.txt',
'some text\n',
)

sys.path.insert(0, str(target_file))

# Test
import mypkg # type: ignore[import-not-found]

assert mypkg.__file__.endswith(os.path.join('mymodule.zip', 'mypkg', '__init__.py'))

loader = mypkg.data.load_resource

readable = loader.readable('resource.txt')
assert not isinstance(readable, Path)
assert readable.read_text() == 'some text\n'

with loader.as_path('resource.txt') as path:
assert isinstance(path, Path)
assert path.exists()
assert path.read_text() == 'some text\n'
assert not path.exists()

cached = loader.cached('resource.txt')
assert isinstance(cached, Path)
assert cached.exists()
assert cached.read_text() == 'some text\n'

new_loader = Loader('mypkg.data')
assert new_loader.cached('resource.txt') == cached

# Teardown
sys.path.pop(0)

0 comments on commit 9cee2b6

Please sign in to comment.