diff options
Diffstat (limited to 'qutebrowser/utils/utils.py')
-rw-r--r-- | qutebrowser/utils/utils.py | 200 |
1 files changed, 68 insertions, 132 deletions
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 698a608ef..03a3c7842 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -30,14 +30,13 @@ import datetime import traceback import functools import contextlib -import posixpath import shlex import mimetypes -import pathlib import ctypes import ctypes.util -from typing import (Any, Callable, IO, Iterator, Optional, Sequence, Tuple, Type, Union, - Iterable, TypeVar, TYPE_CHECKING) +from typing import (Any, Callable, IO, Iterator, + Optional, Sequence, Tuple, Type, Union, + TypeVar, TYPE_CHECKING) try: # Protocol was added in Python 3.8 from typing import Protocol @@ -50,11 +49,7 @@ except ImportError: # pragma: no cover from PyQt5.QtCore import QUrl, QVersionNumber, QRect from PyQt5.QtGui import QClipboard, QDesktopServices from PyQt5.QtWidgets import QApplication -# 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 yaml try: from yaml import (CSafeLoader as YamlLoader, @@ -65,13 +60,10 @@ except ImportError: # pragma: no cover SafeDumper as YamlDumper) YAML_C_EXT = False -import qutebrowser from qutebrowser.utils import log - fake_clipboard = None log_clipboard = False -_resource_cache = {} is_mac = sys.platform.startswith('darwin') is_linux = sys.platform.startswith('linux') @@ -92,26 +84,74 @@ class Comparable(Protocol): ... -if TYPE_CHECKING: - class VersionNumber(Comparable, QVersionNumber): +class VersionNumber: + + """A representation of a version number.""" + + def __init__(self, *args: int) -> None: + self._ver = QVersionNumber(*args) + if self._ver.isNull(): + raise ValueError("Can't construct a null version") + + normalized = self._ver.normalized() + if normalized != self._ver: + raise ValueError( + f"Refusing to construct non-normalized version from {args} " + f"(normalized: {tuple(normalized.segments())}).") + + self.major = self._ver.majorVersion() + self.minor = self._ver.minorVersion() + self.patch = self._ver.microVersion() + self.segments = self._ver.segments() + + assert len(self.segments) <= 3, self.segments + + def __str__(self) -> str: + return ".".join(str(s) for s in self.segments) + + def __repr__(self) -> str: + args = ", ".join(str(s) for s in self.segments) + return f'VersionNumber({args})' + + def strip_patch(self) -> 'VersionNumber': + """Get a new VersionNumber with the patch version removed.""" + return VersionNumber(*self.segments[:2]) + + @classmethod + def parse(cls, s: str) -> 'VersionNumber': + """Parse a version number from a string.""" + ver, _suffix = QVersionNumber.fromString(s) + # FIXME: Should we support a suffix? + + if ver.isNull(): + raise ValueError(f"Failed to parse {s}") - """WORKAROUND for incorrect PyQt stubs.""" -else: - class VersionNumber(QVersionNumber): + return cls(*ver.normalized().segments()) - """We can't inherit from Protocol and QVersionNumber at runtime.""" + def __hash__(self) -> int: + return hash(self._ver) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - normalized = self.normalized() - if normalized != self: - raise ValueError( - f"Refusing to construct non-normalized version from {args} " - f"(normalized: {tuple(normalized.segments())}).") + def __eq__(self, other: object) -> bool: + if not isinstance(other, VersionNumber): + return NotImplemented + return self._ver == other._ver - def __repr__(self): - args = ", ".join(str(s) for s in self.segments()) - return f'VersionNumber({args})' + def __ne__(self, other: object) -> bool: + if not isinstance(other, VersionNumber): + return NotImplemented + return self._ver != other._ver + + def __ge__(self, other: 'VersionNumber') -> bool: + return self._ver >= other._ver # type: ignore[operator] + + def __gt__(self, other: 'VersionNumber') -> bool: + return self._ver > other._ver # type: ignore[operator] + + def __le__(self, other: 'VersionNumber') -> bool: + return self._ver <= other._ver # type: ignore[operator] + + def __lt__(self, other: 'VersionNumber') -> bool: + return self._ver < other._ver # type: ignore[operator] class Unreachable(Exception): @@ -196,110 +236,6 @@ def compact_text(text: str, elidelength: int = None) -> str: return out -def _resource_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 _resource_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_resources( - 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 - path = resource_path / subdir - - if isinstance(resource_path, pathlib.Path): - for full_path in 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 path.is_dir(), path # type: ignore[unreachable] - for subpath in path.iterdir(): - if subpath.name.endswith(ext): - yield posixpath.join(subdir, subpath.name) - - -def preload_resources() -> None: - """Load resource files into the cache.""" - resource_path = _resource_path('') - for subdir, ext in [ - ('html', '.html'), - ('javascript', '.js'), - ('javascript/quirks', '.js'), - ]: - for name in _glob_resources(resource_path, subdir, ext): - _resource_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 _resource_cache: - return _resource_cache[filename] - - path = _resource_path(filename) - with _resource_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 = _resource_path(filename) - with _resource_keyerror_workaround(): - return path.read_bytes() - - -def parse_version(version: str) -> VersionNumber: - """Parse a version string.""" - ver, _suffix = QVersionNumber.fromString(version) - return VersionNumber(ver.normalized()) - - def format_seconds(total_seconds: int) -> str: """Format a count of seconds to get a [H:]M:SS string.""" prefix = '-' if total_seconds < 0 else '' |