Diagnostics work again... somewhat.

There appear to be timing issues for the diag requests. Somehow, we're sending
out-of-date diagnostics and then not updating the UI when things change.

That needs to be fixed.
This commit is contained in:
Strahinja Val Markovic 2013-10-03 10:14:31 -07:00
parent e44fd87582
commit b9bb788a2a
13 changed files with 298 additions and 83 deletions

View File

@ -260,7 +260,7 @@ function! s:OnCursorHold()
call s:SetUpCompleteopt() call s:SetUpCompleteopt()
" Order is important here; we need to extract any done diagnostics before " Order is important here; we need to extract any done diagnostics before
" reparsing the file again " reparsing the file again
" call s:UpdateDiagnosticNotifications() call s:UpdateDiagnosticNotifications()
call s:OnFileReadyToParse() call s:OnFileReadyToParse()
endfunction endfunction
@ -272,6 +272,7 @@ function! s:OnFileReadyToParse()
let buffer_changed = b:changedtick != b:ycm_changedtick.file_ready_to_parse let buffer_changed = b:changedtick != b:ycm_changedtick.file_ready_to_parse
if buffer_changed if buffer_changed
py ycm_state.RequestDiagnosticsForCurrentFile()
py ycm_state.OnFileReadyToParse() py ycm_state.OnFileReadyToParse()
endif endif
let b:ycm_changedtick.file_ready_to_parse = b:changedtick let b:ycm_changedtick.file_ready_to_parse = b:changedtick
@ -327,7 +328,7 @@ function! s:OnCursorMovedNormalMode()
return return
endif endif
" call s:UpdateDiagnosticNotifications() call s:UpdateDiagnosticNotifications()
call s:OnFileReadyToParse() call s:OnFileReadyToParse()
endfunction endfunction
@ -338,7 +339,7 @@ function! s:OnInsertLeave()
endif endif
let s:omnifunc_mode = 0 let s:omnifunc_mode = 0
" call s:UpdateDiagnosticNotifications() call s:UpdateDiagnosticNotifications()
call s:OnFileReadyToParse() call s:OnFileReadyToParse()
py ycm_state.OnInsertLeave() py ycm_state.OnInsertLeave()
if g:ycm_autoclose_preview_window_after_completion || if g:ycm_autoclose_preview_window_after_completion ||
@ -408,10 +409,16 @@ endfunction
function! s:UpdateDiagnosticNotifications() function! s:UpdateDiagnosticNotifications()
if get( g:, 'loaded_syntastic_plugin', 0 ) && let should_display_diagnostics =
\ pyeval( 'ycm_state.NativeFiletypeCompletionUsable()' ) && \ get( g:, 'loaded_syntastic_plugin', 0 ) &&
\ pyeval( 'ycm_state.DiagnosticsForCurrentFileReady()' ) && \ g:ycm_register_as_syntastic_checker &&
\ g:ycm_register_as_syntastic_checker \ pyeval( 'ycm_state.NativeFiletypeCompletionUsable()' )
if !should_display_diagnostics
return
endif
if pyeval( 'ycm_state.DiagnosticsForCurrentFileReady()' )
SyntasticCheck SyntasticCheck
endif endif
endfunction endfunction
@ -566,9 +573,7 @@ command! YcmShowDetailedDiagnostic call s:ShowDetailedDiagnostic()
" required (currently that's on buffer save) OR when the SyntasticCheck command " required (currently that's on buffer save) OR when the SyntasticCheck command
" is invoked " is invoked
function! youcompleteme#CurrentFileDiagnostics() function! youcompleteme#CurrentFileDiagnostics()
" TODO: Make this work again. return pyeval( 'ycm_state.GetDiagnosticsFromStoredRequest()' )
" return pyeval( 'ycm_state.GetDiagnosticsForCurrentFile()' )
return []
endfunction endfunction

View File

@ -92,25 +92,11 @@ void TranslationUnit::Destroy() {
std::vector< Diagnostic > TranslationUnit::LatestDiagnostics() { std::vector< Diagnostic > TranslationUnit::LatestDiagnostics() {
std::vector< Diagnostic > diagnostics;
if ( !clang_translation_unit_ ) if ( !clang_translation_unit_ )
return diagnostics; return std::vector< Diagnostic >();
unique_lock< mutex > lock( diagnostics_mutex_ ); unique_lock< mutex > lock( diagnostics_mutex_ );
return latest_diagnostics_;
// We don't need the latest diags after we return them once so we swap the
// internal data with a new, empty diag vector. This vector is then returned
// and on C++11 compilers a move ctor is invoked, thus no copy is created.
// Theoretically, just returning the value of a
// [boost::|std::]move(latest_diagnostics_) call _should_ leave the
// latest_diagnostics_ vector in an emtpy, valid state but I'm not going to
// rely on that. I just had to look this up in the standard to be sure, and
// future readers of this code (myself included) should not be forced to do
// that to understand what the hell is going on.
std::swap( latest_diagnostics_, diagnostics );
return diagnostics;
} }

View File

@ -30,7 +30,7 @@ class CommandRequest( BaseRequest ):
self._completer_target = ( completer_target if completer_target self._completer_target = ( completer_target if completer_target
else 'filetype_default' ) else 'filetype_default' )
self._is_goto_command = ( self._is_goto_command = (
True if arguments and arguments[ 0 ].startswith( 'GoTo' ) else False ) arguments and arguments[ 0 ].startswith( 'GoTo' ) )
self._response = None self._response = None

View File

@ -0,0 +1,75 @@
#!/usr/bin/env python
#
# Copyright (C) 2013 Strahinja Val Markovic <val@markovic.io>
#
# This file is part of YouCompleteMe.
#
# YouCompleteMe 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.
#
# YouCompleteMe 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 YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
import traceback
from ycm import vimsupport
from ycm.client.base_request import ( BaseRequest, BuildRequestData,
JsonFromFuture )
class DiagnosticsRequest( BaseRequest ):
def __init__( self ):
super( DiagnosticsRequest, self ).__init__()
self._cached_response = None
def Start( self ):
request_data = BuildRequestData()
try:
self._response_future = self.PostDataToHandlerAsync( request_data,
'diagnostics' )
except:
vimsupport.EchoText( traceback.format_exc() )
def Done( self ):
return self._response_future.done()
def Response( self ):
if self._cached_response:
return self._cached_response
if not self._response_future:
return []
try:
self._cached_response = [ _ConvertDiagnosticDataToVimData( x )
for x in JsonFromFuture(
self._response_future ) ]
return self._cached_response
except Exception as e:
vimsupport.PostVimMessage( str( e ) )
return []
def _ConvertDiagnosticDataToVimData( diagnostic ):
# see :h getqflist for a description of the dictionary fields
# Note that, as usual, Vim is completely inconsistent about whether
# line/column numbers are 1 or 0 based in its various APIs. Here, it wants
# them to be 1-based.
return {
'bufnr' : vimsupport.GetBufferNumberForFilename( diagnostic[ 'filepath' ]),
'lnum' : diagnostic[ 'line_num' ] + 1,
'col' : diagnostic[ 'column_num' ] + 1,
'text' : diagnostic[ 'text' ],
'type' : diagnostic[ 'kind' ],
'valid' : 1
}

View File

@ -233,11 +233,7 @@ class Completer( object ):
pass pass
def DiagnosticsForCurrentFileReady( self ): def GetDiagnosticsForCurrentFile( self, request_data ):
return False
def GetDiagnosticsForCurrentFile( self ):
return [] return []

View File

@ -44,7 +44,6 @@ class ClangCompleter( Completer ):
self._max_diagnostics_to_display = user_options[ self._max_diagnostics_to_display = user_options[
'max_diagnostics_to_display' ] 'max_diagnostics_to_display' ]
self._completer = ycm_core.ClangCompleter() self._completer = ycm_core.ClangCompleter()
self._last_prepared_diagnostics = []
self._flags = Flags() self._flags = Flags()
self._diagnostic_store = None self._diagnostic_store = None
self._logger = logging.getLogger( __name__ ) self._logger = logging.getLogger( __name__ )
@ -213,13 +212,6 @@ class ClangCompleter( Completer ):
ToUtf8IfNeeded( request_data[ 'unloaded_buffer' ] ) ) ToUtf8IfNeeded( request_data[ 'unloaded_buffer' ] ) )
def DiagnosticsForCurrentFileReady( self ):
# if not self.parse_future:
# return False
# return self.parse_future.ResultsReady()
pass
def GettingCompletions( self, request_data ): def GettingCompletions( self, request_data ):
return self._completer.UpdatingTranslationUnit( return self._completer.UpdatingTranslationUnit(
ToUtf8IfNeeded( request_data[ 'filepath' ] ) ) ToUtf8IfNeeded( request_data[ 'filepath' ] ) )
@ -227,19 +219,11 @@ class ClangCompleter( Completer ):
def GetDiagnosticsForCurrentFile( self, request_data ): def GetDiagnosticsForCurrentFile( self, request_data ):
filename = request_data[ 'filepath' ] filename = request_data[ 'filepath' ]
if self.DiagnosticsForCurrentFileReady(): diagnostics = self._completer.DiagnosticsForFile(
diagnostics = self._completer.DiagnosticsForFile( ToUtf8IfNeeded( filename ) )
ToUtf8IfNeeded( filename ) ) self._diagnostic_store = DiagnosticsToDiagStructure( diagnostics )
self._diagnostic_store = DiagnosticsToDiagStructure( diagnostics ) return [ ConvertToDiagnosticResponse( x ) for x in
self._last_prepared_diagnostics = [ diagnostics[ : self._max_diagnostics_to_display ] ]
responses.BuildDiagnosticData( x ) for x in
diagnostics[ : self._max_diagnostics_to_display ] ]
# self.parse_future = None
# if self.extra_parse_desired:
# self.OnFileReadyToParse( request_data )
return self._last_prepared_diagnostics
def GetDetailedDiagnostic( self, request_data ): def GetDetailedDiagnostic( self, request_data ):
@ -279,6 +263,7 @@ class ClangCompleter( Completer ):
source, source,
list( flags ) ) list( flags ) )
def _FlagsForRequest( self, request_data ): def _FlagsForRequest( self, request_data ):
filename = request_data[ 'filepath' ] filename = request_data[ 'filepath' ]
if 'compilation_flags' in request_data: if 'compilation_flags' in request_data:
@ -286,20 +271,6 @@ class ClangCompleter( Completer ):
filename ) filename )
return self._flags.FlagsForFile( filename ) return self._flags.FlagsForFile( filename )
# TODO: Make this work again
# def DiagnosticToDict( diagnostic ):
# # see :h getqflist for a description of the dictionary fields
# return {
# # TODO: wrap the bufnr generation into a function
# 'bufnr' : int( vim.eval( "bufnr('{0}', 1)".format(
# diagnostic.filename_ ) ) ),
# 'lnum' : diagnostic.line_number_,
# 'col' : diagnostic.column_number_,
# 'text' : diagnostic.text_,
# 'type' : diagnostic.kind_,
# 'valid' : 1
# }
def ConvertCompletionData( completion_data ): def ConvertCompletionData( completion_data ):
return responses.BuildCompletionData( return responses.BuildCompletionData(
@ -326,3 +297,11 @@ def InCFamilyFile( filetypes ):
return ClangAvailableForFiletypes( filetypes ) return ClangAvailableForFiletypes( filetypes )
def ConvertToDiagnosticResponse( diagnostic ):
return responses.BuildDiagnosticData( diagnostic.filename_,
diagnostic.line_number_ - 1,
diagnostic.column_number_ - 1,
diagnostic.text_,
diagnostic.kind_ )

View File

@ -1 +1 @@
{ "filepath_completion_use_working_dir": 0, "min_num_of_chars_for_completion": 2, "semantic_triggers": {}, "collect_identifiers_from_comments_and_strings": 0, "filetype_specific_completion_to_disable": {}, "collect_identifiers_from_tags_files": 0, "extra_conf_globlist": [], "global_ycm_extra_conf": "", "confirm_extra_conf": 1, "complete_in_comments": 0, "complete_in_strings": 1, "min_num_identifier_candidate_chars": 0, "max_diagnostics_to_display": 30, "auto_stop_csharp_server": 1, "seed_identifiers_with_syntax": 0, "csharp_server_port": 2000, "filetype_whitelist": { "*": "1" }, "auto_start_csharp_server": 1, "filetype_blacklist": { "tagbar": "1", "qf": "1", "gitcommit": "1", "notes": "1", "markdown": "1", "unite": "1", "text": "1" } } { "filepath_completion_use_working_dir": 0, "min_num_of_chars_for_completion": 2, "semantic_triggers": {}, "collect_identifiers_from_comments_and_strings": 0, "filetype_specific_completion_to_disable": { "gitcommit": 1, }, "collect_identifiers_from_tags_files": 0, "extra_conf_globlist": [], "global_ycm_extra_conf": "", "confirm_extra_conf": 1, "complete_in_comments": 0, "complete_in_strings": 1, "min_num_identifier_candidate_chars": 0, "max_diagnostics_to_display": 30, "auto_stop_csharp_server": 1, "seed_identifiers_with_syntax": 0, "csharp_server_port": 2000, "filetype_whitelist": { "*": "1" }, "auto_start_csharp_server": 1, "filetype_blacklist": { "tagbar": "1", "qf": "1", "notes": "1", "markdown": "1", "unite": "1", "text": "1" } }

View File

@ -21,7 +21,8 @@ from webtest import TestApp
from .. import ycmd from .. import ycmd
from ..responses import BuildCompletionData from ..responses import BuildCompletionData
from nose.tools import ok_, eq_, with_setup from nose.tools import ok_, eq_, with_setup
from hamcrest import assert_that, has_items, has_entry from hamcrest import ( assert_that, has_items, has_entry, contains,
contains_string, has_entries )
import bottle import bottle
bottle.debug( True ) bottle.debug( True )
@ -281,6 +282,47 @@ def DefinedSubcommands_WorksWhenNoExplicitCompleterTargetSpecified_test():
app.post_json( '/defined_subcommands', subcommands_data ).json ) app.post_json( '/defined_subcommands', subcommands_data ).json )
@with_setup( Setup )
def GetDiagnostics_ClangCompleter_ZeroBasedLineAndColumn_test():
app = TestApp( ycmd.app )
contents = """
struct Foo {
int x // semicolon missing here!
int y;
int c;
int d;
};
"""
filename = '/foo.cpp'
diag_data = {
'compilation_flags': ['-x', 'c++'],
'line_num': 0,
'column_num': 0,
'filetypes': ['cpp'],
'filepath': filename,
'file_data': {
filename: {
'contents': contents,
'filetypes': ['cpp']
}
}
}
event_data = diag_data.copy()
event_data.update( {
'event_name': 'FileReadyToParse',
} )
app.post_json( '/event_notification', event_data )
results = app.post_json( '/diagnostics', diag_data ).json
assert_that( results,
contains(
has_entries( { 'text': contains_string( "expected ';'" ),
'line_num': 2,
'column_num': 7 } ) ) )
@with_setup( Setup ) @with_setup( Setup )
def FiletypeCompletionAvailable_Works_test(): def FiletypeCompletionAvailable_Works_test():
app = TestApp( ycmd.app ) app = TestApp( ycmd.app )

