summaryrefslogtreecommitdiff
path: root/qutebrowser/utils/resources.py
diff options
context:
space:
mode:
Diffstat (limited to 'qutebrowser/utils/resources.py')
-rw-r--r--qutebrowser/utils/resources.py133
1 files changed, 133 insertions, 0 deletions
diff --git a/qutebrowser/utils/resources.py b/qutebrowser/utils/resources.py
new file mode 100644
index 000000000..e812ece99
--- /dev/null
+++ b/qutebrowser/utils/resources.py
@@ -0,0 +1,133 @@
+# 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
+
+ if hasattr(sys, 'frozen'):
+ # For PyInstaller, where we can't store resource files in a qutebrowser/ folder
+ # because the executable is already named "qutebrowser" (at least on macOS).
+ return pathlib.Path(sys.executable).parent / 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 zipfiel stubs (correctly) only declare zipfile.Path with
+ # Python 3.8...
+ assert glob_path.is_dir(), 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]
+
+ file_path = path(filename)
+ with keyerror_workaround():
+ return file_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.
+ """
+ file_binary_path = path(filename)
+ with keyerror_workaround():
+ return file_binary_path.read_bytes()