diff --git a/qubes_config/global_config/rule_list_widgets.py b/qubes_config/global_config/rule_list_widgets.py index ef86762..a21eabb 100644 --- a/qubes_config/global_config/rule_list_widgets.py +++ b/qubes_config/global_config/rule_list_widgets.py @@ -261,7 +261,7 @@ def _combobox_changed(self, *_args): self.callback() def _format_new_value(self, new_value): - self.name_widget.set_markup(f"{self.choices[new_value]}") + self.name_widget.set_text(f"{self.choices[new_value]}") if self.verb_description: self.additional_text_widget.set_text( self.verb_description.get_verb_for_action_and_target( diff --git a/qubes_config/global_config/thisdevice_handler.py b/qubes_config/global_config/thisdevice_handler.py index c6e4f2e..8cbf8ac 100644 --- a/qubes_config/global_config/thisdevice_handler.py +++ b/qubes_config/global_config/thisdevice_handler.py @@ -23,13 +23,14 @@ import qubesadmin.vm from ..widgets.gtk_utils import show_error, load_icon, copy_to_global_clipboard +from ..widgets.gtk_utils import markup_format from .page_handler import PageHandler from .policy_manager import PolicyManager import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk +from gi.repository import Gtk, GLib import gettext @@ -126,7 +127,7 @@ def __init__( ).decode() except subprocess.CalledProcessError as ex: label_text += _("Failed to load system data: {ex}\n").format( - ex=str(ex) + ex=GLib.markup_escape_text(str(ex)) ) self.hcl_check = "" @@ -141,8 +142,9 @@ def __init__( label_text += _("Failed to load system data.\n") self.data_label.get_style_context().add_class("red_code") - label_text += _( - """Brand: {brand} + label_text += markup_format( + _( + """Brand: {brand} Model: {model} CPU: {cpu} @@ -156,7 +158,7 @@ def __init__( Kernel: {kernel_ver} Xen: {xen_ver} """ - ).format( + ), brand=self._get_data("brand"), model=self._get_data("model"), cpu=self._get_data("cpu"), @@ -169,16 +171,18 @@ def __init__( xen_ver=self._get_version("xen"), ) self.set_state(self.compat_hvm_image, self._get_data("hvm")) - self.compat_hvm_label.set_markup(f"HVM: {self._get_data('hvm')}") + self.compat_hvm_label.set_markup( + markup_format("HVM: {}", self._get_data("hvm")) + ) self.set_state(self.compat_iommu_image, self._get_data("iommu")) self.compat_iommu_label.set_markup( - f"I/O MMU: {self._get_data('iommu')}" + markup_format("I/O MMU: {}", self._get_data("iommu")) ) self.set_state(self.compat_hap_image, self._get_data("slat")) self.compat_hap_label.set_markup( - f"HAP/SLAT: {self._get_data('slat')}" + markup_format("HAP/SLAT: {}", self._get_data("slat")) ) self.set_state( @@ -201,7 +205,7 @@ def __init__( self.set_state(self.compat_remapping_image, self._get_data("remap")) self.compat_remapping_label.set_markup( - f"Remapping: {self._get_data('remap')}" + markup_format(_("Remapping: {}"), self._get_data("remap")) ) self.set_policy_state() @@ -218,7 +222,7 @@ def __init__( ) self.compat_pv_tooltip.set_tooltip_markup( _("The following qubes have PV virtualization mode:\n - ") - + "\n - ".join([vm.name for vm in pv_vms]) + + "\n - ".join([GLib.markup_escape_text(vm.name) for vm in pv_vms]) ) self.compat_pv_tooltip.set_visible(bool(pv_vms)) diff --git a/qubes_config/global_config/usb_devices.py b/qubes_config/global_config/usb_devices.py index db3d834..bf03f52 100644 --- a/qubes_config/global_config/usb_devices.py +++ b/qubes_config/global_config/usb_devices.py @@ -554,7 +554,7 @@ def load_rules_for_usb_qube(self): def disable_u2f(self, reason: str): self.problem_fatal_box.set_visible(True) self.problem_fatal_box.show_all() - self.problem_fatal_label.set_markup(reason) + self.problem_fatal_label.set_text(reason) self.enable_check.set_active(False) self.enable_check.set_sensitive(False) self.box.set_visible(False) diff --git a/qubes_config/widgets/gtk_utils.py b/qubes_config/widgets/gtk_utils.py index 9317521..be8b626 100644 --- a/qubes_config/widgets/gtk_utils.py +++ b/qubes_config/widgets/gtk_utils.py @@ -88,6 +88,22 @@ def load_icon(icon_name: str, width: int = 24, height: int = 24): return pixbuf +def _escape_str(s: Union[str, float, int]) -> Union[str, float, int]: + # pylint: disable=unidiomatic-typecheck + if type(s) is str: + return GLib.markup_escape_text(s) + # pylint: disable=unidiomatic-typecheck + if type(s) in (float, int, bool): + return s + raise TypeError(f"Unsupported input type {type(s)}") + + +def markup_format(s, *args, **kwargs) -> str: + escaped_args = [_escape_str(i) for i in args] + escaped_kwargs = {k: _escape_str(v) for k, v in kwargs.items()} + return s.format(*escaped_args, **escaped_kwargs) + + def show_error(parent, title, text): """ Helper function to display error messages. @@ -184,7 +200,7 @@ def show_dialog( if isinstance(text, str): label: Gtk.Label = Gtk.Label() - label.set_markup(text) + label.set_text(text) label.set_line_wrap_mode(Gtk.WrapMode.WORD) label.set_max_width_chars(200) label.set_xalign(0) diff --git a/qui/clipboard.py b/qui/clipboard.py index 0118bf9..3ea7007 100644 --- a/qui/clipboard.py +++ b/qui/clipboard.py @@ -48,7 +48,7 @@ t = gettext.translation("desktop-linux-manager", fallback=True) _ = t.gettext -from .utils import run_asyncio_and_show_errors +from .utils import run_asyncio_and_show_errors, markup_format gbulb.install() @@ -127,19 +127,24 @@ def _copy(self, metadata: dict) -> None: size = clipboard_formatted_size(metadata["sent_size"]) if metadata["malformed_request"]: - body = ERROR_MALFORMED_DATA.format(vmname=metadata["vmname"]) + body = markup_format( + ERROR_MALFORMED_DATA, vmname=metadata["vmname"] + ) icon = "dialog-error" elif ( metadata["qrexec_clipboard"] and metadata["sent_size"] >= metadata["buffer_size"] ): # Microsoft Windows clipboard case - body = WARNING_POSSIBLE_TRUNCATION.format( - vmname=metadata["vmname"], size=size + body = markup_format( + WARNING_POSSIBLE_TRUNCATION, + vmname=metadata["vmname"], + size=size, ) icon = "dialog-warning" elif metadata["oversized_request"]: - body = ERROR_OVERSIZED_DATA.format( + body = markup_format( + ERROR_OVERSIZED_DATA, vmname=metadata["vmname"], size=size, limit=clipboard_formatted_size(metadata["buffer_size"]), @@ -150,13 +155,16 @@ def _copy(self, metadata: dict) -> None: and metadata["cleared"] and metadata["sent_size"] == 0 ): - body = WARNING_EMPTY_CLIPBOARD.format(vmname=metadata["vmname"]) + body = markup_format( + WARNING_EMPTY_CLIPBOARD, vmname=metadata["vmname"] + ) icon = "dialog-warning" elif not metadata["successful"]: - body = ERROR_ON_COPY.format(vmname=metadata["vmname"]) + body = markup_format(ERROR_ON_COPY, vmname=metadata["vmname"]) icon = "dialog-error" else: - body = MSG_COPY_SUCCESS.format( + body = markup_format( + MSG_COPY_SUCCESS, vmname=metadata["vmname"], size=size, shortcut=self.gtk_app.paste_shortcut, @@ -173,14 +181,15 @@ def _copy(self, metadata: dict) -> None: def _paste(self, metadata: dict) -> None: """Sends Paste notification via Gio.Notification.""" if not metadata["successful"] or metadata["malformed_request"]: - body = ERROR_ON_PASTE.format(vmname=metadata["vmname"]) + body = markup_format(ERROR_ON_PASTE, vmname=metadata["vmname"]) body += MSG_WIPED icon = "dialog-error" elif ( "protocol_version_xside" in metadata.keys() and metadata["protocol_version_xside"] >= 0x00010008 ): - body = MSG_PASTE_SUCCESS_METADATA.format( + body = markup_format( + MSG_PASTE_SUCCESS_METADATA, size=clipboard_formatted_size(metadata["sent_size"]), vmname=metadata["vmname"], ) @@ -355,9 +364,11 @@ def update_clipboard_contents( else: self.clipboard_label.set_markup( - _( - "Global clipboard contents: {0} from {1}" - ).format(size, vm) + markup_format( + _("Global clipboard contents: {0} from {1}"), + size, + vm, + ) ) self.icon.set_from_icon_name("edit-copy") @@ -391,10 +402,14 @@ def setup_ui(self, *_args, **_kwargs): help_label = Gtk.Label(xalign=0) help_label.set_markup( - _( - "Use {copy} to copy and " - "{paste} to paste." - ).format(copy=self.copy_shortcut, paste=self.paste_shortcut) + markup_format( + _( + "Use {copy} to copy and " + "{paste} to paste." + ), + copy=self.copy_shortcut, + paste=self.paste_shortcut, + ) ) help_item = Gtk.MenuItem() help_item.set_margin_left(10) @@ -442,9 +457,11 @@ def copy_dom0_clipboard(self, *_args, **_kwargs): '"protocol_version_xside":65544,\n' '"protocol_version_vmside":65544,\n' "}}\n".format( - xevent_timestamp=str(Gtk.get_current_event_time()), - sent_size=os.path.getsize(DATA), - buffer_size="256000", + xevent_timestamp=json.dumps( + Gtk.get_current_event_time() + ), + sent_size=json.dumps(os.path.getsize(DATA)), + buffer_size=json.dumps(256000), ) ) except Exception: # pylint: disable=broad-except diff --git a/qui/decorators.py b/qui/decorators.py index 39faf61..428034d 100644 --- a/qui/decorators.py +++ b/qui/decorators.py @@ -10,6 +10,7 @@ from gi.repository import Gtk, Pango, GLib, GdkPixbuf # isort:skip from qubesadmin import exc from qubesadmin.utils import size_to_human +from .utils import markup_format import gettext @@ -146,12 +147,13 @@ def update_tooltip(self, netvm_changed=False, storage_changed=False): else: perc_storage = self.cur_storage / self.max_storage - tooltip += _( - "\nTemplate: {template}" - "\nNetworking: {netvm}" - "\nPrivate storage: {current_storage:.2f}GB/" - "{max_storage:.2f}GB ({perc_storage:.1%})" - ).format( + tooltip += markup_format( + _( + "\nTemplate: {template}" + "\nNetworking: {netvm}" + "\nPrivate storage: {current_storage:.2f}GB/" + "{max_storage:.2f}GB ({perc_storage:.1%})" + ), template=self.template_name, netvm=self.netvm_name, current_storage=self.cur_storage, @@ -193,7 +195,8 @@ def update_state(self, cpu=0, header=False): .get_color(Gtk.StateFlags.INSENSITIVE) .to_color() ) - markup = f'0%' + escaped_color = GLib.markup_escape_text(color.to_string()) + markup = f'0%' self.cpu_label.set_markup(markup) @@ -264,8 +267,9 @@ def device_hbox(device) -> Gtk.Box: name_label = Gtk.Label(xalign=0) name = f"{device.backend_domain}:{device.port_id} - {device.description}" if device.attachments: - dev_list = ", ".join(list(device.attachments)) - name_label.set_markup(f"{name} ({dev_list})") + dev_list = GLib.markup_escape_text(", ".join(list(device.attachments))) + name_escaped = GLib.markup_escape_text(name) + name_label.set_markup(f"{name_escaped} ({dev_list})") else: name_label.set_text(name) name_label.set_max_width_chars(64) @@ -296,7 +300,7 @@ def device_domain_hbox(vm, attached: bool) -> Gtk.Box: name = Gtk.Label(xalign=0) if attached: - name.set_markup(f"{vm.vm_name}") + name.set_markup(f"{GLib.markup_escape_text(vm.vm_name)}") else: name.set_text(vm.vm_name) diff --git a/qui/devices/actionable_widgets.py b/qui/devices/actionable_widgets.py index 812337b..30129dc 100644 --- a/qui/devices/actionable_widgets.py +++ b/qui/devices/actionable_widgets.py @@ -145,7 +145,7 @@ def __init__( self.backend_label = Gtk.Label(xalign=0) backend_label: str = vm.name if name_extension: - backend_label += ": " + name_extension + backend_label += ": " + GLib.markup_escape_text(name_extension) self.backend_label.set_markup(backend_label) self.pack_start(self.backend_icon, False, False, 4) @@ -250,7 +250,9 @@ def __init__( self, vm: backend.VM, device: backend.Device, variant: str = "dark" ): super().__init__( - "detach", "Detach from " + vm.name + "", variant + "detach", + "Detach from " + GLib.markup_escape_text(vm.name) + "", + variant, ) self.vm = vm self.device = device @@ -383,14 +385,14 @@ def __init__(self, device: backend.Device, variant: str = "dark"): super().__init__(orientation=Gtk.Orientation.VERTICAL) # FUTURE: this is proposed layout for new API # self.device_label = Gtk.Label() - # self.device_label.set_markup(device.name) + # self.device_label.set_text(device.name) # self.device_label.get_style_context().add_class('device_name') # self.edit_icon = VariantIcon('edit', 'dark', 24) # self.detailed_description_label = Gtk.Label() # self.detailed_description_label.set_text(device.description) # self.backend_icon = VariantIcon(device.vm_icon, 'dark', 24) # self.backend_label = Gtk.Label(xalign=0) - # self.backend_label.set_markup(str(device.backend_domain)) + # self.backend_label.set_text(str(device.backend_domain)) # # self.title_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) # self.title_box.add(self.device_label) @@ -405,7 +407,7 @@ def __init__(self, device: backend.Device, variant: str = "dark"): # self.add(self.attachment_box) self.device_label = Gtk.Label() - self.device_label.set_markup(device.name) + self.device_label.set_text(device.name) self.device_label.get_style_context().add_class("device_name") self.device_label.set_xalign(Gtk.Align.CENTER) self.device_label.set_halign(Gtk.Align.CENTER) @@ -447,7 +449,7 @@ def __init__(self, device: backend.Device, variant: str = "dark"): self.device_label = Gtk.Label(xalign=0) - label_markup = device.name + label_markup = GLib.markup_escape_text(device.name) if ( device.connection_timestamp and int(time.monotonic() - device.connection_timestamp) < 120 diff --git a/qui/tray/disk_space.py b/qui/tray/disk_space.py index 6e61e0f..e17a318 100644 --- a/qui/tray/disk_space.py +++ b/qui/tray/disk_space.py @@ -29,7 +29,7 @@ def __init__(self, vm): self.check_usage() - def check_usage(self): + def check_usage(self) -> None: self.problem_volumes = {} volumes_to_check = ["private"] if not hasattr(self.vm, "template"): @@ -52,7 +52,7 @@ def __init__(self, qubes_app): self.__populate_vms() - def __populate_vms(self): + def __populate_vms(self) -> None: for vm in self.qubes_app.domains: try: if vm.is_running(): @@ -86,10 +86,14 @@ def __create_widgets(vm_usage): for volume_name, usage in vm_usage.problem_volumes.items(): # pylint: disable=consider-using-f-string label_contents.append( - _("volume {} is {:.1%} full").format(volume_name, usage) + _("volume {} is {:.1%} full").format( + GLib.markup_escape_text(volume_name), usage + ) ) - label_text = f"{vm.name}: " + ", ".join(label_contents) + label_text = f"{GLib.markup_escape_text(vm.name)}: " + ", ".join( + label_contents + ) label_widget.set_markup(label_text) return vm, icon_img, label_widget @@ -252,13 +256,21 @@ def __create_box(pool: PoolWrapper): if pool.has_error: # Pool with errors - formatted_name = f"{pool.name}" + formatted_name = ( + '' + + GLib.markup_escape_text(pool.name) + + "" + ) elif pool.size and "included_in" not in pool.config: # normal pool - formatted_name = f"{pool.name}" + formatted_name = f"{GLib.markup_escape_text(pool.name)}" else: # pool without data or included in another pool - formatted_name = f"{pool.name}" + formatted_name = ( + "" + + GLib.markup_escape_text(pool.name) + + "" + ) pool_name.set_markup(formatted_name) pool_name.set_margin_left(20) @@ -270,20 +282,20 @@ def __create_box(pool: PoolWrapper): if pool.has_error: error_desc = Gtk.Label(xalign=0) - error_desc.set_markup("Error accessing pool data") + error_desc.set_text("Error accessing pool data") error_desc.set_margin_left(40) name_box.pack_start(error_desc, True, True, 0) return name_box, percentage_box, usage_box data_name = Gtk.Label(xalign=0) - data_name.set_markup("data") + data_name.set_text("data") data_name.set_margin_left(40) name_box.pack_start(data_name, True, True, 0) if pool.metadata_perc: metadata_name = Gtk.Label(xalign=0) - metadata_name.set_markup("metadata") + metadata_name.set_text("metadata") metadata_name.set_margin_left(40) name_box.pack_start(metadata_name, True, True, 0) @@ -423,13 +435,15 @@ def set_icon_state(self, pool_warning=None, vm_warning=None): self.icon.set_from_icon_name("dialog-warning") text = _("Qubes Disk Space Monitor\n\nWARNING!") if pool_warning: - text += _("\nYou are running out of disk space.\n") + "".join( - pool_warning + text += GLib.markup_escape_text( + _("\nYou are running out of disk space.\n") + + "".join(pool_warning) ) if vm_warning: - text += _( - "\nThe following qubes are running out of space: " - ) + ", ".join([x.vm.name for x in vm_warning]) + text += GLib.markup_escape_text( + _("\nThe following qubes are running out of space: ") + + ", ".join([x.vm.name for x in vm_warning]) + ) self.icon.set_tooltip_markup(text) else: self.icon.set_from_icon_name("drive-harddisk") @@ -484,7 +498,7 @@ def make_menu(self, _unused, _event): @staticmethod def make_title_item(text): label = Gtk.Label(xalign=0) - label.set_markup(_("{}").format(text)) + label.set_markup("{}".format(GLib.markup_escape_text(text))) menu_item = Gtk.MenuItem() menu_item.add(label) menu_item.set_sensitive(False) diff --git a/qui/tray/domains.py b/qui/tray/domains.py index 8e5fa3d..81e816a 100644 --- a/qui/tray/domains.py +++ b/qui/tray/domains.py @@ -80,7 +80,7 @@ def show_error(title, text): None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK ) dialog.set_title(title) - dialog.set_markup(text) + dialog.set_markup(GLib.markup_escape_text(text)) dialog.connect("response", lambda *x: dialog.destroy()) GLib.idle_add(dialog.show) @@ -232,7 +232,7 @@ class PreferencesItem(VMActionMenuItem): def __init__(self, vm, icon_cache): super().__init__(vm, icon_cache, "preferences", _("Settings")) - def perform_action(self): + def perform_action(self) -> None: # pylint: disable=consider-using-with subprocess.Popen(["qubes-vm-settings", self.vm.name]) @@ -251,7 +251,7 @@ def __init__(self, name, path): self.connect("activate", self.launch_log_viewer) - def launch_log_viewer(self, *_args, **_kwargs): + def launch_log_viewer(self, *_args, **_kwargs) -> None: # pylint: disable=consider-using-with subprocess.Popen(["qubes-log-viewer", self.path]) @@ -270,7 +270,7 @@ def __init__(self, vm, icon_cache): self.connect("activate", self.run_terminal) - def run_terminal(self, _item): + def run_terminal(self, _item) -> None: try: self.vm.run_service("qubes.StartApp+qubes-run-terminal") except exc.QubesException as ex: @@ -318,11 +318,15 @@ class InternalInfoItem(Gtk.MenuItem): def __init__(self): super().__init__() self.label = Gtk.Label(xalign=0) - self.label.set_markup(_("Internal qube")) + self.label.set_markup( + "" + GLib.markup_escape_text(_("Internal qube")) + "" + ) self.set_tooltip_text( - "Internal qubes are used by the operating system. Do not modify" - " them or run programs in them unless you really " - "know what you are doing." + _( + "Internal qubes are used by the operating system. Do not modify" + " them or run programs in them unless you really " + "know what you are doing." + ) ) self.add(self.label) self.set_sensitive(False) @@ -555,7 +559,9 @@ def update_state(self, state): colormap = {"Paused": "grey", "Crashed": "red", "Transient": "red"} if state in colormap: self.name.label.set_markup( - f"{self.vm.name}" + f"" + + GLib.markup_escape_text(self.vm.name) + + "" ) else: self.name.label.set_label(self.vm.name) diff --git a/qui/tray/updates.py b/qui/tray/updates.py index 37ce643..48b7355 100644 --- a/qui/tray/updates.py +++ b/qui/tray/updates.py @@ -15,7 +15,7 @@ import gi # isort:skip gi.require_version("Gtk", "3.0") # isort:skip -from gi.repository import Gtk, Gio # isort:skip +from gi.repository import Gtk, Gio, GLib # isort:skip import gbulb @@ -31,7 +31,7 @@ class TextItem(Gtk.MenuItem): def __init__(self, text): super().__init__() title_label = Gtk.Label() - title_label.set_markup(text) + title_label.set_markup("" + GLib.markup_escape_text(text) + "") title_label.set_halign(Gtk.Align.CENTER) title_label.set_justify(Gtk.Justification.CENTER) self.set_margin_left(10) @@ -91,7 +91,7 @@ def setup_menu(self): self.tray_menu.set_reserve_toggle_size(False) if self.vms_needing_update: - self.tray_menu.append(TextItem(_("Qube updates available!"))) + self.tray_menu.append(TextItem(_("Qube updates available!"))) self.tray_menu.append( RunItem( _( @@ -104,15 +104,21 @@ def setup_menu(self): if self.obsolete_vms: self.tray_menu.append( - TextItem(_("Some qubes are no longer supported!")) + TextItem(_("Some qubes are no longer supported!")) ) obsolete_text = ( - _( - "The following qubes are based on distributions " - "that are no longer supported:\n" + GLib.markup_escape_text( + _( + "The following qubes are based on distributions " + "that are no longer supported:\n" + ) + + ", ".join([str(vm) for vm in self.obsolete_vms]) + ) + + "\n" + + GLib.markup_escape_text( + _("Install new templates with Template Manager") ) - + ", ".join([str(vm) for vm in self.obsolete_vms]) - + _("\nInstall new templates with Template Manager") + + "" ) self.tray_menu.append( RunItem(obsolete_text, self.launch_template_manager) diff --git a/qui/utils.py b/qui/utils.py index 59b0062..4941d4d 100644 --- a/qui/utils.py +++ b/qui/utils.py @@ -24,6 +24,7 @@ import sys import traceback from html import escape +from typing import Union from qubesadmin import exc @@ -38,7 +39,7 @@ import gi # isort:skip gi.require_version("Gtk", "3.0") # isort:skip -from gi.repository import Gtk # isort:skip +from gi.repository import Gtk, GLib # isort:skip with importlib.resources.files("qui").joinpath("eol.json").open() as stream: EOL_DATES = json.load(stream) @@ -65,9 +66,9 @@ def run_asyncio_and_show_errors(loop, tasks, name, restart=True): message = _( "Whoops. A critical error in {} has occurred." " This is most likely a bug." - ).format(name) + ).format(escape(name)) if restart: - message += _(" {} will restart itself.").format(name) + message += escape(_(" {} will restart itself.").format(name)) for d in done: # pylint: disable=invalid-name try: @@ -82,7 +83,7 @@ def run_asyncio_and_show_errors(loop, tasks, name, restart=True): exc_value_descr = escape(str(exc_value)) traceback_descr = escape(traceback.format_exc(limit=10)) exc_description = "\n{}: {}\n{}".format( - exc_type.__name__, exc_value_descr, traceback_descr + escape(exc_type.__name__), exc_value_descr, traceback_descr ) dialog.format_secondary_markup(exc_description) dialog.run() @@ -105,6 +106,34 @@ def check_update(vm) -> bool: return False +def _escape_str(s: Union[str, float, int]) -> Union[str, float, int]: + if isinstance(s, str): + # GLib uses NUL-terminated strings + assert "\0" not in s, "NUL characters not supported" + return GLib.markup_escape_text(s) + # For correctness, this relies on str(s) never containing + # XML metacharacters, as these will not be escaped. + # This is guaranteed for 'float', 'int', and 'bool', + # but not for user-defined subclasses of these types. + # pylint: disable=unidiomatic-typecheck + if type(s) in (float, int, bool): + return s + v = str(s) + if GLib.markup_escape_text(v) != v: + raise ValueError( + "cannot handle subclass of 'float' or 'int' if " + "__str__() returns string needing escaping " + f"(value needing escaping is {v!r})" + ) + return v + + +def markup_format(s, *args, **kwargs) -> str: + escaped_args = [_escape_str(i) for i in args] + escaped_kwargs = {k: _escape_str(v) for k, v in kwargs.items()} + return s.format(*escaped_args, **escaped_kwargs) + + def check_support(vm) -> bool: """Return true if the given template/standalone vm is still supported, by default returns true"""