View File

@ -94,18 +94,35 @@ def GetCompletions():
return _JsonResponse( completer.ComputeCandidates( request_data ) ) return _JsonResponse( completer.ComputeCandidates( request_data ) )
@app.post( '/diagnostics' )
def GetDiagnostics():
LOGGER.info( 'Received diagnostics request')
request_data = request.json
completer = _GetCompleterForRequestData( request_data )
return _JsonResponse( completer.GetDiagnosticsForCurrentFile(
request_data ) )
@app.get( '/user_options' ) @app.get( '/user_options' )
def GetUserOptions(): def GetUserOptions():
LOGGER.info( 'Received user options GET request') LOGGER.info( 'Received user options GET request')
return _JsonResponse( dict( SERVER_STATE.user_options ) ) return _JsonResponse( dict( SERVER_STATE.user_options ) )
@app.get( '/healthy' )
def GetHealthy():
LOGGER.info( 'Received health request')
return _JsonResponse( True )
@app.post( '/user_options' ) @app.post( '/user_options' )
def SetUserOptions(): def SetUserOptions():
LOGGER.info( 'Received user options POST request') LOGGER.info( 'Received user options POST request')
_SetUserOptions( request.json ) _SetUserOptions( request.json )
# TODO: Rename this to 'semantic_completion_available'
@app.post( '/filetype_completion_available') @app.post( '/filetype_completion_available')
def FiletypeCompletionAvailable(): def FiletypeCompletionAvailable():
LOGGER.info( 'Received filetype completion available request') LOGGER.info( 'Received filetype completion available request')

