1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
"""Resources related utilities."""
import os.path
import sys
import contextlib
import posixpath
import pathlib
from typing import Iterator, Iterable
# We cannot use the stdlib version on 3.7-3.8 because we need the files() API.
if sys.version_info >= (3, 9):
import importlib.resources as importlib_resources
else: # pragma: no cover
import importlib_resources
import qutebrowser
_cache = {}
def _path(filename: str) -> pathlib.Path:
"""Get a pathlib.Path object for a resource."""
assert not posixpath.isabs(filename), filename
assert os.path.pardir not in filename.split(posixpath.sep), filename
return importlib_resources.files(qutebrowser) / filename
@contextlib.contextmanager
def _keyerror_workaround() -> Iterator[None]:
"""Re-raise KeyErrors as FileNotFoundErrors.
WORKAROUND for zipfile.Path resources raising KeyError when a file was notfound:
https://bugs.python.org/issue43063
Only needed for Python 3.8 and 3.9.
"""
try:
yield
except KeyError as e:
raise FileNotFoundError(str(e))
def _glob(
resource_path: pathlib.Path,
subdir: str,
ext: str,
) -> Iterable[str]:
"""Find resources with the given extension.
Yields a resource name like "html/log.html" (as string).
"""
assert '*' not in ext, ext
assert ext.startswith('.'), ext
glob_path = resource_path / subdir
if isinstance(resource_path, pathlib.Path):
for full_path in glob_path.glob(f'*{ext}'): # . is contained in ext
yield full_path.relative_to(resource_path).as_posix()
else: # zipfile.Path or importlib_resources compat object
# Unfortunately, we can't tell mypy about resource_path being of type
# Union[pathlib.Path, zipfile.Path] because we set "python_version = 3.6" in
# .mypy.ini, but the zipfile stubs (correctly) only declare zipfile.Path with
# Python 3.8...
assert glob_path.is_dir(), glob_path # type: ignore[unreachable]
for subpath in glob_path.iterdir():
if subpath.name.endswith(ext):
yield posixpath.join(subdir, subpath.name)
def preload() -> None:
"""Load resource files into the cache."""
resource_path = _path('')
for subdir, ext in [
('html', '.html'),
('javascript', '.js'),
('javascript/quirks', '.js'),
]:
for name in _glob(resource_path, subdir, ext):
_cache[name] = read_file(name)
def read_file(filename: str) -> str:
"""Get the contents of a file contained with qutebrowser.
Args:
filename: The filename to open as string.
Return:
The file contents as string.
"""
if filename in _cache:
return _cache[filename]
path = _path(filename)
with _keyerror_workaround():
return path.read_text(encoding='utf-8')
def read_file_binary(filename: str) -> bytes:
"""Get the contents of a binary file contained with qutebrowser.
Args:
filename: The filename to open as string.
Return:
The file contents as a bytes object.
"""
path = _path(filename)
with _keyerror_workaround():
return path.read_bytes()
|