Coverage for python/lsst/resources/packageresource.py: 98%
80 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-01 11:14 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-02-01 11:14 +0000
1# This file is part of lsst-resources.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14__all__ = ("PackageResourcePath",)
16import contextlib
17import logging
18import re
19import sys
21if sys.version_info < (3, 11, 0): 21 ↛ 26line 21 didn't jump to line 26, because the condition on line 21 was never true
22 # Mypy will try to use the first import it encounters and ignores
23 # the sys.version_info. This means that the first import has to be
24 # the backwards compatibility import since we are currently using 3.10
25 # for mypy. Once we switch to 3.11 for mypy the order will have to change.
26 import importlib_resources as resources
27else:
28 from importlib import resources # type: ignore[no-redef]
30from collections.abc import Iterator
32from ._resourceHandles._baseResourceHandle import ResourceHandleProtocol
33from ._resourcePath import ResourcePath
35log = logging.getLogger(__name__)
38class PackageResourcePath(ResourcePath):
39 """URI referring to a Python package resource.
41 These URIs look like: ``resource://lsst.daf.butler/configs/file.yaml``
42 where the network location is the Python package and the path is the
43 resource name.
44 """
46 def _get_ref(self) -> resources.abc.Traversable | None:
47 """Obtain the object representing the resource.
49 Returns
50 -------
51 path : `resources.abc.Traversable` or `None`
52 The reference to the resource path, or `None` if the module
53 associated with the resources is not accessible. This can happen
54 if Python can't import the Python package defining the resource.
55 """
56 # Need the path without the leading /.
57 path = self.path.lstrip("/")
58 try:
59 ref = resources.files(self.netloc).joinpath(path)
60 except ModuleNotFoundError:
61 return None
62 return ref
64 def isdir(self) -> bool:
65 """Return True if this URI is a directory, else False."""
66 if self.dirLike is None:
67 ref = self._get_ref()
68 if ref is not None:
69 self.dirLike = ref.is_dir()
70 else:
71 return False
72 return self.dirLike
74 def exists(self) -> bool:
75 """Check that the python resource exists."""
76 ref = self._get_ref()
77 if ref is None:
78 return False
79 return ref.is_file() or ref.is_dir()
81 def read(self, size: int = -1) -> bytes:
82 ref = self._get_ref()
83 if not ref:
84 raise FileNotFoundError(f"Unable to locate resource {self}.")
85 with ref.open("rb") as fh:
86 return fh.read(size)
88 @contextlib.contextmanager
89 def as_local(self) -> Iterator[ResourcePath]:
90 """Return the location of the Python resource as local file.
92 Yields
93 ------
94 local : `ResourcePath`
95 This might be the original resource or a copy on the local file
96 system.
98 Notes
99 -----
100 The context manager will automatically delete any local temporary
101 file.
103 Examples
104 --------
105 Should be used as a context manager:
107 .. code-block:: py
109 with uri.as_local() as local:
110 ospath = local.ospath
111 """
112 ref = self._get_ref()
113 if ref is None:
114 raise FileNotFoundError(f"Resource {self} could not be located.")
115 if ref.is_dir():
116 raise IsADirectoryError(f"Directory-like URI {self} cannot be fetched as local.")
118 with resources.as_file(ref) as file:
119 yield ResourcePath(file)
121 @contextlib.contextmanager
122 def open(
123 self,
124 mode: str = "r",
125 *,
126 encoding: str | None = None,
127 prefer_file_temporary: bool = False,
128 ) -> Iterator[ResourceHandleProtocol]:
129 # Docstring inherited.
130 if "r" not in mode or "+" in mode:
131 raise RuntimeError(f"Package resource URI {self} is read-only.")
132 ref = self._get_ref()
133 if ref is None:
134 raise FileNotFoundError(f"Could not open resource {self}.")
135 with ref.open(mode, encoding=encoding) as buffer:
136 yield buffer
138 def walk(
139 self, file_filter: str | re.Pattern | None = None
140 ) -> Iterator[list | tuple[ResourcePath, list[str], list[str]]]:
141 # Docstring inherited.
142 if not self.isdir():
143 raise ValueError(f"Can not walk a non-directory URI: {self}")
145 if isinstance(file_filter, str):
146 file_filter = re.compile(file_filter)
148 ref = self._get_ref()
149 if ref is None:
150 raise ValueError(f"Unable to find resource {self}.")
152 files: list[str] = []
153 dirs: list[str] = []
154 for item in ref.iterdir():
155 if item.is_dir():
156 dirs.append(item.name)
157 elif item.is_file(): 157 ↛ 154line 157 didn't jump to line 154, because the condition on line 157 was never false
158 files.append(item.name)
159 # If the item wasn't covered by one of the cases above that
160 # means it was deleted concurrently with this walk or is
161 # not a plain file/directory/symlink
163 if file_filter is not None:
164 files = [f for f in files if file_filter.search(f)]
166 if not dirs and not files:
167 return
168 else:
169 yield type(self)(self, forceAbsolute=False, forceDirectory=True), dirs, files
171 for dir in dirs:
172 new_uri = self.join(dir, forceDirectory=True)
173 yield from new_uri.walk(file_filter)