View File

@ -21,6 +21,7 @@ import tempfile
import os import os
import sys import sys
import signal import signal
import functools
def IsIdentifierChar( char ): def IsIdentifierChar( char ):
return char.isalnum() or char == '_' return char.isalnum() or char == '_'
@ -63,4 +64,13 @@ def AddThirdPartyFoldersToSysPath():
sys.path.insert( 0, os.path.realpath( os.path.join( path_to_third_party, sys.path.insert( 0, os.path.realpath( os.path.join( path_to_third_party,
folder ) ) ) folder ) ) )
def Memoize( obj ):
cache = obj.cache = {}
@functools.wraps( obj )
def memoizer( *args, **kwargs ):
key = str( args ) + str( kwargs )
if key not in cache:
cache[ key ] = obj( *args, **kwargs )
return cache[ key ]
return memoizer

View File

@ -18,6 +18,7 @@
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
import vim import vim
import os
def CurrentLineAndColumn(): def CurrentLineAndColumn():
"""Returns the 0-based current line and 0-based current column.""" """Returns the 0-based current line and 0-based current column."""
@ -75,6 +76,12 @@ def GetUnsavedAndCurrentBufferData():
return buffers_data return buffers_data
def GetBufferNumberForFilename( filename, open_file_if_needed = True ):
return int( vim.eval( "bufnr('{0}', {1})".format(
os.path.realpath( filename ),
int( open_file_if_needed ) ) ) )
# Both |line| and |column| need to be 1-based # Both |line| and |column| need to be 1-based
def JumpToLocation( filename, line, column ): def JumpToLocation( filename, line, column ):
# Add an entry to the jumplist # Add an entry to the jumplist

