Skip to content

Commit b0a2778

Browse files
authored
Merge pull request #337 from mgxd/ci/actions
MAINT: Raise minimum to 3.10, bump actions
2 parents c1126ae + da0eb1d commit b0a2778

File tree

22 files changed

+233
-75
lines changed

22 files changed

+233
-75
lines changed

.github/workflows/pytest.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ jobs:
2424
runs-on: ubuntu-latest
2525
strategy:
2626
matrix:
27-
python-version: ['3.9', '3.10', '3.11']
27+
python-version: ['3.10', '3.11']
2828

2929
steps:
3030
- name: Set up Python ${{ matrix.python-version }}
31-
uses: actions/setup-python@v4
31+
uses: actions/setup-python@v5
3232
with:
3333
python-version: ${{ matrix.python-version }}
34-
- uses: actions/checkout@v3
34+
- uses: actions/checkout@v4
3535
with:
3636
fetch-depth: 0
3737
- name: Install nibabies

.github/workflows/style.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ jobs:
2020
runs-on: ubuntu-latest
2121
strategy:
2222
matrix:
23-
python-version: ['3.10']
23+
python-version: ['3.11']
2424

2525
steps:
26-
- uses: actions/checkout@v3
26+
- uses: actions/checkout@v4
2727
with:
2828
fetch-depth: 1 # Only fetch the latest commit
2929
- name: Set up Python ${{ matrix.python-version }}
30-
uses: actions/setup-python@v4
30+
uses: actions/setup-python@v5
3131
with:
3232
python-version: ${{ matrix.python-version }}
3333
- name: Install dependencies

nibabies/_warnings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Manipulate Python warnings."""
2+
23
import logging
34
import warnings
45

nibabies/cli/run.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,6 @@ def main():
130130
_copy_any(dseg_tsv, str(config.execution.nibabies_dir / "desc-aparcaseg_dseg.tsv"))
131131
# errno = 0
132132
finally:
133-
from pkg_resources import resource_filename as pkgrf
134-
135133
from ..reports.core import generate_reports
136134

137135
# Generate reports phase

nibabies/cli/workflow.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ def build_workflow(config_file):
4444

4545
# Called with reports only
4646
if config.execution.reports_only:
47-
from pkg_resources import resource_filename as pkgrf
48-
4947
build_logger.log(
5048
25,
5149
"Running --reports-only on participants %s",
@@ -132,14 +130,16 @@ def build_boilerplate(workflow):
132130
from shutil import copyfile
133131
from subprocess import CalledProcessError, TimeoutExpired, check_call
134132

135-
from pkg_resources import resource_filename as pkgrf
133+
from nibabies.data import load as load_data
134+
135+
bib = load_data("boilerplate.bib")
136136

137137
# Generate HTML file resolving citations
138138
cmd = [
139139
"pandoc",
140140
"-s",
141141
"--bibliography",
142-
pkgrf("nibabies", "data/boilerplate.bib"),
142+
bib,
143143
"--citeproc",
144144
"--metadata",
145145
'pagetitle="nibabies citation boilerplate"',
@@ -159,7 +159,7 @@ def build_boilerplate(workflow):
159159
"pandoc",
160160
"-s",
161161
"--bibliography",
162-
pkgrf("nibabies", "data/boilerplate.bib"),
162+
bib,
163163
"--natbib",
164164
str(citation_files["md"]),
165165
"-o",
@@ -171,4 +171,4 @@ def build_boilerplate(workflow):
171171
except (FileNotFoundError, CalledProcessError, TimeoutExpired):
172172
config.loggers.cli.warning("Could not generate CITATION.tex file:\n%s", " ".join(cmd))
173173
else:
174-
copyfile(pkgrf("nibabies", "data/boilerplate.bib"), citation_files["bib"])
174+
copyfile(bib, citation_files["bib"])

nibabies/conftest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""py.test configuration"""
2+
23
from pathlib import Path
34
from tempfile import TemporaryDirectory
45

