Skip to content

Commit

Permalink
Merge pull request #109 from templateflow/mnt/py312
Browse files Browse the repository at this point in the history
MNT: Python 3.12 support, drop Python 3.7 and pkg_resources
  • Loading branch information
effigies authored Oct 12, 2023
2 parents 0e3795c + 9bd9d27 commit 9f5462b
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
source /tmp/venv/bin/activate
pip install -U pip
pip install -r /tmp/src/templateflow/requirements.txt
pip install "datalad ~= 0.11.8"
pip install datalad
pip install "setuptools>=45" "setuptools_scm >= 6.2" nipreps-versions build twine codecov
- run:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ jobs:
needs: build
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
mode: ['wheel']
include:
- {python-version: '3.9', mode: 'repo'}
- {python-version: '3.9', mode: 'sdist'}
- {python-version: '3.9', mode: 'editable'}
- {python-version: '3.11', mode: 'repo'}
- {python-version: '3.11', mode: 'sdist'}
- {python-version: '3.11', mode: 'editable'}

env:
TEMPLATEFLOW_HOME: /tmp/home
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pybids>=0.15.2
importlib_resources >= 5.7; python_version < '3.11'
requests
tqdm
pytest
Expand Down
6 changes: 4 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ classifiers =
Intended Audience :: Science/Research
Topic :: Scientific/Engineering :: Image Recognition
License :: OSI Approved :: Apache Software License
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
description = TemplateFlow Python Client - TemplateFlow is the Zone of neuroimaging templates.
license = Apache-2.0
license_file = LICENSE
Expand All @@ -24,13 +25,14 @@ project_urls =
Source Code = https://github.com/templateflow/python-client

[options]
python_requires = >= 3.7
python_requires = >= 3.8
setup_requires =
setuptools >= 45
setuptools_scm >= 6.2
wheel
install_requires =
pybids >= 0.15.2
importlib_resources >= 5.7; python_version < '3.11'
requests
tqdm
test_requires =
Expand Down
13 changes: 6 additions & 7 deletions templateflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
try:
from ._version import __version__
except ModuleNotFoundError:
from pkg_resources import get_distribution, DistributionNotFound

from importlib.metadata import version, PackageNotFoundError
try:
__version__ = get_distribution(__packagename__).version
except DistributionNotFound:
__version__ = "unknown"
del get_distribution
del DistributionNotFound
__version__ = version(__packagename__)
except PackageNotFoundError:
__version__ = "0+unknown"
del version
del PackageNotFoundError