View File

@ -29,6 +29,7 @@ from ycm.completers.general import syntax_parse
from ycm.client.base_request import BaseRequest, BuildRequestData from ycm.client.base_request import BaseRequest, BuildRequestData
from ycm.client.command_request import SendCommandRequest from ycm.client.command_request import SendCommandRequest
from ycm.client.completion_request import CompletionRequest from ycm.client.completion_request import CompletionRequest
from ycm.client.diagnostics_request import DiagnosticsRequest
from ycm.client.event_notification import SendEventNotificationAsync from ycm.client.event_notification import SendEventNotificationAsync
try: try:
@ -43,7 +44,8 @@ class YouCompleteMe( object ):
def __init__( self, user_options ): def __init__( self, user_options ):
self._user_options = user_options self._user_options = user_options
self._omnicomp = OmniCompleter( user_options ) self._omnicomp = OmniCompleter( user_options )
self._current_completion_request = None self._latest_completion_request = None
self._latest_diagnostics_request = None
self._server_stdout = None self._server_stdout = None
self._server_stderr = None self._server_stderr = None
self._server_popen = None self._server_popen = None
@ -91,8 +93,8 @@ class YouCompleteMe( object ):
# We have to store a reference to the newly created CompletionRequest # We have to store a reference to the newly created CompletionRequest
# because VimScript can't store a reference to a Python object across # because VimScript can't store a reference to a Python object across
# function calls... Thus we need to keep this request somewhere. # function calls... Thus we need to keep this request somewhere.
self._current_completion_request = CompletionRequest() self._latest_completion_request = CompletionRequest()
return self._current_completion_request return self._latest_completion_request
def SendCommandRequest( self, arguments, completer ): def SendCommandRequest( self, arguments, completer ):
@ -105,7 +107,7 @@ class YouCompleteMe( object ):
def GetCurrentCompletionRequest( self ): def GetCurrentCompletionRequest( self ):
return self._current_completion_request return self._latest_completion_request
def GetOmniCompleter( self ): def GetOmniCompleter( self ):
@ -114,8 +116,7 @@ class YouCompleteMe( object ):
def NativeFiletypeCompletionAvailable( self ): def NativeFiletypeCompletionAvailable( self ):
try: try:
return BaseRequest.PostDataToHandler( BuildRequestData(), return _NativeFiletypeCompletionAvailableForFile( vim.current.buffer.name )
'filetype_completion_available')
except: except:
return False return False
@ -174,17 +175,25 @@ class YouCompleteMe( object ):
SendEventNotificationAsync( 'CurrentIdentifierFinished' ) SendEventNotificationAsync( 'CurrentIdentifierFinished' )
# TODO: Make this work again.
def DiagnosticsForCurrentFileReady( self ): def DiagnosticsForCurrentFileReady( self ):
# if self.FiletypeCompletionUsable(): return bool( self._latest_diagnostics_request and
# return self.GetFiletypeCompleter().DiagnosticsForCurrentFileReady() self._latest_diagnostics_request.Done() )
return False
# TODO: Make this work again. def RequestDiagnosticsForCurrentFile( self ):
def GetDiagnosticsForCurrentFile( self ): self._latest_diagnostics_request = DiagnosticsRequest()
# if self.FiletypeCompletionUsable(): self._latest_diagnostics_request.Start()
# return self.GetFiletypeCompleter().GetDiagnosticsForCurrentFile()
def GetDiagnosticsFromStoredRequest( self ):
if self._latest_diagnostics_request:
to_return = self._latest_diagnostics_request.Response()
# We set the diagnostics request to None because we want to prevent
# Syntastic from repeatedly refreshing the buffer with the same diags.
# Setting this to None makes DiagnosticsForCurrentFileReady return False
# until the next request is created.
self._latest_diagnostics_request = None
return to_return
return [] return []
@ -260,3 +269,12 @@ def _AddUltiSnipsDataIfNeeded( extra_data ):
} for x in rawsnips ] } for x in rawsnips ]
# 'filepath' is here only as a key for Memoize
# This can't be a nested function inside NativeFiletypeCompletionAvailable
# because then the Memoize decorator wouldn't work (nested functions are
# re-created on every call to the outer function).
@utils.Memoize
def _NativeFiletypeCompletionAvailableForFile( filepath ):
return BaseRequest.PostDataToHandler( BuildRequestData(),
'filetype_completion_available')