56
import pytest
6-
from pkg_resources import resource_filename
7+
8+
from nibabies.data import load as load_data
79

810
FILES = (
911
"functional.nii",
@@ -33,5 +35,5 @@ def data_dir():
3335
@pytest.fixture(autouse=True)
3436
def set_namespace(doctest_namespace, data_dir):
3537
doctest_namespace["data_dir"] = data_dir
36-
doctest_namespace["test_data"] = Path(resource_filename("nibabies", "tests/data"))
38+
doctest_namespace["test_data"] = load_data.cached('../tests/data')
3739
doctest_namespace["Path"] = Path

nibabies/data/__init__.py

Lines changed: 167 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
1+
"""Data file retrieval
2+
3+
.. autofunction:: load
4+
5+
.. automethod:: load.readable
6+
7+
.. automethod:: load.as_path
8+
9+
.. automethod:: load.cached
10+
11+
.. autoclass:: Loader
12+
"""
13+
14+
from __future__ import annotations
15+
116
import atexit
2-
from contextlib import ExitStack
17+
import os
18+
from contextlib import AbstractContextManager, ExitStack
19+
from functools import cached_property
320
from pathlib import Path
21+
from types import ModuleType
22+
from typing import Union
423

524
try:
625
from functools import cache
@@ -10,16 +29,155 @@
1029
try: # Prefer backport to leave consistency to dependency spec
1130
from importlib_resources import as_file, files
1231
except ImportError:
13-
from importlib.resources import as_file, files
32+
from importlib.resources import as_file, files # type: ignore
33+
34+
try: # Prefer stdlib so Sphinx can link to authoritative documentation
35+
from importlib.resources.abc import Traversable
36+
except ImportError:
37+
from importlib_resources.abc import Traversable
38+
39+
__all__ = ["load"]
40+
41+
42+
class Loader:
43+
"""A loader for package files relative to a module
44+
45+
This class wraps :mod:`importlib.resources` to provide a getter
46+
function with an interpreter-lifetime scope. For typical packages
47+
it simply passes through filesystem paths as :class:`~pathlib.Path`
48+
objects. For zipped distributions, it will unpack the files into
49+
a temporary directory that is cleaned up on interpreter exit.
50+
51+
This loader accepts a fully-qualified module name or a module
52+
object.
53+
54+
Expected usage::
55+
56+
'''Data package
57+
58+
.. autofunction:: load_data
59+
60+
.. automethod:: load_data.readable
61+
62+
.. automethod:: load_data.as_path
63+
64+
.. automethod:: load_data.cached
65+
'''
66+
67+
from nibabies.data import Loader
68+
69+
load_data = Loader(__package__)
70+
71+
:class:`~Loader` objects implement the :func:`callable` interface
72+
and generate a docstring, and are intended to be treated and documented
73+
as functions.
74+
75+
For greater flexibility and improved readability over the ``importlib.resources``
76+
interface, explicit methods are provided to access resources.
77+
78+
+---------------+----------------+------------------+
79+
| On-filesystem | Lifetime | Method |
80+
+---------------+----------------+------------------+
81+
| `True` | Interpreter | :meth:`cached` |
82+
+---------------+----------------+------------------+
83+
| `True` | `with` context | :meth:`as_path` |
84+
+---------------+----------------+------------------+
85+
| `False` | n/a | :meth:`readable` |
86+
+---------------+----------------+------------------+
87+
88+
It is also possible to use ``Loader`` directly::
89+
90+
from nibabies.data import Loader
91+
92+
Loader(other_package).readable('data/resource.ext').read_text()
93+
94+
with Loader(other_package).as_path('data') as pkgdata:
95+
# Call function that requires full Path implementation
96+
func(pkgdata)
97+
98+
# contrast to
99+
100+
from importlib_resources import files, as_file
101+
102+
files(other_package).joinpath('data/resource.ext').read_text()
103+
104+
with as_file(files(other_package) / 'data') as pkgdata:
105+
func(pkgdata)
106+
107+
.. automethod:: readable
108+
109+
.. automethod:: as_path
110+
111+
.. automethod:: cached
112+
"""
113+
114+
def __init__(self, anchor: Union[str, ModuleType]):
115+
self._anchor = anchor
116+
self.files = files(anchor)
117+
self.exit_stack = ExitStack()
118+
atexit.register(self.exit_stack.close)
119+
# Allow class to have a different docstring from instances
120+
self.__doc__ = self._doc
121+
122+
@cached_property
123+
def _doc(self):
124+
"""Construct docstring for instances
125+
126+
Lists the public top-level paths inside the location, where
127+
non-public means has a `.` or `_` prefix or is a 'tests'
128+
directory.
129+
"""
130+
top_level = sorted(
131+
os.path.relpath(p, self.files) + "/"[: p.is_dir()]
132+
for p in self.files.iterdir()
133+
if p.name[0] not in (".", "_") and p.name != "tests"
134+
)
135+
doclines = [
136+
f"Load package files relative to ``{self._anchor}``.",
137+
"",
138+
"This package contains the following (top-level) files/directories:",
139+
"",
140+
*(f"* ``{path}``" for path in top_level),
141+
]
142+
143+
return "\n".join(doclines)
144+
145+
def readable(self, *segments) -> Traversable:
146+
"""Provide read access to a resource through a Path-like interface.
147+
148+
This file may or may not exist on the filesystem, and may be
149+
efficiently used for read operations, including directory traversal.
150+
151+
This result is not cached or copied to the filesystem in cases where
152+
that would be necessary.
153+
"""
154+
return self.files.joinpath(*segments)
155+
156+
def as_path(self, *segments) -> AbstractContextManager[Path]:
157+
"""Ensure data is available as a :class:`~pathlib.Path`.
158+
159+
This method generates a context manager that yields a Path when
160+
entered.
161+
162+
This result is not cached, and any temporary files that are created
163+
are deleted when the context is exited.
164+
"""
165+
return as_file(self.files.joinpath(*segments))
166+
167+
@cache
168+
def cached(self, *segments) -> Path:
169+
"""Ensure data is available as a :class:`~pathlib.Path`.
14170
15-
__all__ = ["load_resource"]
171+
Any temporary files that are created remain available throughout
172+
the duration of the program, and are deleted when Python exits.
16173
17-
exit_stack = ExitStack()
18-
atexit.register(exit_stack.close)
174+
Results are cached so that multiple calls do not unpack the same
175+
data multiple times, but the cache is sensitive to the specific
176+
argument(s) passed.
177+
"""
178+
return self.exit_stack.enter_context(as_file(self.files.joinpath(*segments)))
19179

20-
path = files(__package__)
180+
__call__ = cached
21181

22182

23-
@cache
24-
def load_resource(fname: str) -> Path:
25-
return exit_stack.enter_context(as_file(path.joinpath(fname)))
183+
load = Loader(__package__)

nibabies/interfaces/confounds.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class GatherConfounds(SimpleInterface):
114114
>>> tmpdir.cleanup()
115115
116116
"""
117+
117118
input_spec = GatherConfoundsInputSpec
118119
output_spec = GatherConfoundsOutputSpec
119120

nibabies/interfaces/maths.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""A module for interfaces """
2+
23
import os
34

45
import numpy as np

nibabies/reports/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from nireports.assembler.report import Report
44

5-
from nibabies.data import load_resource
5+
from nibabies.data import load as load_data
66

77

88
def run_reports(
@@ -22,7 +22,7 @@ def run_reports(
2222
run_uuid,
2323
subject=subject,
2424
session=session,
25-
bootstrap_file=load_resource('reports-spec.yml'),
25+
bootstrap_file=load_data.readable('reports-spec.yml'),
2626
reportlets_dir=reportlets_dir,
2727
).generate_report()
2828

0 commit comments

Comments
 (0)