mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-22 06:37:17 +00:00
Integrated the jobprogress library into hscommon
I have a fix to make in it and it's really silly to pretend that this lib is of any use to anybody outside HS apps. Bringing it back here will make things more simple.
This commit is contained in:
0
hscommon/__init__.py
Executable file → Normal file
0
hscommon/__init__.py
Executable file → Normal file
@@ -1,61 +1,58 @@
|
||||
# Created On: 2013/07/01
|
||||
# Copyright 2014 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from jobprogress.performer import ThreadedJobPerformer
|
||||
|
||||
from ..jobprogress.performer import ThreadedJobPerformer
|
||||
from .base import GUIObject
|
||||
from .text_field import TextField
|
||||
|
||||
class ProgressWindowView:
|
||||
"""Expected interface for :class:`ProgressWindow`'s view.
|
||||
|
||||
|
||||
*Not actually used in the code. For documentation purposes only.*
|
||||
|
||||
|
||||
Our view, some kind window with a progress bar, two labels and a cancel button, is expected
|
||||
to properly respond to its callbacks.
|
||||
|
||||
|
||||
It's also expected to call :meth:`ProgressWindow.cancel` when the cancel button is clicked.
|
||||
"""
|
||||
def show(self):
|
||||
"""Show the dialog.
|
||||
"""
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Close the dialog.
|
||||
"""
|
||||
|
||||
|
||||
def set_progress(self, progress):
|
||||
"""Set the progress of the progress bar to ``progress``.
|
||||
|
||||
|
||||
Not all jobs are equally responsive on their job progress report and it is recommended that
|
||||
you put your progressbar in "indeterminate" mode as long as you haven't received the first
|
||||
``set_progress()`` call to avoid letting the user think that the app is frozen.
|
||||
|
||||
|
||||
:param int progress: a value between ``0`` and ``100``.
|
||||
"""
|
||||
|
||||
class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||
"""Cross-toolkit GUI-enabled progress window.
|
||||
|
||||
This class allows you to run a long running, `job enabled`_ function in a separate thread and
|
||||
|
||||
This class allows you to run a long running, job enabled function in a separate thread and
|
||||
allow the user to follow its progress with a progress dialog.
|
||||
|
||||
|
||||
To use it, you start your long-running job with :meth:`run` and then have your UI layer
|
||||
regularly call :meth:`pulse` to refresh the job status in the UI. It is advised that you call
|
||||
:meth:`pulse` in the main thread because GUI toolkit usually only support calling UI-related
|
||||
functions from the main thread.
|
||||
|
||||
We subclass :class:`.GUIObject` and ``ThreadedJobPerformer`` (from the ``jobprogress`` library).
|
||||
|
||||
We subclass :class:`.GUIObject` and :class:`ThreadedJobPerformer`.
|
||||
Expected view: :class:`ProgressWindowView`.
|
||||
|
||||
|
||||
:param finishfunc: A function ``f(jobid)`` that is called when a job is completed. ``jobid`` is
|
||||
an arbitrary id passed to :meth:`run`.
|
||||
|
||||
.. _job enabled: https://pypi.python.org/pypi/jobprogress
|
||||
"""
|
||||
def __init__(self, finish_func):
|
||||
# finish_func(jobid) is the function that is called when a job is completed.
|
||||
@@ -68,7 +65,7 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||
#: during its course.
|
||||
self.progressdesc_textfield = TextField()
|
||||
self.jobid = None
|
||||
|
||||
|
||||
def cancel(self):
|
||||
"""Call for a user-initiated job cancellation.
|
||||
"""
|
||||
@@ -77,13 +74,13 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||
# we verify that the job is still running.
|
||||
if self._job_running:
|
||||
self.job_cancelled = True
|
||||
|
||||
|
||||
def pulse(self):
|
||||
"""Update progress reports in the GUI.
|
||||
|
||||
|
||||
Call this regularly from the GUI main run loop. The values might change before
|
||||
:meth:`ProgressWindowView.set_progress` happens.
|
||||
|
||||
|
||||
If the job is finished, ``pulse()`` will take care of closing the window and re-raising any
|
||||
exception that might have been raised during the job (in the main thread this time). If
|
||||
there was no exception, ``finish_func(jobid)`` is called to let you take appropriate action.
|
||||
@@ -101,13 +98,13 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||
if last_desc:
|
||||
self.progressdesc_textfield.text = last_desc
|
||||
self.view.set_progress(last_progress)
|
||||
|
||||
|
||||
def run(self, jobid, title, target, args=()):
|
||||
"""Starts a threaded job.
|
||||
|
||||
The ``target`` function will be sent, as its first argument, a ``Job`` instance (from the
|
||||
``jobprogress`` library) which it can use to report on its progress.
|
||||
|
||||
|
||||
The ``target`` function will be sent, as its first argument, a :class:`Job` instance which
|
||||
it can use to report on its progress.
|
||||
|
||||
:param jobid: Arbitrary identifier which will be passed to ``finish_func()`` at the end.
|
||||
:param title: A title for the task you're starting.
|
||||
:param target: The function that does your famous long running job.
|
||||
@@ -122,4 +119,4 @@ class ProgressWindow(GUIObject, ThreadedJobPerformer):
|
||||
self.run_threaded(target, args)
|
||||
self.jobdesc_textfield.text = title
|
||||
self.view.show()
|
||||
|
||||
|
||||
|
||||
0
hscommon/jobprogress/__init__.py
Normal file
0
hscommon/jobprogress/__init__.py
Normal file
160
hscommon/jobprogress/job.py
Normal file
160
hscommon/jobprogress/job.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2004/12/20
|
||||
# Copyright 2011 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
class JobCancelled(Exception):
|
||||
"The user has cancelled the job"
|
||||
|
||||
class JobInProgressError(Exception):
|
||||
"A job is already being performed, you can't perform more than one at the same time."
|
||||
|
||||
class JobCountError(Exception):
|
||||
"The number of jobs started have exceeded the number of jobs allowed"
|
||||
|
||||
class Job:
|
||||
"""Manages a job's progression and return it's progression through a callback.
|
||||
|
||||
Note that this class is not foolproof. For example, you could call
|
||||
start_subjob, and then call add_progress from the parent job, and nothing
|
||||
would stop you from doing it. However, it would mess your progression
|
||||
because it is the sub job that is supposed to drive the progression.
|
||||
Another example would be to start a subjob, then start another, and call
|
||||
add_progress from the old subjob. Once again, it would mess your progression.
|
||||
There are no stops because it would remove the lightweight aspect of the
|
||||
class (A Job would need to have a Parent instead of just a callback,
|
||||
and the parent could be None. A lot of checks for nothing.).
|
||||
Another one is that nothing stops you from calling add_progress right after
|
||||
SkipJob.
|
||||
"""
|
||||
#---Magic functions
|
||||
def __init__(self, job_proportions, callback):
|
||||
"""Initialize the Job with 'jobcount' jobs. Start every job with
|
||||
start_job(). Every time the job progress is updated, 'callback' is called
|
||||
'callback' takes a 'progress' int param, and a optional 'desc'
|
||||
parameter. Callback must return false if the job must be cancelled.
|
||||
"""
|
||||
if not hasattr(callback, '__call__'):
|
||||
raise TypeError("'callback' MUST be set when creating a Job")
|
||||
if isinstance(job_proportions, int):
|
||||
job_proportions = [1] * job_proportions
|
||||
self._job_proportions = list(job_proportions)
|
||||
self._jobcount = sum(job_proportions)
|
||||
self._callback = callback
|
||||
self._current_job = 0
|
||||
self._passed_jobs = 0
|
||||
self._progress = 0
|
||||
self._currmax = 1
|
||||
|
||||
#---Private
|
||||
def _subjob_callback(self, progress, desc=''):
|
||||
"""This is the callback passed to children jobs.
|
||||
"""
|
||||
self.set_progress(progress, desc)
|
||||
return True #if JobCancelled has to be raised, it will be at the highest level
|
||||
|
||||
def _do_update(self, desc):
|
||||
"""Calls the callback function with a % progress as a parameter.
|
||||
|
||||
The parameter is a int in the 0-100 range.
|
||||
"""
|
||||
if self._current_job:
|
||||
passed_progress = self._passed_jobs * self._currmax
|
||||
current_progress = self._current_job * self._progress
|
||||
total_progress = self._jobcount * self._currmax
|
||||
progress = ((passed_progress + current_progress) * 100) // total_progress
|
||||
else:
|
||||
progress = -1 # indeterminate
|
||||
# It's possible that callback doesn't support a desc arg
|
||||
result = self._callback(progress, desc) if desc else self._callback(progress)
|
||||
if not result:
|
||||
raise JobCancelled()
|
||||
|
||||
#---Public
|
||||
def add_progress(self, progress=1, desc=''):
|
||||
self.set_progress(self._progress + progress, desc)
|
||||
|
||||
def check_if_cancelled(self):
|
||||
self._do_update('')
|
||||
|
||||
def iter_with_progress(self, sequence, desc_format=None, every=1):
|
||||
''' Iterate through sequence while automatically adding progress.
|
||||
'''
|
||||
desc = ''
|
||||
if desc_format:
|
||||
desc = desc_format % (0, len(sequence))
|
||||
self.start_job(len(sequence), desc)
|
||||
for i, element in enumerate(sequence, start=1):
|
||||
yield element
|
||||
if i % every == 0:
|
||||
if desc_format:
|
||||
desc = desc_format % (i, len(sequence))
|
||||
self.add_progress(progress=every, desc=desc)
|
||||
if desc_format:
|
||||
desc = desc_format % (len(sequence), len(sequence))
|
||||
self.set_progress(100, desc)
|
||||
|
||||
def start_job(self, max_progress=100, desc=''):
|
||||
"""Begin work on the next job. You must not call start_job more than
|
||||
'jobcount' (in __init__) times.
|
||||
'max' is the job units you are to perform.
|
||||
'desc' is the description of the job.
|
||||
"""
|
||||
self._passed_jobs += self._current_job
|
||||
try:
|
||||
self._current_job = self._job_proportions.pop(0)
|
||||
except IndexError:
|
||||
raise JobCountError()
|
||||
self._progress = 0
|
||||
self._currmax = max(1, max_progress)
|
||||
self._do_update(desc)
|
||||
|
||||
def start_subjob(self, job_proportions, desc=''):
|
||||
"""Starts a sub job. Use this when you want to split a job into
|
||||
multiple smaller jobs. Pretty handy when starting a process where you
|
||||
know how many subjobs you will have, but don't know the work unit count
|
||||
for every of them.
|
||||
returns the Job object
|
||||
"""
|
||||
self.start_job(100, desc)
|
||||
return Job(job_proportions, self._subjob_callback)
|
||||
|
||||
def set_progress(self, progress, desc=''):
|
||||
"""Sets the progress of the current job to 'progress', and call the
|
||||
callback
|
||||
"""
|
||||
self._progress = progress
|
||||
if self._progress > self._currmax:
|
||||
self._progress = self._currmax
|
||||
if self._progress < 0:
|
||||
self._progress = 0
|
||||
self._do_update(desc)
|
||||
|
||||
|
||||
class NullJob:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def add_progress(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def check_if_cancelled(self):
|
||||
pass
|
||||
|
||||
def iter_with_progress(self, sequence, *args, **kwargs):
|
||||
return iter(sequence)
|
||||
|
||||
def start_job(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def start_subjob(self, *args, **kwargs):
|
||||
return NullJob()
|
||||
|
||||
def set_progress(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
nulljob = NullJob()
|
||||
72
hscommon/jobprogress/performer.py
Normal file
72
hscommon/jobprogress/performer.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2010-11-19
|
||||
# Copyright 2011 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from threading import Thread
|
||||
import sys
|
||||
|
||||
from .job import Job, JobInProgressError, JobCancelled
|
||||
|
||||
class ThreadedJobPerformer:
|
||||
"""Run threaded jobs and track progress.
|
||||
|
||||
To run a threaded job, first create a job with _create_job(), then call _run_threaded(), with
|
||||
your work function as a parameter.
|
||||
|
||||
Example:
|
||||
|
||||
j = self._create_job()
|
||||
self._run_threaded(self.some_work_func, (arg1, arg2, j))
|
||||
"""
|
||||
_job_running = False
|
||||
last_error = None
|
||||
|
||||
#--- Protected
|
||||
def create_job(self):
|
||||
if self._job_running:
|
||||
raise JobInProgressError()
|
||||
self.last_progress = -1
|
||||
self.last_desc = ''
|
||||
self.job_cancelled = False
|
||||
return Job(1, self._update_progress)
|
||||
|
||||
def _async_run(self, *args):
|
||||
target = args[0]
|
||||
args = tuple(args[1:])
|
||||
self._job_running = True
|
||||
self.last_error = None
|
||||
try:
|
||||
target(*args)
|
||||
except JobCancelled:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.last_error = e
|
||||
self.last_traceback = sys.exc_info()[2]
|
||||
finally:
|
||||
self._job_running = False
|
||||
self.last_progress = None
|
||||
|
||||
def reraise_if_error(self):
|
||||
"""Reraises the error that happened in the thread if any.
|
||||
|
||||
Call this after the caller of run_threaded detected that self._job_running returned to False
|
||||
"""
|
||||
if self.last_error is not None:
|
||||
raise self.last_error.with_traceback(self.last_traceback)
|
||||
|
||||
def _update_progress(self, newprogress, newdesc=''):
|
||||
self.last_progress = newprogress
|
||||
if newdesc:
|
||||
self.last_desc = newdesc
|
||||
return not self.job_cancelled
|
||||
|
||||
def run_threaded(self, target, args=()):
|
||||
if self._job_running:
|
||||
raise JobInProgressError()
|
||||
args = (target, ) + args
|
||||
Thread(target=self._async_run, args=args).start()
|
||||
|
||||
52
hscommon/jobprogress/qt.py
Normal file
52
hscommon/jobprogress/qt.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-09-14
|
||||
# Copyright 2011 Hardcoded Software (http://www.hardcoded.net)
|
||||
#
|
||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
||||
# which should be included with this package. The terms are also available at
|
||||
# http://www.hardcoded.net/licenses/bsd_license
|
||||
|
||||
from PyQt4.QtCore import pyqtSignal, Qt, QTimer
|
||||
from PyQt4.QtGui import QProgressDialog
|
||||
|
||||
from . import job, performer
|
||||
|
||||
class Progress(QProgressDialog, performer.ThreadedJobPerformer):
|
||||
finished = pyqtSignal(['QString'])
|
||||
|
||||
def __init__(self, parent):
|
||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
||||
QProgressDialog.__init__(self, '', "Cancel", 0, 100, parent, flags)
|
||||
self.setModal(True)
|
||||
self.setAutoReset(False)
|
||||
self.setAutoClose(False)
|
||||
self._timer = QTimer()
|
||||
self._jobid = ''
|
||||
self._timer.timeout.connect(self.updateProgress)
|
||||
|
||||
def updateProgress(self):
|
||||
# the values might change before setValue happens
|
||||
last_progress = self.last_progress
|
||||
last_desc = self.last_desc
|
||||
if not self._job_running or last_progress is None:
|
||||
self._timer.stop()
|
||||
self.close()
|
||||
if not self.job_cancelled:
|
||||
self.finished.emit(self._jobid)
|
||||
return
|
||||
if self.wasCanceled():
|
||||
self.job_cancelled = True
|
||||
return
|
||||
if last_desc:
|
||||
self.setLabelText(last_desc)
|
||||
self.setValue(last_progress)
|
||||
|
||||
def run(self, jobid, title, target, args=()):
|
||||
self._jobid = jobid
|
||||
self.reset()
|
||||
self.setLabelText('')
|
||||
self.run_threaded(target, args)
|
||||
self.setWindowTitle(title)
|
||||
self.show()
|
||||
self._timer.start(500)
|
||||
|
||||
0
hscommon/path.py
Executable file → Normal file
0
hscommon/path.py
Executable file → Normal file
Reference in New Issue
Block a user