summaryrefslogtreecommitdiff
path: root/qutebrowser/utils/resources.py
blob: 60d90fd31c9ba0286fa1625e3b3ac6cc0cec4c42 (plain)
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
# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""Resources related utilities."""

import os.path
import sys
import contextlib
import posixpath
import pathlib
from typing import Iterator, Iterable, Union


# We cannot use the stdlib version on 3.8 because we need the files() API.
if sys.version_info >= (3, 11):  # pragma: no cover
    # https://github.com/python/cpython/issues/90276
    import importlib.resources as importlib_resources
    from importlib.resources.abc import Traversable
elif sys.version_info >= (3, 9):
    import importlib.resources as importlib_resources
    from importlib.abc import Traversable
else:  # pragma: no cover
    import importlib_resources
    from importlib_resources.abc import Traversable

import qutebrowser
_cache = {}


_ResourceType = Union[Traversable, pathlib.Path]


def _path(filename: str) -> _ResourceType:
    """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: _ResourceType,
    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):
        assert isinstance(glob_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 other importlib_resources.abc.Traversable
        assert glob_path.is_dir(), glob_path
        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()