mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Images can now be embedded as PNG or JPG
This commit is contained in:
parent
e4cac1c346
commit
7bef122896
17 changed files with 333 additions and 22 deletions
|
|
@ -1,7 +1,14 @@
|
|||
0.3.1 - (not released yet)
|
||||
==========================
|
||||
|
||||
tbd
|
||||
Added
|
||||
-----
|
||||
|
||||
* Embedded images can now be JPG or PNG. By default, small images and
|
||||
images with an alpha channel will be stored as PNG, the rest as
|
||||
JPG. In the newly created settings dialog, this behaviour can be
|
||||
changed to always use PNG (the former behaviour) always JPG.
|
||||
|
||||
|
||||
0.3.0 - 2023-11-23
|
||||
==================
|
||||
|
|
|
|||
|
|
@ -29,19 +29,14 @@ Install additional development requirements::
|
|||
|
||||
Run unittests with::
|
||||
|
||||
pytest .
|
||||
pytest --cov .
|
||||
|
||||
This will also generate a coverage report: ``htmlcov/index.html``.
|
||||
|
||||
Run codechecks with::
|
||||
|
||||
flake8 .
|
||||
|
||||
Run unittests with coverage report::
|
||||
|
||||
coverage run --source=beeref -m pytest
|
||||
coverage html
|
||||
|
||||
If your browser doesn't open automatically, view ``htmlcov/index.html``.
|
||||
|
||||
Beeref files are sqlite databases, so they can be inspected with any sqlite browser.
|
||||
|
||||
For debugging options, run::
|
||||
|
|
|
|||
|
|
@ -64,9 +64,7 @@ Features
|
|||
Regarding the bee file format
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Currently, all images are embedded into the bee file as png files. While png is a lossless format, it may also produce larger file sizes than compressed jpg files, so bee files may become bigger than the imported images on their own. More embedding options are to come later.
|
||||
|
||||
The bee file format is a sqlite database inside which the images are stored in an sqlar table—meaning they can be extracted with the `sqlite command line program <https://www.sqlite.org/cli.html>`_::
|
||||
All images are embedded into the bee file as PNG or JPG. The bee file format is a sqlite database inside which the images are stored in an sqlar table—meaning they can be extracted with the `sqlite command line program <https://www.sqlite.org/cli.html>`_::
|
||||
|
||||
sqlite3 myfile.bee -Axv
|
||||
|
||||
|
|
|
|||
|
|
@ -278,6 +278,11 @@ actions = [
|
|||
'checkable': True,
|
||||
'callback': 'on_action_always_on_top',
|
||||
},
|
||||
{
|
||||
'id': 'settings',
|
||||
'text': '&Settings',
|
||||
'callback': 'on_action_settings',
|
||||
},
|
||||
{
|
||||
'id': 'open_settings_dir',
|
||||
'text': 'Open Settings Folder',
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ menu_structure = [
|
|||
{
|
||||
'menu': '&Settings',
|
||||
'items': [
|
||||
'settings',
|
||||
'open_settings_dir',
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -93,8 +93,23 @@ class CommandlineArgs:
|
|||
return getattr(self._args, name)
|
||||
|
||||
|
||||
class BeeSettingsEvents(QtCore.QObject):
|
||||
restore_defaults = QtCore.pyqtSignal()
|
||||
|
||||
|
||||
# We want to send and receive settings events globally, not per
|
||||
# BeeSettings instance. Since we can't instantiate BeeSettings
|
||||
# globally on module level (because the Qt app doesn't exist yet), we
|
||||
# use this events proxy
|
||||
settings_events = BeeSettingsEvents()
|
||||
|
||||
|
||||
class BeeSettings(QtCore.QSettings):
|
||||
|
||||
DEFAULTS = {
|
||||
'FileIO/image_storage_format': 'best',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
settings_format = QtCore.QSettings.Format.IniFormat
|
||||
settings_scope = QtCore.QSettings.Scope.UserScope
|
||||
|
|
@ -108,6 +123,18 @@ class BeeSettings(QtCore.QSettings):
|
|||
constants.APPNAME,
|
||||
constants.APPNAME)
|
||||
|
||||
def valueOrDefault(self, key, type=None):
|
||||
val = self.value(key, type)
|
||||
if val is None:
|
||||
val = self.DEFAULTS.get(key)
|
||||
return val
|
||||
|
||||
def restore_defaults(self):
|
||||
logger.debug('Restoring settings to defaults')
|
||||
for key in self.DEFAULTS.keys():
|
||||
self.remove(key)
|
||||
settings_events.restore_defaults.emit()
|
||||
|
||||
def fileName(self):
|
||||
return os.path.normpath(super().fileName())
|
||||
|
||||
|
|
|
|||
|
|
@ -274,13 +274,13 @@ class SQLiteIO:
|
|||
item.save_id = self.cursor.lastrowid
|
||||
|
||||
if hasattr(item, 'pixmap_to_bytes'):
|
||||
pixmap = item.pixmap_to_bytes()
|
||||
pixmap, imgformat = item.pixmap_to_bytes()
|
||||
|
||||
if item.filename:
|
||||
basename = os.path.splitext(os.path.basename(item.filename))[0]
|
||||
name = '%04d-%s.png' % (item.save_id, basename)
|
||||
name = f'{item.save_id:04}-{basename}.{imgformat}'
|
||||
else:
|
||||
name = '%04d.png' % item.save_id
|
||||
name = f'{item.save_id:04}.{imgformat}'
|
||||
|
||||
self.ex(
|
||||
'INSERT INTO sqlar (item_id, name, mode, sz, data) '
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets
|
|||
from PyQt6.QtCore import Qt
|
||||
|
||||
from beeref import commands
|
||||
from beeref.config import BeeSettings
|
||||
from beeref.constants import COLORS
|
||||
from beeref.selection import SelectableMixin
|
||||
|
||||
|
|
@ -91,6 +92,7 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
self.is_croppable = True
|
||||
self.crop_mode = False
|
||||
self.init_selectable()
|
||||
self.settings = BeeSettings()
|
||||
|
||||
@classmethod
|
||||
def create_from_data(self, **kwargs):
|
||||
|
|
@ -129,14 +131,32 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
self.crop.width(),
|
||||
self.crop.height()]}
|
||||
|
||||
def get_imgformat(self, img):
|
||||
"""Determines the format for storing this image."""
|
||||
|
||||
formt = self.settings.valueOrDefault('FileIO/image_storage_format')
|
||||
if formt not in ('png', 'jpg', 'best'):
|
||||
formt = 'best'
|
||||
|
||||
if formt == 'best':
|
||||
if (img.hasAlphaChannel()
|
||||
or (img.height() < 200 and img.width() < 200)):
|
||||
formt = 'png'
|
||||
else:
|
||||
formt = 'jpg'
|
||||
|
||||
logger.debug(f'Found format {formt} for {self}')
|
||||
return formt
|
||||
|
||||
def pixmap_to_bytes(self):
|
||||
"""Convert the pixmap data to PNG bytestring."""
|
||||
barray = QtCore.QByteArray()
|
||||
buffer = QtCore.QBuffer(barray)
|
||||
buffer.open(QtCore.QIODevice.OpenModeFlag.WriteOnly)
|
||||
img = self.pixmap().toImage()
|
||||
img.save(buffer, 'PNG')
|
||||
return barray.data()
|
||||
imgformat = self.get_imgformat(img)
|
||||
img.save(buffer, imgformat.upper())
|
||||
return (barray.data(), imgformat)
|
||||
|
||||
def setPixmap(self, pixmap):
|
||||
super().setPixmap(pixmap)
|
||||
|
|
|
|||
|
|
@ -384,6 +384,9 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
logger.info('User quit. Exiting...')
|
||||
self.app.quit()
|
||||
|
||||
def on_action_settings(self):
|
||||
widgets.settings.SettingsDialog(self)
|
||||
|
||||
def on_action_help(self):
|
||||
widgets.HelpDialog(self)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from PyQt6.QtCore import Qt
|
|||
from beeref import constants
|
||||
from beeref.config import logfile_name, BeeSettings
|
||||
from beeref.main_controls import MainControlsMixin
|
||||
from beeref.widgets import settings # noqa: F401
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -158,6 +159,7 @@ class HelpDialog(QtWidgets.QDialog):
|
|||
super().__init__(parent)
|
||||
self.setWindowTitle(f'{constants.APPNAME} Help')
|
||||
docdir = os.path.join(os.path.dirname(__file__),
|
||||
'..',
|
||||
'documentation')
|
||||
tabs = QtWidgets.QTabWidget()
|
||||
|
||||
128
beeref/widgets/settings.py
Normal file
128
beeref/widgets/settings.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# This file is part of BeeRef.
|
||||
#
|
||||
# BeeRef 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.
|
||||
#
|
||||
# BeeRef 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 BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from PyQt6 import QtWidgets
|
||||
|
||||
from beeref import constants
|
||||
from beeref.config import BeeSettings, settings_events
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageStorageFormatWidget(QtWidgets.QGroupBox):
|
||||
KEY = 'FileIO/image_storage_format'
|
||||
OPTIONS = (
|
||||
('best', 'Best Guess',
|
||||
('Small images and images with alpha channel are stored as png,'
|
||||
' everything else as jpg')),
|
||||
('png', 'Always PNG', 'Lossless, but large bee file'),
|
||||
('jpg', 'Always JPG',
|
||||
'Small bee file, but lossy and no transparency support'))
|
||||
|
||||
def __init__(self, parent):
|
||||
super().__init__('Image Storage Format:')
|
||||
parent.settings_widgets.append(self)
|
||||
self.settings = BeeSettings()
|
||||
settings_events.restore_defaults.connect(self.on_restore_defaults)
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
helptxt = QtWidgets.QLabel(
|
||||
'How images are stored inside bee files.'
|
||||
' Changes will only take effect on newly saved images.')
|
||||
helptxt.setWordWrap(True)
|
||||
layout.addWidget(helptxt)
|
||||
|
||||
self.ignore_values_changed = True
|
||||
self.buttons = {}
|
||||
for (value, label, helptext) in self.OPTIONS:
|
||||
btn = QtWidgets.QRadioButton(label)
|
||||
self.buttons[value] = btn
|
||||
btn.setToolTip(helptext)
|
||||
btn.toggled.connect(
|
||||
partial(self.on_values_changed, value=value, button=btn))
|
||||
if value == self.settings.valueOrDefault(self.KEY):
|
||||
btn.setChecked(True)
|
||||
layout.addWidget(btn)
|
||||
|
||||
self.ignore_values_changed = False
|
||||
|
||||
def on_values_changed(self, value, button):
|
||||
if self.ignore_values_changed:
|
||||
return
|
||||
|
||||
if value != self.settings.valueOrDefault(self.KEY):
|
||||
logger.debug(f'Setting {self.KEY} changed to: {value}')
|
||||
self.settings.setValue(self.KEY, value)
|
||||
|
||||
def on_restore_defaults(self):
|
||||
new_value = self.settings.valueOrDefault(self.KEY)
|
||||
self.ignore_values_changed = True
|
||||
for value, btn in self.buttons.items():
|
||||
btn.setChecked(value == new_value)
|
||||
self.ignore_values_changed = False
|
||||
|
||||
|
||||
class SettingsDialog(QtWidgets.QDialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f'{constants.APPNAME} Settings')
|
||||
tabs = QtWidgets.QTabWidget()
|
||||
|
||||
self.settings_widgets = []
|
||||
|
||||
# Miscellaneous
|
||||
misc = QtWidgets.QWidget()
|
||||
misc_layout = QtWidgets.QGridLayout()
|
||||
misc.setLayout(misc_layout)
|
||||
misc_layout.addWidget(ImageStorageFormatWidget(self), 0, 0)
|
||||
tabs.addTab(misc, '&Miscellaneous')
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
layout.addWidget(tabs)
|
||||
|
||||
# Bottom row of buttons
|
||||
buttons = QtWidgets.QWidget()
|
||||
btn_layout = QtWidgets.QHBoxLayout()
|
||||
buttons.setLayout(btn_layout)
|
||||
reset_btn = QtWidgets.QPushButton('&Restore Defaults')
|
||||
reset_btn.setAutoDefault(False)
|
||||
reset_btn.clicked.connect(self.on_restore_defaults)
|
||||
btn_layout.addWidget(reset_btn)
|
||||
|
||||
close_btn = QtWidgets.QPushButton('&Close')
|
||||
close_btn.setAutoDefault(True)
|
||||
close_btn.clicked.connect(self.on_close)
|
||||
btn_layout.addWidget(close_btn)
|
||||
btn_layout.insertStretch(1)
|
||||
|
||||
layout.addWidget(buttons)
|
||||
self.show()
|
||||
|
||||
def on_close(self, *args, **kwargs):
|
||||
self.close()
|
||||
|
||||
def on_restore_defaults(self, *args, **kwargs):
|
||||
reply = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
'Restore defaults?',
|
||||
'Do you want to restore all settings to their default values?')
|
||||
|
||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||
BeeSettings().restore_defaults()
|
||||
|
|
@ -6,3 +6,4 @@
|
|||
flake8==6.1.0
|
||||
pybadges==3.0.1
|
||||
yamllint==1.33.0
|
||||
pytest-cov==4.1.0
|
||||
|
|
|
|||
5
setup.cfg
Normal file
5
setup.cfg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[coverage:run]
|
||||
source = beeref
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --cov-report html --cov-config=setup.cfg
|
||||
|
|
@ -225,7 +225,7 @@ def test_sqliteio_write_inserts_new_text_item(tmpfile, view):
|
|||
assert result[9] is None
|
||||
|
||||
|
||||
def test_sqliteio_write_inserts_new_pixmap_item(tmpfile, view):
|
||||
def test_sqliteio_write_inserts_new_pixmap_item_png(tmpfile, view):
|
||||
item = BeePixmapItem(QtGui.QImage(), filename='bee.jpg')
|
||||
view.scene.addItem(item)
|
||||
item.setScale(1.3)
|
||||
|
|
@ -234,7 +234,7 @@ def test_sqliteio_write_inserts_new_pixmap_item(tmpfile, view):
|
|||
item.setRotation(33)
|
||||
item.do_flip()
|
||||
item.crop = QtCore.QRectF(5, 5, 100, 80)
|
||||
item.pixmap_to_bytes = MagicMock(return_value=b'abc')
|
||||
item.pixmap_to_bytes = MagicMock(return_value=(b'abc', 'png'))
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
|
||||
|
|
@ -259,6 +259,23 @@ def test_sqliteio_write_inserts_new_pixmap_item(tmpfile, view):
|
|||
assert result[9] == '0001-bee.png'
|
||||
|
||||
|
||||
def test_sqliteio_write_inserts_new_pixmap_item_jpg(tmpfile, view):
|
||||
item = BeePixmapItem(QtGui.QImage(), filename='bee.jpg')
|
||||
view.scene.addItem(item)
|
||||
item.pixmap_to_bytes = MagicMock(return_value=(b'abc', 'jpg'))
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
|
||||
assert item.save_id == 1
|
||||
result = io.fetchone(
|
||||
'SELECT type, sqlar.data, sqlar.name '
|
||||
'FROM items '
|
||||
'INNER JOIN sqlar on sqlar.item_id = items.id')
|
||||
assert result[0] == 'pixmap'
|
||||
assert result[1] == b'abc'
|
||||
assert result[2] == '0001-bee.jpg'
|
||||
|
||||
|
||||
def test_sqliteio_write_inserts_new_pixmap_item_without_filename(
|
||||
tmpfile, view, item):
|
||||
view.scene.addItem(item)
|
||||
|
|
@ -316,7 +333,7 @@ def test_sqliteio_write_updates_existing_pixmap_item(tmpfile, view):
|
|||
item.setRotation(33)
|
||||
item.save_id = 1
|
||||
item.crop = QtCore.QRectF(5, 5, 80, 100)
|
||||
item.pixmap_to_bytes = MagicMock(return_value=b'abc')
|
||||
item.pixmap_to_bytes = MagicMock(return_value=(b'abc', 'png'))
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.write()
|
||||
item.setScale(0.7)
|
||||
|
|
|
|||
|
|
@ -81,9 +81,83 @@ def test_get_extra_save_data(item):
|
|||
}
|
||||
|
||||
|
||||
def test_pixmap_to_bytes(qapp, imgfilename3x3):
|
||||
def test_get_imgformat_test_with_real_image(
|
||||
qapp, imgfilename3x3, item, settings):
|
||||
settings.setValue('FileIO/image_storage_format', 'best')
|
||||
img = QtGui.QImage(imgfilename3x3)
|
||||
assert item.get_imgformat(img) == 'png'
|
||||
|
||||
|
||||
def test_get_imgformat_unknown_option_defaults_to_best(
|
||||
qapp, imgfilename3x3, item, settings):
|
||||
settings.setValue('FileIO/image_storage_format', 'foo')
|
||||
img = QtGui.QImage(imgfilename3x3)
|
||||
assert item.get_imgformat(img) == 'png'
|
||||
|
||||
|
||||
def test_get_imgformat_jpg_for_large_nonalpha_image_when_setting_best(
|
||||
qapp, settings, item):
|
||||
settings.setValue('FileIO/image_storage_format', 'best')
|
||||
img = MagicMock(
|
||||
hasAlphaChannel=MagicMock(return_value=False),
|
||||
height=MagicMock(return_value=1600),
|
||||
width=MagicMock(return_value=1200))
|
||||
assert item.get_imgformat(img) == 'jpg'
|
||||
|
||||
|
||||
def test_get_imgformat_png_for_large_alpha_image_when_setting_best(
|
||||
qapp, settings, item):
|
||||
settings.setValue('FileIO/image_storage_format', 'best')
|
||||
img = MagicMock(
|
||||
hasAlphaChannel=MagicMock(return_value=True),
|
||||
height=MagicMock(return_value=1600),
|
||||
width=MagicMock(return_value=1200))
|
||||
assert item.get_imgformat(img) == 'png'
|
||||
|
||||
|
||||
def test_get_imgformat_png_for_small_nonalpha_image_when_setting_best(
|
||||
qapp, settings, item):
|
||||
settings.setValue('FileIO/image_storage_format', 'best')
|
||||
img = MagicMock(
|
||||
hasAlphaChannel=MagicMock(return_value=False),
|
||||
height=MagicMock(return_value=100),
|
||||
width=MagicMock(return_value=100))
|
||||
assert item.get_imgformat(img) == 'png'
|
||||
|
||||
|
||||
def test_get_imgformat_jpg_when_setting_jpg(
|
||||
qapp, settings, item):
|
||||
settings.setValue('FileIO/image_storage_format', 'jpg')
|
||||
img = MagicMock(
|
||||
hasAlphaChannel=MagicMock(return_value=True),
|
||||
height=MagicMock(return_value=100),
|
||||
width=MagicMock(return_value=100))
|
||||
assert item.get_imgformat(img) == 'jpg'
|
||||
|
||||
|
||||
def test_get_imgformat_png_when_setting_png(
|
||||
qapp, settings, item):
|
||||
settings.setValue('FileIO/image_storage_format', 'png')
|
||||
img = MagicMock(
|
||||
hasAlphaChannel=MagicMock(return_value=False),
|
||||
height=MagicMock(return_value=1600),
|
||||
width=MagicMock(return_value=1020))
|
||||
assert item.get_imgformat(img) == 'png'
|
||||
|
||||
|
||||
def test_pixmap_to_bytes_png(qapp, imgfilename3x3):
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
assert item.pixmap_to_bytes().startswith(b'\x89PNG')
|
||||
data, imgformat = item.pixmap_to_bytes()
|
||||
assert imgformat == 'png'
|
||||
assert data.startswith(b'\x89PNG')
|
||||
|
||||
|
||||
def test_pixmap_to_bytes_jpg(qapp, imgfilename3x3, settings):
|
||||
settings.setValue('FileIO/image_storage_format', 'jpg')
|
||||
item = BeePixmapItem(QtGui.QImage(imgfilename3x3))
|
||||
data, imgformat = item.pixmap_to_bytes()
|
||||
assert imgformat == 'jpg'
|
||||
assert data.startswith(b'\xff\xd8\xff\xe0\x00\x10JFIF')
|
||||
|
||||
|
||||
def test_pixmap_from_bytes(qapp, item, imgfilename3x3):
|
||||
|
|
|
|||
|
|
@ -35,6 +35,28 @@ def test_command_line_args_get_unknown():
|
|||
CommandlineArgs._instance = None
|
||||
|
||||
|
||||
def test_settings_value_or_default_gets_default(settings):
|
||||
assert settings.valueOrDefault('FileIO/image_storage_format') == 'best'
|
||||
|
||||
|
||||
def test_settings_value_or_default_gets_overriden_value(settings):
|
||||
settings.setValue('FileIO/image_storage_format', 'png')
|
||||
assert settings.valueOrDefault('FileIO/image_storage_format') == 'png'
|
||||
|
||||
|
||||
def test_restore_defaults_restores(settings):
|
||||
settings.setValue('FileIO/image_storage_format', 'png')
|
||||
settings.restore_defaults()
|
||||
assert settings.contains('FileIO/image_storage_format') is False
|
||||
|
||||
|
||||
def test_restore_defaults_leaves_other_settings(settings):
|
||||
settings.setValue('foo/bar', 'baz')
|
||||
settings.restore_defaults()
|
||||
assert settings.contains('foo/bar') is True
|
||||
assert settings.value('foo/bar') == 'baz'
|
||||
|
||||
|
||||
def test_settings_recent_files_get_empty(settings):
|
||||
settings.get_recent_files() == []
|
||||
|
||||
|
|
|
|||
|
|
@ -292,6 +292,12 @@ def test_on_action_save_when_no_filename(save_as_mock, view, imgfilename3x3):
|
|||
view.scene.cancel_crop_mode.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('beeref.widgets.settings.SettingsDialog.show')
|
||||
def test_on_action_settings(show_mock, view):
|
||||
view.on_action_settings()
|
||||
show_mock.assert_called_once()
|
||||
|
||||
|
||||
@patch('beeref.widgets.HelpDialog.show')
|
||||
def test_on_action_help(show_mock, view):
|
||||
view.on_action_help()
|
||||
|
|
|
|||
Loading…
Reference in a new issue