import os
from . import api
Expand Down
171 changes: 171 additions & 0 deletions templateflow/_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""Resource loader module
.. autoclass:: Loader
"""
from __future__ import annotations

import atexit
import os
from contextlib import AbstractContextManager, ExitStack
from functools import cached_property
from pathlib import Path
from types import ModuleType
from typing import Union

try:
from functools import cache
except ImportError: # PY38 # pragma: no cover
from functools import lru_cache as cache

try: # Prefer backport to leave consistency to dependency spec
from importlib_resources import as_file, files
except ImportError: # pragma: no cover
from importlib.resources import as_file, files # type: ignore

try: # Prefer stdlib so Sphinx can link to authoritative documentation
from importlib.resources.abc import Traversable
except ImportError: # pragma: no cover
from importlib_resources.abc import Traversable

__all__ = ["Loader"]


class Loader:
"""A loader for package files relative to a module
This class wraps :mod:`importlib.resources` to provide a getter
function with an interpreter-lifetime scope. For typical packages
it simply passes through filesystem paths as :class:`~pathlib.Path`
objects. For zipped distributions, it will unpack the files into
a temporary directory that is cleaned up on interpreter exit.
This loader accepts a fully-qualified module name or a module
object.
Expected usage::
'''Data package
.. autofunction:: load_data
.. automethod:: load_data.readable
.. automethod:: load_data.as_path
.. automethod:: load_data.cached
'''
from templateflow._loader import Loader
load_data = Loader(__package__)
:class:`~Loader` objects implement the :func:`callable` interface
and generate a docstring, and are intended to be treated and documented
as functions.
For greater flexibility and improved readability over the ``importlib.resources``
interface, explicit methods are provided to access resources.
+---------------+----------------+------------------+
| On-filesystem | Lifetime | Method |
+---------------+----------------+------------------+
| `True` | Interpreter | :meth:`cached` |
+---------------+----------------+------------------+
| `True` | `with` context | :meth:`as_path` |
+---------------+----------------+------------------+
| `False` | n/a | :meth:`readable` |
+---------------+----------------+------------------+
It is also possible to use ``Loader`` directly::
from templateflow._loader import Loader
Loader(other_package).readable('data/resource.ext').read_text()
with Loader(other_package).as_path('data') as pkgdata:
# Call function that requires full Path implementation
func(pkgdata)
# contrast to
from importlib_resources import files, as_file
files(other_package).joinpath('data/resource.ext').read_text()
with as_file(files(other_package) / 'data') as pkgdata:
func(pkgdata)
.. automethod:: readable
.. automethod:: as_path
.. automethod:: cached
"""

def __init__(self, anchor: Union[str, ModuleType]):
self._anchor = anchor
self.files = files(anchor)
self.exit_stack = ExitStack()
atexit.register(self.exit_stack.close)
# Allow class to have a different docstring from instances
self.__doc__ = self._doc

@cached_property
def _doc(self):
"""Construct docstring for instances
Lists the public top-level paths inside the location, where
non-public means has a `.` or `_` prefix or is a 'tests'
directory.
"""
top_level = sorted(
os.path.relpath(p, self.files) + "/"[: p.is_dir()]
for p in self.files.iterdir()
if p.name[0] not in (".", "_") and p.name != "tests"
)
doclines = [
f"Load package files relative to ``{self._anchor}``.",
"",
"This package contains the following (top-level) files/directories:",
"",
*(f"* ``{path}``" for path in top_level),
]

return "\n".join(doclines)

def readable(self, *segments) -> Traversable:
"""Provide read access to a resource through a Path-like interface.
This file may or may not exist on the filesystem, and may be
efficiently used for read operations, including directory traversal.
This result is not cached or copied to the filesystem in cases where
that would be necessary.
"""
return self.files.joinpath(*segments)

def as_path(self, *segments) -> AbstractContextManager[Path]:
"""Ensure data is available as a :class:`~pathlib.Path`.
This method generates a context manager that yields a Path when
entered.
This result is not cached, and any temporary files that are created
are deleted when the context is exited.
"""
return as_file(self.files.joinpath(*segments))

@cache
def cached(self, *segments) -> Path:
"""Ensure data is available as a :class:`~pathlib.Path`.
Any temporary files that are created remain available throughout
the duration of the program, and are deleted when Python exits.
Results are cached so that multiple calls do not unpack the same
data multiple times, but the cache is sensitive to the specific
argument(s) passed.
"""
return self.exit_stack.enter_context(as_file(self.files.joinpath(*segments)))

__call__ = cached
3 changes: 3 additions & 0 deletions templateflow/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from pathlib import Path
from contextlib import suppress
from functools import wraps
from .._loader import Loader

load_data = Loader(__package__)

TF_DEFAULT_HOME = Path.home() / ".cache" / "templateflow"
TF_HOME = Path(getenv("TEMPLATEFLOW_HOME", str(TF_DEFAULT_HOME)))
Expand Down
9 changes: 4 additions & 5 deletions templateflow/conf/_s3.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
"""Tooling to handle S3 downloads."""
from pathlib import Path
from tempfile import mkstemp
from pkg_resources import resource_filename

from . import load_data

TF_SKEL_URL = (
"https://raw.githubusercontent.com/templateflow/python-client/"
"{release}/templateflow/conf/templateflow-skel.{ext}"
).format
TF_SKEL_PATH = Path(resource_filename("templateflow", "conf/templateflow-skel.zip"))
TF_SKEL_MD5 = Path(
resource_filename("templateflow", "conf/templateflow-skel.md5")
).read_text()
TF_SKEL_PATH = load_data("templateflow-skel.zip")
TF_SKEL_MD5 = load_data.readable("templateflow-skel.md5").read_text()


def update(dest, local=True, overwrite=True, silent=False):
Expand Down
5 changes: 3 additions & 2 deletions templateflow/conf/bids.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Extending pyBIDS for querying TemplateFlow."""
from pkg_resources import resource_filename
from bids.layout import BIDSLayout, add_config_paths

add_config_paths(templateflow=resource_filename("templateflow", "conf/config.json"))
from . import load_data

add_config_paths(templateflow=load_data("config.json"))


class Layout(BIDSLayout):
Expand Down
17 changes: 8 additions & 9 deletions templateflow/tests/test_version.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Test _version.py."""
import sys
from collections import namedtuple
from pkg_resources import DistributionNotFound
from importlib.metadata import PackageNotFoundError
from importlib import reload
import templateflow

Expand All @@ -18,14 +18,13 @@ class _version:


def test_version_scm1(monkeypatch):
"""Retrieve the version via pkg_resources."""
"""Retrieve the version via importlib.metadata."""
monkeypatch.setitem(sys.modules, "templateflow._version", None)

def _dist(name):
Distribution = namedtuple("Distribution", ["name", "version"])
return Distribution(name, "success")
def _ver(name):
return "success"

monkeypatch.setattr("pkg_resources.get_distribution", _dist)
monkeypatch.setattr("importlib.metadata.version", _ver)
reload(templateflow)
assert templateflow.__version__ == "success"

Expand All @@ -35,8 +34,8 @@ def test_version_scm2(monkeypatch):
monkeypatch.setitem(sys.modules, "templateflow._version", None)

def _raise(name):
raise DistributionNotFound("No get_distribution mock")
raise PackageNotFoundError("No get_distribution mock")

monkeypatch.setattr("pkg_resources.get_distribution", _raise)
monkeypatch.setattr("importlib.metadata.version", _raise)
reload(templateflow)
assert templateflow.__version__ == "unknown"
assert templateflow.__version__ == "0+unknown"

0 comments on commit 9f5462b

Please sign in to comment.