Coverage for python/lsst/resources/packageresource.py: 98%
90 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-19 11:17 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-19 11:17 +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
31from typing import TYPE_CHECKING
33from ._resourceHandles._baseResourceHandle import ResourceHandleProtocol
34from ._resourcePath import ResourcePath
36if TYPE_CHECKING:
37 import urllib.parse
39log = logging.getLogger(__name__)
42class PackageResourcePath(ResourcePath):
43 """URI referring to a Python package resource.
45 These URIs look like: ``resource://lsst.daf.butler/configs/file.yaml``
46 where the network location is the Python package and the path is the
47 resource name.
48 """
50 @classmethod
51 def _fixDirectorySep(
52 cls, parsed: urllib.parse.ParseResult, forceDirectory: bool = False
53 ) -> tuple[urllib.parse.ParseResult, bool]:
54 """Ensure that a path separator is present on directory paths."""
55 parsed, dirLike = super()._fixDirectorySep(parsed, forceDirectory=forceDirectory)
56 if not dirLike:
57 try:
58 # If the resource location does not exist this can
59 # fail immediately. It is possible we are doing path
60 # manipulation and not wanting to read the resource now,
61 # so catch the error and move on.
62 ref = resources.files(parsed.netloc).joinpath(parsed.path.lstrip("/"))
63 except ModuleNotFoundError:
64 pass
65 else:
66 dirLike = ref.is_dir()
67 return parsed, dirLike
69 def _get_ref(self) -> resources.abc.Traversable | None:
70 """Obtain the object representing the resource.
72 Returns
73 -------
74 path : `resources.abc.Traversable` or `None`
75 The reference to the resource path, or `None` if the module
76 associated with the resources is not accessible. This can happen
77 if Python can't import the Python package defining the resource.
78 """
79 try:
80 ref = resources.files(self.netloc).joinpath(self.relativeToPathRoot)
81 except ModuleNotFoundError:
82 return None
83 return ref
85 def isdir(self) -> bool:
86 """Return True if this URI is a directory, else False."""
87 if self.dirLike: # Always bypass if we guessed the resource is a directory.
88 return True
89 ref = self._get_ref()
90 if ref is None:
91 return False # Does not seem to exist so assume not a directory.
92 return ref.is_dir()
94 def exists(self) -> bool:
95 """Check that the python resource exists."""
96 ref = self._get_ref()
97 if ref is None:
98 return False
99 return ref.is_file() or ref.is_dir()
101 def read(self, size: int = -1) -> bytes:
102 ref = self._get_ref()
103 if not ref:
104 raise FileNotFoundError(f"Unable to locate resource {self}.")
105 with ref.open("rb") as fh:
106 return fh.read(size)
108 @contextlib.contextmanager
109 def as_local(self) -> Iterator[ResourcePath]:
110 """Return the location of the Python resource as local file.
112 Yields
113 ------
114 local : `ResourcePath`
115 This might be the original resource or a copy on the local file
116 system.
118 Notes
119 -----
120 The context manager will automatically delete any local temporary
121 file.
123 Examples
124 --------
125 Should be used as a context manager:
127 .. code-block:: py
129 with uri.as_local() as local:
130 ospath = local.ospath
131 """
132 ref = self._get_ref()
133 if ref is None:
134 raise FileNotFoundError(f"Resource {self} could not be located.")
135 if ref.is_dir():
136 raise IsADirectoryError(f"Directory-like URI {self} cannot be fetched as local.")
138 with resources.as_file(ref) as file:
139 yield ResourcePath(file)
141 @contextlib.contextmanager
142 def open(
143 self,
144 mode: str = "r",
145 *,
146 encoding: str | None = None,
147 prefer_file_temporary: bool = False,
148 ) -> Iterator[ResourceHandleProtocol]:
149 # Docstring inherited.
150 if "r" not in mode or "+" in mode:
151 raise RuntimeError(f"Package resource URI {self} is read-only.")
152 ref = self._get_ref()
153 if ref is None:
154 raise FileNotFoundError(f"Could not open resource {self}.")
155 with ref.open(mode, encoding=encoding) as buffer:
156 yield buffer
158 def walk(
159 self, file_filter: str | re.Pattern | None = None
160 ) -> Iterator[list | tuple[ResourcePath, list[str], list[str]]]:
161 # Docstring inherited.
162 if not self.isdir():
163 raise ValueError(f"Can not walk a non-directory URI: {self}")
165 if isinstance(file_filter, str):
166 file_filter = re.compile(file_filter)
168 ref = self._get_ref()
169 if ref is None:
170 raise ValueError(f"Unable to find resource {self}.")
172 files: list[str] = []
173 dirs: list[str] = []
174 for item in ref.iterdir():
175 if item.is_dir():
176 dirs.append(item.name)
177 elif item.is_file(): 177 ↛ 174line 177 didn't jump to line 174, because the condition on line 177 was never false
178 files.append(item.name)
179 # If the item wasn't covered by one of the cases above that
180 # means it was deleted concurrently with this walk or is
181 # not a plain file/directory/symlink
183 if file_filter is not None:
184 files = [f for f in files if file_filter.search(f)]
186 if not dirs and not files:
187 return
188 else:
189 yield type(self)(self, forceAbsolute=False, forceDirectory=True), dirs, files
191 for dir in dirs:
192 new_uri = self.join(dir, forceDirectory=True)
193 yield from new_uri.walk(file_filter)