mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Move file IO to dedicated thread
This commit is contained in:
parent
a907343d04
commit
d2c26d98d6
13 changed files with 425 additions and 129 deletions
|
|
@ -18,12 +18,16 @@ from PyQt6 import QtCore, QtGui
|
|||
|
||||
class InsertItems(QtGui.QUndoCommand):
|
||||
|
||||
def __init__(self, scene, items):
|
||||
def __init__(self, scene, items, ignore_first_redo=False):
|
||||
super().__init__('Insert items')
|
||||
self.scene = scene
|
||||
self.items = items
|
||||
self.ignore_first_redo = ignore_first_redo
|
||||
|
||||
def redo(self):
|
||||
if self.ignore_first_redo:
|
||||
self.ignore_first_redo = False
|
||||
return
|
||||
self.scene.clearSelection()
|
||||
for item in self.items:
|
||||
self.scene.addItem(item)
|
||||
|
|
|
|||
|
|
@ -15,23 +15,86 @@
|
|||
|
||||
import logging
|
||||
|
||||
|
||||
from PyQt6 import QtCore, QtGui
|
||||
|
||||
from beeref import commands
|
||||
from beeref.fileio.errors import BeeFileIOError
|
||||
from beeref.fileio.sql import SQLiteIO
|
||||
from beeref.items import BeePixmapItem
|
||||
|
||||
|
||||
__all__ = ['load', 'save', 'BeeFileIOError']
|
||||
__all__ = [
|
||||
'load_bee',
|
||||
'save_bee',
|
||||
'load_images',
|
||||
'ThreadedLoader',
|
||||
'BeeFileIOError',
|
||||
]
|
||||
|
||||
logger = logging.getLogger('BeeRef')
|
||||
|
||||
|
||||
def load(filename, scene, progress=None):
|
||||
def load_bee(filename, scene, worker=None):
|
||||
"""Load BeeRef native file."""
|
||||
logger.info(f'Loading from file {filename}...')
|
||||
io = SQLiteIO(filename, scene, readonly=True, progress=progress)
|
||||
io = SQLiteIO(filename, scene, readonly=True, worker=worker)
|
||||
return io.read()
|
||||
|
||||
|
||||
def save(filename, scene, create_new=False, progress=None):
|
||||
def save_bee(filename, scene, create_new=False, worker=None):
|
||||
"""Save BeeRef native file."""
|
||||
logger.info(f'Saving to file {filename}...')
|
||||
logger.debug(f'Create new: {create_new}')
|
||||
io = SQLiteIO(filename, scene, create_new, progress=progress)
|
||||
io = SQLiteIO(filename, scene, create_new, worker=worker)
|
||||
io.write()
|
||||
logger.info('Saved!')
|
||||
|
||||
|
||||
def load_images(filenames, pos, scene, worker):
|
||||
"""Add images to existing scene."""
|
||||
|
||||
errors = []
|
||||
items = []
|
||||
worker.begin_processing.emit(len(filenames))
|
||||
for i, filename in enumerate(filenames):
|
||||
logger.info(f'Loading image from file {filename}')
|
||||
img = QtGui.QImage(filename)
|
||||
worker.progress.emit(i)
|
||||
if img.isNull():
|
||||
errors.append(filename)
|
||||
continue
|
||||
item = BeePixmapItem(img, filename)
|
||||
item.set_pos_center(pos)
|
||||
pos.setX(pos.x() + 50)
|
||||
pos.setY(pos.y() + 50)
|
||||
scene.add_item_later(item, selected=True)
|
||||
items.append(item)
|
||||
if worker.canceled:
|
||||
break
|
||||
|
||||
scene.undo_stack.push(
|
||||
commands.InsertItems(scene, items, ignore_first_redo=True))
|
||||
worker.finished.emit('', errors)
|
||||
|
||||
|
||||
class ThreadedIO(QtCore.QThread):
|
||||
"""Dedicated thread for loading and saving."""
|
||||
|
||||
progress = QtCore.pyqtSignal(int)
|
||||
finished = QtCore.pyqtSignal(str, list)
|
||||
begin_processing = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, func, *args, **kwargs):
|
||||
super().__init__()
|
||||
self.func = func
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.kwargs['worker'] = self
|
||||
self.canceled = False
|
||||
|
||||
def run(self):
|
||||
self.func(*self.args, **self.kwargs)
|
||||
|
||||
def on_canceled(self):
|
||||
self.canceled = True
|
||||
|
|
|
|||
|
|
@ -43,9 +43,10 @@ def handle_sqlite_errors(func):
|
|||
func(self, *args, **kwargs)
|
||||
except sqlite3.Error as e:
|
||||
logger.exception(f'Error while reading/writing {self.filename}')
|
||||
if self.progress:
|
||||
self.progress.setValue(self.progress.maximum())
|
||||
raise BeeFileIOError(msg=str(e), filename=self.filename) from e
|
||||
if self.worker:
|
||||
self.worker.finished.emit('', [str(e)])
|
||||
else:
|
||||
raise BeeFileIOError(msg=str(e), filename=self.filename) from e
|
||||
|
||||
return wrapper
|
||||
|
||||
|
|
@ -55,12 +56,12 @@ class SQLiteIO:
|
|||
APPLICATION_ID = 2060242126
|
||||
|
||||
def __init__(self, filename, scene, create_new=False, readonly=False,
|
||||
progress=None):
|
||||
worker=None):
|
||||
self.scene = scene
|
||||
self.create_new = create_new
|
||||
self.filename = filename
|
||||
self.readonly = readonly
|
||||
self.progress = progress
|
||||
self.worker = worker
|
||||
|
||||
def __del__(self):
|
||||
self._close_connection()
|
||||
|
|
@ -130,24 +131,27 @@ class SQLiteIO:
|
|||
'SELECT items.id, x, y, z, scale, rotation, flip, filename, '
|
||||
'sqlar.data '
|
||||
'FROM items INNER JOIN sqlar on sqlar.item_id = items.id')
|
||||
if self.progress:
|
||||
self.progress.setMaximum(len(rows))
|
||||
if self.worker:
|
||||
self.worker.begin_processing.emit(len(rows))
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
item = BeePixmapItem(QtGui.QImage(), filename=row[7])
|
||||
item.save_id = row[0]
|
||||
item.pixmap_from_bytes(row[8])
|
||||
item.setPos(row[1], row[2])
|
||||
self.scene.addItem(item)
|
||||
self.scene.add_item_later(item)
|
||||
item.setZValue(row[3])
|
||||
item.setScale(row[4])
|
||||
item.setRotation(row[5])
|
||||
if row[6] == -1:
|
||||
item.do_flip()
|
||||
if self.progress:
|
||||
self.progress.setValue(i)
|
||||
if self.progress.wasCanceled():
|
||||
break
|
||||
if self.worker:
|
||||
self.worker.progress.emit(i)
|
||||
if self.worker.canceled:
|
||||
self.worker.finished.emit('', [])
|
||||
return
|
||||
if self.worker:
|
||||
self.worker.finished.emit(self.filename, [])
|
||||
|
||||
@handle_sqlite_errors
|
||||
def write(self):
|
||||
|
|
@ -168,8 +172,8 @@ class SQLiteIO:
|
|||
def write_data(self):
|
||||
to_delete = self.fetchall('SELECT id from ITEMS')
|
||||
to_save = list(self.scene.items_for_save())
|
||||
if self.progress:
|
||||
self.progress.setMaximum(len(to_save))
|
||||
if self.worker:
|
||||
self.worker.begin_processing.emit(len(to_save))
|
||||
for i, item in enumerate(to_save):
|
||||
logger.debug(f'Saving {item} with id {item.save_id}')
|
||||
if item.save_id:
|
||||
|
|
@ -177,12 +181,14 @@ class SQLiteIO:
|
|||
to_delete.remove((item.save_id,))
|
||||
else:
|
||||
self.insert_item(item)
|
||||
if self.progress:
|
||||
self.progress.setValue(i)
|
||||
if self.progress.wasCanceled():
|
||||
if self.worker:
|
||||
self.worker.progress.emit(i)
|
||||
if self.worker.canceled:
|
||||
break
|
||||
self.delete_items(to_delete)
|
||||
self.connection.commit()
|
||||
if self.worker:
|
||||
self.worker.finished.emit(self.filename, [])
|
||||
|
||||
def delete_items(self, to_delete):
|
||||
self.exmany('DELETE FROM items WHERE id=?', to_delete)
|
||||
|
|
|
|||
|
|
@ -44,10 +44,20 @@ class WelcomeOverlay(QtWidgets.QWidget):
|
|||
|
||||
class BeeProgressDialog(QtWidgets.QProgressDialog):
|
||||
|
||||
def __init__(self, label, maximum=100, parent=None):
|
||||
def __init__(self, label, worker, maximum=100, parent=None):
|
||||
super().__init__(label, 'Cancel', 0, maximum, parent=parent)
|
||||
self.setMinimumDuration(2)
|
||||
self.setMinimumDuration(0)
|
||||
self.setWindowModality(Qt.WindowModality.WindowModal)
|
||||
worker.begin_processing.connect(self.on_begin_processing)
|
||||
worker.progress.connect(self.on_progress)
|
||||
worker.finished.connect(self.on_finished)
|
||||
self.canceled.connect(worker.on_canceled)
|
||||
|
||||
def setValue(self, value):
|
||||
super().setValue(value + 1)
|
||||
def on_progress(self, value):
|
||||
self.setValue(value + 1)
|
||||
|
||||
def on_begin_processing(self, value):
|
||||
self.setMaximum(value)
|
||||
|
||||
def on_finished(self, filename, errors):
|
||||
self.close()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from queue import Queue
|
||||
import logging
|
||||
import math
|
||||
|
||||
|
|
@ -38,6 +39,7 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
self.rubberband_item = RubberbandItem()
|
||||
self.selectionChanged.connect(self.on_selection_change)
|
||||
self.changed.connect(self.on_change)
|
||||
self.items_to_add = Queue()
|
||||
|
||||
def normalize_width_or_height(self, mode):
|
||||
"""Scale the selected images to have the same width or height, as
|
||||
|
|
@ -205,3 +207,19 @@ class BeeGraphicsScene(QtWidgets.QGraphicsScene):
|
|||
and not self.multi_select_item.rotate_active):
|
||||
self.multi_select_item.fit_selection_area(
|
||||
self.get_selection_rect())
|
||||
|
||||
def add_item_later(self, item, selected=False):
|
||||
"""Keep an item for adding later via ``add_delayed_items``"""
|
||||
|
||||
self.items_to_add.put((item, selected))
|
||||
|
||||
def add_delayed_items(self):
|
||||
"""Adds items added via ``add_items_later``"""
|
||||
|
||||
while not self.items_to_add.empty():
|
||||
item, selected = self.items_to_add.get()
|
||||
self.addItem(item)
|
||||
if selected:
|
||||
item.setSelected(True)
|
||||
item.bring_to_front()
|
||||
self.max_z = max(self.max_z, item.zValue())
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ class BaseItemMixin:
|
|||
def setZValue(self, value):
|
||||
logger.debug(f'Setting z-value for {self} to {value}')
|
||||
super().setZValue(value)
|
||||
self.scene().max_z = max(self.scene().max_z, value)
|
||||
if self.scene():
|
||||
self.scene().max_z = max(self.scene().max_z, value)
|
||||
|
||||
def bring_to_front(self):
|
||||
self.setZValue(self.scene().max_z + 0.001)
|
||||
|
|
@ -536,7 +537,7 @@ class MultiSelectItem(SelectableMixin,
|
|||
def fit_selection_area(self, rect):
|
||||
"""Updates itself to fit the given selection area."""
|
||||
|
||||
logging.debug(f'Fit selection area to {rect}')
|
||||
logger.debug(f'Fit selection area to {rect}')
|
||||
|
||||
# Only update when values have changed, otherwise we end up in an
|
||||
# infinite event loop sceneChange -> itemChange -> sceneChange ...
|
||||
|
|
|
|||
139
beeref/view.py
139
beeref/view.py
|
|
@ -78,7 +78,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
|
||||
def on_scene_changed(self, region):
|
||||
if not self.scene.items():
|
||||
logger.info('No items in scene')
|
||||
logger.debug('No items in scene')
|
||||
self.setTransform(QtGui.QTransform())
|
||||
self.welcome_overlay.show()
|
||||
else:
|
||||
|
|
@ -138,23 +138,32 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
def on_action_flip_vertically(self):
|
||||
self.scene.flip_items(vertical=True)
|
||||
|
||||
def open_from_file(self, filename):
|
||||
logger.info(f'Opening file {filename}')
|
||||
self.clear_scene()
|
||||
try:
|
||||
progress = BeeProgressDialog(
|
||||
'Loading %s' % filename,
|
||||
parent=self)
|
||||
fileio.load(filename, self.scene, progress)
|
||||
def on_items_loaded(self, value):
|
||||
self.scene.add_delayed_items()
|
||||
|
||||
def on_loading_finished(self, filename, errors):
|
||||
if filename:
|
||||
self.filename = filename
|
||||
progress.close()
|
||||
except fileio.BeeFileIOError:
|
||||
if errors:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
'Problem loading file',
|
||||
('<p>Problem loading file %s</p>'
|
||||
'<p>Not accessible or not a proper bee file</p>') % filename)
|
||||
|
||||
def open_from_file(self, filename):
|
||||
logger.info(f'Opening file {filename}')
|
||||
self.clear_scene()
|
||||
self.worker = fileio.ThreadedIO(
|
||||
fileio.load_bee, filename, self.scene)
|
||||
self.worker.progress.connect(self.on_items_loaded)
|
||||
self.worker.finished.connect(self.on_loading_finished)
|
||||
self.progress = BeeProgressDialog(
|
||||
'Loading %s' % filename,
|
||||
worker=self.worker,
|
||||
parent=self)
|
||||
self.worker.start()
|
||||
|
||||
def on_action_open(self):
|
||||
filename, f = QtWidgets.QFileDialog.getOpenFileName(
|
||||
parent=self,
|
||||
|
|
@ -164,44 +173,59 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
self.open_from_file(filename)
|
||||
self.filename = filename
|
||||
|
||||
def on_saving_finished(self, filename, errors):
|
||||
if filename:
|
||||
self.filename = filename
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
'Problem saving file',
|
||||
('<p>Problem saving file %s</p>'
|
||||
'<p>File/directory not accessible</p>') % filename)
|
||||
|
||||
def do_save(self, filename, create_new):
|
||||
if not filename.endswith('.bee'):
|
||||
filename = f'{filename}.bee'
|
||||
self.worker = fileio.ThreadedIO(
|
||||
fileio.save_bee, filename, self.scene, create_new=create_new)
|
||||
self.worker.finished.connect(self.on_saving_finished)
|
||||
self.progress = BeeProgressDialog(
|
||||
'Saving %s' % filename,
|
||||
worker=self.worker,
|
||||
parent=self)
|
||||
self.worker.start()
|
||||
|
||||
def on_action_save_as(self):
|
||||
filename, f = QtWidgets.QFileDialog.getSaveFileName(
|
||||
parent=self,
|
||||
caption='Save file',
|
||||
filter='BeeRef File (*.bee)')
|
||||
if filename:
|
||||
if not filename.endswith('.bee'):
|
||||
filename = f'{filename}.bee'
|
||||
try:
|
||||
progress = BeeProgressDialog(
|
||||
'Saving %s' % filename,
|
||||
parent=self)
|
||||
fileio.save(filename, self.scene, create_new=True,
|
||||
progress=progress)
|
||||
self.filename = filename
|
||||
progress.close()
|
||||
except fileio.BeeFileIOError:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
'Problem saving file',
|
||||
('<p>Problem saving file %s</p>'
|
||||
'<p>File/directory not accessible</p>') % filename)
|
||||
self.do_save(filename, create_new=True)
|
||||
|
||||
def on_action_save(self):
|
||||
if not self.filename:
|
||||
self.on_action_save_as()
|
||||
else:
|
||||
progress = BeeProgressDialog(
|
||||
'Saving %s' % self.filename,
|
||||
parent=self)
|
||||
fileio.save(self.filename, self.scene, create_new=False,
|
||||
progress=progress)
|
||||
progress.close()
|
||||
self.do_save(self.filename, create_new=False)
|
||||
|
||||
def on_action_quit(self):
|
||||
logger.info('User quit. Exiting...')
|
||||
self.app.quit()
|
||||
|
||||
def on_insert_images_finished(self, filename, errors):
|
||||
if errors:
|
||||
errornames = [
|
||||
f'<li>{fn}</li>' for fn in errors]
|
||||
errornames = '<ul>%s</ul>' % '\n'.join(errornames)
|
||||
msg = ('{errors} image(s) out of {total} '
|
||||
'could not be opened:'.format(
|
||||
errors=len(errors), total=len(errors)))
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
'Problem loading images',
|
||||
msg + errornames)
|
||||
|
||||
def on_action_insert_images(self):
|
||||
formats = self.get_supported_image_formats(QtGui.QImageReader)
|
||||
filenames, f = QtWidgets.QFileDialog.getOpenFileNames(
|
||||
|
|
@ -209,42 +233,19 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin):
|
|||
caption='Select one ore more images to open',
|
||||
filter=f'Images ({formats})')
|
||||
|
||||
pos = self.mapToScene(self.get_view_center())
|
||||
errors = []
|
||||
items = []
|
||||
|
||||
progress = BeeProgressDialog(
|
||||
'Loading images...', len(filenames), parent=self)
|
||||
|
||||
for i, filename in enumerate(filenames):
|
||||
logger.info(f'Loading image from file {filename}')
|
||||
img = QtGui.QImage(filename)
|
||||
progress.setValue(i)
|
||||
if progress.wasCanceled():
|
||||
break
|
||||
if img.isNull():
|
||||
errors.append(filename)
|
||||
continue
|
||||
item = BeePixmapItem(img, filename)
|
||||
item.set_pos_center(pos)
|
||||
items.append(item)
|
||||
pos.setX(pos.x() + 50)
|
||||
pos.setY(pos.y() + 50)
|
||||
|
||||
progress.close()
|
||||
self.undo_stack.push(commands.InsertItems(self.scene, items))
|
||||
|
||||
if errors:
|
||||
errornames = [
|
||||
f'<li>{fn}</li>' for fn in errors]
|
||||
errornames = '<ul>%s</ul>' % '\n'.join(errornames)
|
||||
msg = ('{errors} image(s) out of {total} '
|
||||
'could not be opened:'.format(
|
||||
errors=len(errors), total=len(filenames)))
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
'Problem loading images',
|
||||
msg + errornames)
|
||||
self.scene.clearSelection()
|
||||
self.worker = fileio.ThreadedIO(
|
||||
fileio.load_images,
|
||||
filenames,
|
||||
self.mapToScene(self.get_view_center()),
|
||||
self.scene)
|
||||
self.worker.progress.connect(self.on_items_loaded)
|
||||
self.worker.finished.connect(self.on_insert_images_finished)
|
||||
self.progress = BeeProgressDialog(
|
||||
'Loading images',
|
||||
worker=self.worker,
|
||||
parent=self)
|
||||
self.worker.start()
|
||||
|
||||
def on_action_paste(self):
|
||||
logger.info('Pasting from clipboard...')
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ class BeeTestCase(TestCase):
|
|||
inst = QtWidgets.QApplication.instance()
|
||||
cls.app = inst if inst else QtWidgets.QApplication([])
|
||||
|
||||
def queue2list(self, queue):
|
||||
qlist = []
|
||||
while not queue.empty():
|
||||
qlist.append(queue.get())
|
||||
return qlist
|
||||
|
||||
# @classmethod
|
||||
# def tearDownClass(cls):
|
||||
# cls.app.quit()
|
||||
|
|
|
|||
|
|
@ -1,21 +1,87 @@
|
|||
import os.path
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PyQt6 import QtCore
|
||||
|
||||
from beeref import fileio
|
||||
from beeref import commands
|
||||
from beeref.scene import BeeGraphicsScene
|
||||
from ..base import BeeTestCase
|
||||
|
||||
|
||||
@patch('beeref.fileio.sql.SQLiteIO.write')
|
||||
def test_save_create_new_false(write_mock):
|
||||
def test_save_bee_create_new_false(write_mock):
|
||||
with tempfile.TemporaryDirectory() as dirname:
|
||||
fname = os.path.join(dirname, 'test.bee')
|
||||
fileio.save(fname, 'myscene', create_new=False)
|
||||
fileio.save_bee(fname, 'myscene', create_new=False)
|
||||
write_mock.assert_called_once()
|
||||
|
||||
|
||||
@patch('beeref.fileio.sql.SQLiteIO.read')
|
||||
def test_write(read_mock):
|
||||
def test_read_bee(read_mock):
|
||||
with tempfile.TemporaryDirectory() as dirname:
|
||||
fname = os.path.join(dirname, 'test.bee')
|
||||
fileio.load(fname, 'myscene')
|
||||
fileio.load_bee(fname, 'myscene')
|
||||
read_mock.assert_called_once()
|
||||
|
||||
|
||||
class LoadImagesTestCase(BeeTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.scene = BeeGraphicsScene(MagicMock())
|
||||
|
||||
def test_loads(self):
|
||||
worker = MagicMock(canceled=False)
|
||||
fileio.load_images([self.imgfilename3x3],
|
||||
QtCore.QPointF(5, 6), self.scene, worker)
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with('', [])
|
||||
items = self.queue2list(self.scene.items_to_add)
|
||||
assert len(items) == 1
|
||||
item = items[0][0]
|
||||
args = self.scene.undo_stack.push.call_args_list[0][0]
|
||||
cmd = args[0]
|
||||
assert isinstance(cmd, commands.InsertItems)
|
||||
assert cmd.items == [item]
|
||||
assert cmd.scene == self.scene
|
||||
assert cmd.ignore_first_redo is True
|
||||
assert item.pos() == QtCore.QPointF(3.5, 4.5)
|
||||
|
||||
def test_canceled(self):
|
||||
worker = MagicMock(canceled=True)
|
||||
fileio.load_images([self.imgfilename3x3, self.imgfilename3x3],
|
||||
QtCore.QPointF(5, 6), self.scene, worker)
|
||||
worker.begin_processing.emit.assert_called_once_with(2)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with('', [])
|
||||
items = self.queue2list(self.scene.items_to_add)
|
||||
assert len(items) == 1
|
||||
item = items[0][0]
|
||||
args = self.scene.undo_stack.push.call_args_list[0][0]
|
||||
cmd = args[0]
|
||||
assert isinstance(cmd, commands.InsertItems)
|
||||
assert cmd.items == [item]
|
||||
assert cmd.scene == self.scene
|
||||
assert cmd.ignore_first_redo is True
|
||||
assert item.pos() == QtCore.QPointF(3.5, 4.5)
|
||||
|
||||
def test_error(self):
|
||||
worker = MagicMock(canceled=False)
|
||||
fileio.load_images(['foo.jpg', self.imgfilename3x3],
|
||||
QtCore.QPointF(5, 6), self.scene, worker)
|
||||
worker.begin_processing.emit.assert_called_once_with(2)
|
||||
worker.progress.emit.assert_any_call(0)
|
||||
worker.progress.emit.assert_any_call(1)
|
||||
worker.finished.emit.assert_called_once_with('', ['foo.jpg'])
|
||||
items = self.queue2list(self.scene.items_to_add)
|
||||
assert len(items) == 1
|
||||
item = items[0][0]
|
||||
args = self.scene.undo_stack.push.call_args_list[0][0]
|
||||
cmd = args[0]
|
||||
assert isinstance(cmd, commands.InsertItems)
|
||||
assert cmd.items == [item]
|
||||
assert cmd.scene == self.scene
|
||||
assert cmd.ignore_first_redo is True
|
||||
assert item.pos() == QtCore.QPointF(3.5, 4.5)
|
||||
|
|
|
|||
|
|
@ -189,14 +189,28 @@ class SQLiteIOWriteTestCase(BeeTestCase):
|
|||
assert result[0] == 1
|
||||
|
||||
def test_updates_progress(self):
|
||||
progress = MagicMock()
|
||||
worker = MagicMock(canceled=False)
|
||||
io = SQLiteIO(':memory:', self.scene, create_new=True,
|
||||
progress=progress)
|
||||
worker=worker)
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
self.scene.addItem(item)
|
||||
io.write()
|
||||
progress.setMaximum.assert_called_once_with(1)
|
||||
progress.setValue.assert_called_once_with(0)
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with(':memory:', [])
|
||||
|
||||
def test_canceled(self):
|
||||
worker = MagicMock(canceled=True)
|
||||
io = SQLiteIO(':memory:', self.scene, create_new=True,
|
||||
worker=worker)
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
self.scene.addItem(item)
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
self.scene.addItem(item)
|
||||
io.write()
|
||||
worker.begin_processing.emit.assert_called_once_with(2)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with(':memory:', [])
|
||||
|
||||
|
||||
class SQLiteIOReadTestCase(BeeTestCase):
|
||||
|
|
@ -220,8 +234,8 @@ class SQLiteIOReadTestCase(BeeTestCase):
|
|||
|
||||
io = SQLiteIO(fname, self.scene, readonly=True)
|
||||
io.read()
|
||||
assert len(self.scene.items()) == 1
|
||||
item = self.scene.items()[0]
|
||||
item, selected = self.scene.items_to_add.get()
|
||||
assert selected is False
|
||||
assert item.save_id == 1
|
||||
assert item.pos().x() == 22.2
|
||||
assert item.pos().y() == 33.3
|
||||
|
|
@ -232,11 +246,12 @@ class SQLiteIOReadTestCase(BeeTestCase):
|
|||
assert item.filename == 'bee.png'
|
||||
assert item.width == 3
|
||||
assert item.height == 3
|
||||
assert self.scene.items_to_add.empty() is True
|
||||
|
||||
def test_updates_progress(self):
|
||||
progress = MagicMock()
|
||||
worker = MagicMock(canceled=False)
|
||||
io = SQLiteIO(':memory:', self.scene, create_new=True,
|
||||
progress=progress)
|
||||
worker=worker)
|
||||
|
||||
io.create_schema_on_new()
|
||||
io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
|
||||
|
|
@ -245,8 +260,29 @@ class SQLiteIOReadTestCase(BeeTestCase):
|
|||
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b''))
|
||||
io.connection.commit()
|
||||
io.read()
|
||||
progress.setMaximum.assert_called_once_with(1)
|
||||
progress.setValue.assert_called_once_with(0)
|
||||
worker.begin_processing.emit.assert_called_once_with(1)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with(':memory:', [])
|
||||
|
||||
def test_canceled(self):
|
||||
worker = MagicMock(canceled=True)
|
||||
io = SQLiteIO(':memory:', self.scene, create_new=True,
|
||||
worker=worker)
|
||||
|
||||
io.create_schema_on_new()
|
||||
io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?) ',
|
||||
('pixmap', 0, 0, 0, 1, 'bee.png'))
|
||||
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b''))
|
||||
io.ex('INSERT INTO items (type, x, y, z, scale, filename) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?) ',
|
||||
('pixmap', 50, 50, 0, 1, 'bee2.png'))
|
||||
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)', (1, b''))
|
||||
io.connection.commit()
|
||||
io.read()
|
||||
worker.begin_processing.emit.assert_called_once_with(2)
|
||||
worker.progress.emit.assert_called_once_with(0)
|
||||
worker.finished.emit.assert_called_once_with('', [])
|
||||
|
||||
def test_raises_error_when_file_borked(self):
|
||||
with tempfile.TemporaryDirectory() as dirname:
|
||||
|
|
@ -259,6 +295,20 @@ class SQLiteIOReadTestCase(BeeTestCase):
|
|||
io.read()
|
||||
assert exinfo.value.filename == fname
|
||||
|
||||
def test_emits_error_message_when_file_borked(self):
|
||||
with tempfile.TemporaryDirectory() as dirname:
|
||||
fname = os.path.join(dirname, 'test.bee')
|
||||
with open(fname, 'w') as f:
|
||||
f.write('foobar')
|
||||
|
||||
worker = MagicMock()
|
||||
io = SQLiteIO(fname, self.scene, readonly=True, worker=worker)
|
||||
io.read()
|
||||
worker.finished.emit.assert_called_once()
|
||||
args = worker.finished.emit.call_args_list[0][0]
|
||||
assert args[0] == ''
|
||||
assert len(args[1]) == 1
|
||||
|
||||
def test_reads_raises_error_when_file_empty(self):
|
||||
with tempfile.TemporaryDirectory() as dirname:
|
||||
fname = os.path.join(dirname, 'test.bee')
|
||||
|
|
|
|||
|
|
@ -30,6 +30,26 @@ class InsertItemsTestCase(BeeTestCase):
|
|||
assert list(scene.items_for_save()) == [item1]
|
||||
assert item1.isSelected() is False
|
||||
|
||||
@patch('beeref.scene.BeeGraphicsScene.views')
|
||||
def test_ignore_first_redo(self, views_mock):
|
||||
scene = BeeGraphicsScene(None)
|
||||
view = MagicMock(get_scale=MagicMock(return_value=1))
|
||||
views_mock.return_value = [view]
|
||||
scene.update_selection = MagicMock()
|
||||
scene.max_z = 5
|
||||
item1 = BeePixmapItem(QtGui.QImage())
|
||||
scene.addItem(item1)
|
||||
item2 = BeePixmapItem(QtGui.QImage())
|
||||
command = commands.InsertItems(scene, [item2], ignore_first_redo=True)
|
||||
command.redo()
|
||||
assert list(scene.items_for_save()) == [item1]
|
||||
assert item1.isSelected() is False
|
||||
command.redo()
|
||||
assert list(scene.items_for_save()) == [item1, item2]
|
||||
assert item1.isSelected() is False
|
||||
assert item2.isSelected() is True
|
||||
item2.zValue() > 5
|
||||
|
||||
|
||||
class DeleteItemsTestCase(BeeTestCase):
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class BeeGraphicsSceneTestCase(BeeTestCase):
|
|||
self.scene.flip_items(vertical=True)
|
||||
args = self.scene.undo_stack.push.call_args_list[0][0]
|
||||
cmd = args[0]
|
||||
isinstance(cmd, commands.FlipItems)
|
||||
assert isinstance(cmd, commands.FlipItems)
|
||||
assert cmd.items == [item]
|
||||
assert cmd.anchor == QtCore.QPointF(60, 50)
|
||||
assert cmd.vertical is True
|
||||
|
|
@ -272,7 +272,7 @@ class BeeGraphicsSceneTestCase(BeeTestCase):
|
|||
self.scene.undo_stack.push.assert_called_once()
|
||||
args = self.scene.undo_stack.push.call_args_list[0][0]
|
||||
cmd = args[0]
|
||||
isinstance(cmd, commands.MoveItemsBy)
|
||||
assert isinstance(cmd, commands.MoveItemsBy)
|
||||
assert cmd.items == [item]
|
||||
assert cmd.ignore_first_redo is True
|
||||
assert cmd.delta.x() == 10
|
||||
|
|
@ -473,3 +473,26 @@ class BeeGraphicsSceneTestCase(BeeTestCase):
|
|||
self.scene.multi_select_item.rotate_active = True
|
||||
self.scene.on_change(None)
|
||||
self.scene.multi_select_item.fit_selection_area.assert_not_called()
|
||||
|
||||
def test_add_delayed_items_unselected(self):
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
item.setZValue(0.33)
|
||||
self.scene.add_item_later(item, selected=False)
|
||||
self.scene.add_delayed_items()
|
||||
assert self.scene.items() == [item]
|
||||
assert item.isSelected() is False
|
||||
assert self.scene.max_z == 0.33
|
||||
|
||||
def test_add_delayed_items_selected(self):
|
||||
self.scene.max_z = 0.6
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
item.setZValue(0.33)
|
||||
self.scene.add_item_later(item, selected=True)
|
||||
self.scene.add_delayed_items()
|
||||
assert self.scene.items() == [item]
|
||||
assert item.isSelected() is True
|
||||
assert item.zValue() > 0.6
|
||||
|
||||
def test_add_delayed_items_when_no_items(self):
|
||||
self.scene.add_delayed_items()
|
||||
assert self.scene.items() == []
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import os.path
|
|||
import tempfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from pytest import mark
|
||||
|
||||
from PyQt6 import QtGui, QtWidgets
|
||||
|
||||
from beeref.items import BeePixmapItem
|
||||
|
|
@ -87,29 +89,44 @@ class BeeGraphicsViewTestCase(BeeTestCase):
|
|||
root = os.path.dirname(__file__)
|
||||
filename = os.path.join(root, 'assets', 'test1item.bee')
|
||||
self.view.open_from_file(filename)
|
||||
assert len(self.view.scene.items()) == 1
|
||||
self.view.worker.wait()
|
||||
items = self.queue2list(self.view.scene.items_to_add)
|
||||
assert len(items) == 1
|
||||
item, selected = items[0]
|
||||
assert items[0][0].pixmap()
|
||||
assert items[0][1] is False
|
||||
clear_mock.assert_called_once_with()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.warning')
|
||||
def test_open_from_file_when_error(self, warn_mock):
|
||||
# FIXME: #1
|
||||
# Can't check signal handling currently
|
||||
self.view.open_from_file('uieauiae')
|
||||
self.view.worker.wait()
|
||||
assert self.view.scene.items_to_add.empty() is True
|
||||
assert len(self.view.scene.items()) == 0
|
||||
warn_mock.assert_called_once()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileName')
|
||||
def test_on_action_open(self, dialog_mock):
|
||||
# FIXME: #1
|
||||
# Can't check signal handling currently
|
||||
root = os.path.dirname(__file__)
|
||||
dialog_mock.return_value = (
|
||||
os.path.join(root, 'assets', 'test1item.bee'),
|
||||
None)
|
||||
self.view.on_action_open()
|
||||
assert len(self.view.scene.items()) == 1
|
||||
self.view.worker.wait()
|
||||
items = self.queue2list(self.view.scene.items_to_add)
|
||||
assert len(items) == 1
|
||||
assert items[0][0].pixmap()
|
||||
assert items[0][1] is False
|
||||
|
||||
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileName')
|
||||
def test_on_action_open_when_no_filename(self, dialog_mock):
|
||||
@patch('beeref.view.BeeGraphicsView.on_action_open')
|
||||
def test_on_action_open_when_no_filename(self, dialog_mock, open_mock):
|
||||
dialog_mock.return_value = (None, None)
|
||||
self.view.on_action_open()
|
||||
assert len(self.view.scene.items()) == 0
|
||||
open_mock.assert_not_called()
|
||||
|
||||
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
|
||||
def test_on_action_save_as(self, dialog_mock):
|
||||
|
|
@ -120,10 +137,11 @@ class BeeGraphicsViewTestCase(BeeTestCase):
|
|||
assert os.path.exists(filename) is False
|
||||
dialog_mock.return_value = (filename, None)
|
||||
self.view.on_action_save_as()
|
||||
self.view.worker.wait()
|
||||
assert os.path.exists(filename) is True
|
||||
|
||||
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
|
||||
@patch('beeref.fileio.save')
|
||||
@patch('beeref.view.BeeGraphicsView.do_save')
|
||||
def test_on_action_save_as_when_no_filename(self, save_mock, dialog_mock):
|
||||
item = BeePixmapItem(QtGui.QImage(self.imgfilename3x3))
|
||||
self.view.scene.addItem(item)
|
||||
|
|
@ -140,8 +158,10 @@ class BeeGraphicsViewTestCase(BeeTestCase):
|
|||
assert os.path.exists(filename) is False
|
||||
dialog_mock.return_value = (filename, None)
|
||||
self.view.on_action_save_as()
|
||||
self.view.worker.wait()
|
||||
assert os.path.exists(f'{filename}.bee') is True
|
||||
|
||||
@mark.skip('needs pytest-qt')
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.warning')
|
||||
@patch('PyQt6.QtWidgets.QFileDialog.getSaveFileName')
|
||||
@patch('beeref.fileio.save')
|
||||
|
|
@ -165,6 +185,7 @@ class BeeGraphicsViewTestCase(BeeTestCase):
|
|||
self.view.filename = os.path.join(tmpdir, 'test.bee')
|
||||
assert os.path.exists(self.view.filename) is False
|
||||
self.view.on_action_save()
|
||||
self.view.worker.wait()
|
||||
assert os.path.exists(self.view.filename) is True
|
||||
|
||||
@patch('beeref.view.BeeGraphicsView.on_action_save_as')
|
||||
|
|
@ -178,23 +199,30 @@ class BeeGraphicsViewTestCase(BeeTestCase):
|
|||
@patch('beeref.scene.BeeGraphicsScene.clearSelection')
|
||||
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileNames')
|
||||
def test_on_action_insert_images(self, dialog_mock, clear_mock):
|
||||
# FIXME: #1
|
||||
# Can't check signal handling currently
|
||||
dialog_mock.return_value = ([self.imgfilename3x3], None)
|
||||
self.view.on_action_insert_images()
|
||||
assert len(self.view.scene.items()) == 1
|
||||
assert self.view.scene.items()[0].isSelected() is True
|
||||
self.view.worker.wait()
|
||||
items = self.queue2list(self.view.scene.items_to_add)
|
||||
assert len(items) == 1
|
||||
assert items[0][0].pixmap()
|
||||
assert items[0][1] is True
|
||||
clear_mock.assert_called_once_with()
|
||||
|
||||
@patch('beeref.scene.BeeGraphicsScene.clearSelection')
|
||||
@patch('PyQt6.QtWidgets.QMessageBox.warning')
|
||||
@patch('PyQt6.QtWidgets.QFileDialog.getOpenFileNames')
|
||||
def test_on_action_insert_images_when_error(
|
||||
self, dialog_mock, warn_mock, clear_mock):
|
||||
def test_on_action_insert_images_when_error(self, dialog_mock, clear_mock):
|
||||
# FIXME: #1
|
||||
# Can't check signal handling currently
|
||||
dialog_mock.return_value = (
|
||||
[self.imgfilename3x3, 'iaeiae', 'trntrn'], None)
|
||||
self.view.on_action_insert_images()
|
||||
assert len(self.view.scene.items()) == 1
|
||||
assert self.view.scene.items()[0].isSelected() is True
|
||||
warn_mock.assert_called_once()
|
||||
self.view.worker.wait()
|
||||
items = self.queue2list(self.view.scene.items_to_add)
|
||||
assert len(items) == 1
|
||||
assert items[0][0].pixmap()
|
||||
assert items[0][1] is True
|
||||
clear_mock.assert_called_once_with()
|
||||
|
||||
@patch('beeref.scene.BeeGraphicsScene.clearSelection')
|
||||
|
|
|
|||
Loading…
Reference in a new issue