Coverage for python/lsst/resources/packageresource.py: 99%
89 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-25 09:29 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-25 09:29 +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 """Read the contents of the resource."""
103 ref = self._get_ref()
104 if not ref:
105 raise FileNotFoundError(f"Unable to locate resource {self}.")
106 with ref.open("rb") as fh:
107 return fh.read(size)
109 @contextlib.contextmanager
110 def as_local(self) -> Iterator[ResourcePath]:
111 """Return the location of the Python resource as local file.
113 Yields
114 ------
115 local : `ResourcePath`
116 This might be the original resource or a copy on the local file
117 system.
119 Notes
120 -----
121 The context manager will automatically delete any local temporary
122 file.
124 Examples
125 --------
126 Should be used as a context manager:
128 .. code-block:: py
130 with uri.as_local() as local:
131 ospath = local.ospath
132 """
133 ref = self._get_ref()
134 if ref is None:
135 raise FileNotFoundError(f"Resource {self} could not be located.")
136 if ref.is_dir():
137 raise IsADirectoryError(f"Directory-like URI {self} cannot be fetched as local.")
139 with resources.as_file(ref) as file:
140 yield ResourcePath(file)
142 @contextlib.contextmanager
143 def open(
144 self,
145 mode: str = "r",
146 *,
147 encoding: str | None = None,
148 prefer_file_temporary: bool = False,
149 ) -> Iterator[ResourceHandleProtocol]:
150 # Docstring inherited.
151 if "r" not in mode or "+" in mode:
152 raise RuntimeError(f"Package resource URI {self} is read-only.")
153 ref = self._get_ref()
154 if ref is None:
155 raise FileNotFoundError(f"Could not open resource {self}.")
156 with ref.open(mode, encoding=encoding) as buffer:
157 yield buffer
159 def walk(
160 self, file_filter: str | re.Pattern | None = None
161 ) -> Iterator[list | tuple[ResourcePath, list[str], list[str]]]:
162 # Docstring inherited.
163 if not self.isdir():
164 raise ValueError(f"Can not walk a non-directory URI: {self}")
166 if isinstance(file_filter, str):
167 file_filter = re.compile(file_filter)
169 ref = self._get_ref()
170 if ref is None:
171 raise ValueError(f"Unable to find resource {self}.")
173 files: list[str] = []
174 dirs: list[str] = []
175 for item in ref.iterdir():
176 if item.is_file():
177 files.append(item.name)
178 else:
179 # This is a directory.
180 dirs.append(item.name)
182 if file_filter is not None:
183 files = [f for f in files if file_filter.search(f)]
185 if not dirs and not files:
186 return
187 else:
188 yield type(self)(self, forceAbsolute=False, forceDirectory=True), dirs, files
190 for dir in dirs:
191 new_uri = self.join(dir, forceDirectory=True)
192 yield from new_uri.walk(file_filter)