-
Notifications
You must be signed in to change notification settings - Fork 152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Rewrite gitjob.Git() as a single-job class using Qt-style signals #981
Conversation
This refers to my comment #974 (comment) Git() is now a more straightforward object responsible for synchronously or asynchronously running a single Git command. The asynchronous behaviour relies completely on Qt-style signalling, i.e. there are no callback functions but the option to connect to signals. The class forwards all the signals from QProcess.
Except concerns of lagging too much behind the schedule, I like to rewrite any part of my code if it is necessary. I'll give my feedback after reading your commit. |
I do this without further discussion but in a separate commit which makes it revertable
This has been superseded by the wrapped QProcess signals
a) Git commands don't raise GitError only because of something present in stderr b) Through checking self._stderr and _stdout for "None" it is possible to determine a job has completed. c) run_blocking returns a (stdout, stderr) tuple, the async version also populates these two properties => Of course these changes have to be reflected by the callers (= currently gitrepoo.Repo() )
I've added some more commits reshaping and cleaning I would also suggest to continue with #974 (comment) and especially #974 (comment), |
So in my opinion we can change |
Yes and no.
Not necessarily. Depending on the use case, "my" Git() can also be kept as a member variable. it is designed to perform multiple jobs consecutively, although it has a narrower focus of just one job at a time. It is provided for that for example by reinitializing the result fields before starting the process.
Yes, if the process is "owned" by some persistent object. I think I've done this by passing the owner through the constructor methods, but we have to test this once the objects are actually instantiated (all of my code here is completely untested so far. "command queue" ad Scenario 1: yes, this is true, and maybe this can be keptl I think it would then be the responsibility of the queue (and maybe we should use the existing However, I would still investigate QFileSystemWatcher to trigger the updates. It should be much easier to simply react to an external event than to repeatedly going through this loop of (expensive) Git checks. ad Scenario 2: I think I said this somewhere, but we should take the behaviour of
I think this is a sufficient approach for the Git updates too.
OK, I have no objections. |
The test code I insert into path_1 = '/home/pysrc/git/frescobaldi/frescobaldi/frescobaldi_app/vcs/d.py'
path_2 = '/home/pysrc/git/frescobaldi/frescobaldi/frescobaldi_app/vcs/gitrepoo.py'
path_3 = '/home/pysrc/git/frescobaldi/frescobaldi/.git/index/'
path_4 = '/home/'
path_5 = '/home/pysrc/git/frescobaldi/frescobaldi/'
import gitjob
class fakeowner:
def __init__(self, path):
self._root_path = path
self._relative_path = ''
owner = fakeowner(path_5)
self.git = gitjob.Git(owner)
args1 = ['status', '--porcelain', owner._relative_path]
args2 = ['status']
def printStarted():
print('Started is emitted')
def printStateChanged(state):
dict = {
0 : 'State: NotRunning',
1 : 'State: Starting',
2 : 'State: Running'
}
print(dict[state])
def printReadyReadStandardError():
print('readyReadStandardError is emitted')
def printReadyReadStandardOutput():
print('readyReadStandardOutput is emitted')
def printres(gitprocess):
print("finished is emmitted")
print(gitprocess.stdout())
self.git.started.connect(printStarted)
self.git.stateChanged.connect(printStateChanged)
self.git.readyReadStandardError.connect(printReadyReadStandardError)
self.git.readyReadStandardOutput.connect(printReadyReadStandardOutput)
self.git.finished.connect(printres)
self.git.run_blocking(args1)
self.git.run_blocking(args2)
self.git.run_blocking(args1)
self.git.run(args2) |
Use a raw list instead
The test code I insert into path_1 = '/home/pysrc/git/frescobaldi/frescobaldi/frescobaldi_app/vcs/d.py'
path_2 = '/home/pysrc/git/frescobaldi/frescobaldi/frescobaldi_app/vcs/gitrepoo.py'
path_3 = '/home/pysrc/git/frescobaldi/frescobaldi/.git/index/'
path_4 = '/home/'
path_5 = '/home/pysrc/git/frescobaldi/frescobaldi/'
import gitjob
class fakeowner:
def __init__(self, path):
self._root_path = path
self._relative_path = ''
self.owner = fakeowner(path_5)
self.jobqueue = gitjob.GitJobQueue()
args1 = ['status', '--porcelain', self.owner._relative_path]
args2 = ['status']
def printStarted():
print('Started is emitted')
def printStateChanged(state):
dict = {
0 : 'State: NotRunning',
1 : 'State: Starting',
2 : 'State: Running'
}
print(dict[state])
def printReadyReadStandardError():
print('readyReadStandardError is emitted')
def printReadyReadStandardOutput():
print('readyReadStandardOutput is emitted')
def printres(gitprocess):
print("finished is emmitted")
print(gitprocess.stdout())
# to invoke next git command in the jobqueue
gitprocess.executed.emit(0)
git1 = gitjob.Git(self.owner)
git1.preset_args = args1
git1.started.connect(printStarted)
git1.stateChanged.connect(printStateChanged)
git1.readyReadStandardError.connect(printReadyReadStandardError)
git1.readyReadStandardOutput.connect(printReadyReadStandardOutput)
git1.finished.connect(printres)
git2 = gitjob.Git(self.owner)
git2.preset_args = args2
git2.started.connect(printStarted)
git2.stateChanged.connect(printStateChanged)
git2.readyReadStandardError.connect(printReadyReadStandardError)
git2.readyReadStandardOutput.connect(printReadyReadStandardOutput)
git2.finished.connect(printres)
self.jobqueue.enqueue(git1)
self.jobqueue.enqueue(git2) |
This is indeed strange. I asked about it on stackoverflow: https://stackoverflow.com/questions/44868741/is-qqueue-missing-from-pyqt-5 |
Thank you! I find another strange issue: I can't access signal I tried to access signal http://pyqt.sourceforge.net/Docs/PyQt4/qprocess.html#error-2 I think PyQt5 is still in development. And in future they will alter the signal name from |
Signal "errorOccured" is still not an attribute of QProcess in PyQt5 (PyQt 5.5.1 specifically), while it is listed in Qt's doc. Signal "error" does the same job now. It is an attribute of PyQt4's QProcess. I assume that it will be replaced by "errorOccured" in future.
Add a function to print error code: def printErrorOccured(error):
dict = {
0 : 'Error: FailedToStart',
1 : 'Error: Crashed',
2 : 'Error: Timedout',
4 : 'Error: WriteError',
3 : 'Error: ReadError',
5 : 'Error: UnknownError'
}
print(dict[error])
# connect the signal with it
git1.errorOccurred.connect(printErrorOccured) I changed the |
"if not self.isRunning():" doesn't make sense
Invoke kill() will trigger two signal: "errorOccured" and "stateChanged" "errorOccured" is emitted at first with the error code: "Crashed" "stateChanged" then signals with the state code "NotRunning" You will not receive these two signals when the Git() object is destroyed right after Git.kill() is called.
According to this question Is deleteLater() necessary in PyQt/PySide?
Since we need to frequently create and delete |
If "args" is passed in, run() and run_blocking() will use it, otherwise run() and run_blocking() will use "self.preset_args". If both "args" and "self.preset_args" are None, we think it's the programmer's fault. We raise an exception here.
If the process crashes, a "errorOccurred" signal will be emitted and "finished" signal won't be emitted. So _finished() only need to handle the successful result.
Remove the Git() object in queue-head. If the Git() object is running, terminate it by calling kill(). Modify the related code in KillAll() and _auto_run_next()
@wenxin-bupt please review https://stackoverflow.com/questions/44868741/is-qqueue-missing-from-pyqt-5, there has been an informative answer. |
Thank you. |
Yes, but please replace the |
In above commit, should I define an enum instead of emitting an int in signal |
frescobaldi_app/vcs/gitjob.py
Outdated
Error message will output to stderr() | ||
""" | ||
self._handle_results() | ||
self.finished.emit(exitcode) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Oh.... I need to put my question here)
Should I define an enum instead of emitting an int in signal finished()?
"self" should be emitted along with the "exitcode" in signal "finished"
…Occurred" When an error happens to the Git() object in the queue, GitJobQueue() object will not work properly. So "errorOccurred" should be emitted from GitJobQueue() object to let the host object decide what to do next. E.g. kill_all(), run_next()
"stdout()" and "stderr()" now return "_stdout" and "_stderr" directly.
frescobaldi_app/vcs/gitjob.py
Outdated
""" | ||
# TODO: should we check isRunning() before or can we rely on the "is not None" check? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isRunning()
works by checking the process's state: QProcess.NotRunning
There is a time interval between changing state into QProcess.NotRunning
and emitting finished()
signal.
_stdout
and _stderr
are updated after the signal finished()
has been emitted. So there will be the case in which isRunning()
returns False
while _stdout
and _stderr
are still None
.
frescobaldi_app/vcs/gitjob.py
Outdated
""" | ||
# TODO: should we check isRunning() before or can we rely on the "is not None" check? | ||
# A simpler approach would be to simply return self._stdout and have the caller interpret | ||
# the type of result: either None (job not completed) or a (potentially empty) string list |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to return _stdout
and _stderr
directly to let the caller decide what to next.
So I pushed this commit.
Save binary results in "_handle_results()". Let user decide which kind of result they want in "stdout()" and "stderr()"
Class Git() should concentrate on the running process of a Git command. "version()" will be moved to "Repo()".
I think I met two kind of errors. The first kind is induced by intentionally changing the Another kind of errors don't emit
|
The first kind is induced by intentionally changing the
|Git().executable| from "git" to "gitt". |Git()| emits signal
|errorOccurred| with the message |QProcess.FailedToStart| and then
crashes. It won't emit signal |finished|. This kind of error is useful
when we want to check whether |Git| is available in shell.
the vcs module has a function is_available('git')
which in turn loads the vcs.gitrepo module, instantiates Repo and
invokes the class method vcs_available('git')
(see
https://github.com/wbsoft/frescobaldi/blob/rewrite-vcs/frescobaldi_app/vcs/gitrepo.py#L83).
Please copy that function to "your" Repo class and adapt to the new Git
job as you describe.
I think error handling should be placed in |Repo()|. So |Git()| just
simply emits the |errorOccurred| signal.
I think a Repo() object doesn't have to care about the presence of Git.
It should only be created in the first place if Git is available. (And
we don't have to consider the case that a user uninstalls Git during a
Frescobaldi session ...)
Another kind of errors don't emit |errorOccurred| signal. But they
pass a exitcode in |finished()| signal. This kind of error will occur
when I intentionally change the argument |args| to a wrong argument.
And then we can get the error message in |stderr()|. Same as the
solution above, I just let |Git()| emit |finished| with the exitcode
(0 for success, 1 for failure).
|Git()| also emits |errorOccurred| with |QProcess.Crashed| when
calling |kill()| on it. But this kind of "errors" are caused by us. So
it shouldn't be discussed here.
All correct
|
Could I merge this branch to Update: I've started working on |
OK. I read your previous comment but couldn't reply. This branch was only created in order not to impose this Git() class change without discussing it first. So you can merge this pull request whenever you like into |
This refers to my comment #974 (comment)
Git() is now a more straightforward object responsible for synchronously
or asynchronously running a single Git command.
The asynchronous behaviour relies completely on Qt-style signalling,
i.e. there are no callback functions but the option to connect to signals.
The class forwards all the signals from QProcess.
I have removed the queue concept (although it was well implemented). If we need it at all we should move the idea to the
Repo()
object but keep theGit()
object clean and focused.Maybe we don't need a queue at all because the same effect may be achieved using signals and slots (call Git command A and connect to a slot. In that slot call Git command B etc.)
This code will not work without correspondingly updating the owning Git repo class. But I wanted to create this in order to discuss the approach.
I'm feeling somewhat bad to suggest such far-reaching changes, but we should get to a common understanding before building too much further code upon it.
Please ask if there is anything you don't understand - or if you have any objections.