80
third_party/retries/retries.py vendored Normal file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env python
#
# Copyright 2012 by Jeff Laughlin Consulting LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import sys
from time import sleep
# Source: https://gist.github.com/n1ywb/2570004
def example_exc_handler(tries_remaining, exception, delay):
"""Example exception handler; prints a warning to stderr.
tries_remaining: The number of tries remaining.
exception: The exception instance which was raised.
"""
print >> sys.stderr, "Caught '%s', %d tries remaining, sleeping for %s seconds" % (exception, tries_remaining, delay)
def retries(max_tries, delay=1, backoff=2, exceptions=(Exception,), hook=None):
"""Function decorator implementing retrying logic.
delay: Sleep this many seconds * backoff * try number after failure
backoff: Multiply delay by this factor after each failure
exceptions: A tuple of exception classes; default (Exception,)
hook: A function with the signature myhook(tries_remaining, exception);
default None
The decorator will call the function up to max_tries times if it raises
an exception.
By default it catches instances of the Exception class and subclasses.
This will recover after all but the most fatal errors. You may specify a
custom tuple of exception classes with the 'exceptions' argument; the
function will only be retried if it raises one of the specified
exceptions.
Additionally you may specify a hook function which will be called prior
to retrying with the number of remaining tries and the exception instance;
see given example. This is primarily intended to give the opportunity to
log the failure. Hook is not called after failure if no retries remain.
"""
def dec(func):
def f2(*args, **kwargs):
mydelay = delay
tries = range(max_tries)
tries.reverse()
for tries_remaining in tries:
try:
return func(*args, **kwargs)
except exceptions as e:
if tries_remaining > 0:
if hook is not None:
hook(tries_remaining, e, mydelay)
sleep(mydelay)
mydelay = mydelay * backoff
else:
raise
else:
break
return f2
return dec