From b9bb788a2a393fefbefb6073fbeb4bae309e247d Mon Sep 17 00:00:00 2001 From: Strahinja Val Markovic Date: Thu, 3 Oct 2013 10:14:31 -0700 Subject: [PATCH] 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. --- autoload/youcompleteme.vim | 25 +++--- cpp/ycm/ClangCompleter/TranslationUnit.cpp | 18 +---- python/ycm/client/command_request.py | 2 +- python/ycm/client/diagnostics_request.py | 75 ++++++++++++++++++ python/ycm/completers/completer.py | 6 +- python/ycm/completers/cpp/clang_completer.py | 49 ++++-------- python/ycm/server/default_settings.json | 2 +- python/ycm/server/tests/basic_test.py | 44 ++++++++++- python/ycm/server/ycmd.py | 17 +++++ python/ycm/utils.py | 10 +++ python/ycm/vimsupport.py | 7 ++ python/ycm/youcompleteme.py | 46 +++++++---- third_party/retries/retries.py | 80 ++++++++++++++++++++ 13 files changed, 298 insertions(+), 83 deletions(-) create mode 100644 python/ycm/client/diagnostics_request.py create mode 100644 third_party/retries/retries.py diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 550151f9..0c103d5c 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -260,7 +260,7 @@ function! s:OnCursorHold() call s:SetUpCompleteopt() " Order is important here; we need to extract any done diagnostics before " reparsing the file again - " call s:UpdateDiagnosticNotifications() + call s:UpdateDiagnosticNotifications() call s:OnFileReadyToParse() endfunction @@ -272,6 +272,7 @@ function! s:OnFileReadyToParse() let buffer_changed = b:changedtick != b:ycm_changedtick.file_ready_to_parse if buffer_changed + py ycm_state.RequestDiagnosticsForCurrentFile() py ycm_state.OnFileReadyToParse() endif let b:ycm_changedtick.file_ready_to_parse = b:changedtick @@ -327,7 +328,7 @@ function! s:OnCursorMovedNormalMode() return endif - " call s:UpdateDiagnosticNotifications() + call s:UpdateDiagnosticNotifications() call s:OnFileReadyToParse() endfunction @@ -338,7 +339,7 @@ function! s:OnInsertLeave() endif let s:omnifunc_mode = 0 - " call s:UpdateDiagnosticNotifications() + call s:UpdateDiagnosticNotifications() call s:OnFileReadyToParse() py ycm_state.OnInsertLeave() if g:ycm_autoclose_preview_window_after_completion || @@ -408,10 +409,16 @@ endfunction function! s:UpdateDiagnosticNotifications() - if get( g:, 'loaded_syntastic_plugin', 0 ) && - \ pyeval( 'ycm_state.NativeFiletypeCompletionUsable()' ) && - \ pyeval( 'ycm_state.DiagnosticsForCurrentFileReady()' ) && - \ g:ycm_register_as_syntastic_checker + let should_display_diagnostics = + \ get( g:, 'loaded_syntastic_plugin', 0 ) && + \ g:ycm_register_as_syntastic_checker && + \ pyeval( 'ycm_state.NativeFiletypeCompletionUsable()' ) + + if !should_display_diagnostics + return + endif + + if pyeval( 'ycm_state.DiagnosticsForCurrentFileReady()' ) SyntasticCheck endif endfunction @@ -566,9 +573,7 @@ command! YcmShowDetailedDiagnostic call s:ShowDetailedDiagnostic() " required (currently that's on buffer save) OR when the SyntasticCheck command " is invoked function! youcompleteme#CurrentFileDiagnostics() - " TODO: Make this work again. - " return pyeval( 'ycm_state.GetDiagnosticsForCurrentFile()' ) - return [] + return pyeval( 'ycm_state.GetDiagnosticsFromStoredRequest()' ) endfunction diff --git a/cpp/ycm/ClangCompleter/TranslationUnit.cpp b/cpp/ycm/ClangCompleter/TranslationUnit.cpp index 0acc0681..efaaf5ff 100644 --- a/cpp/ycm/ClangCompleter/TranslationUnit.cpp +++ b/cpp/ycm/ClangCompleter/TranslationUnit.cpp @@ -92,25 +92,11 @@ void TranslationUnit::Destroy() { std::vector< Diagnostic > TranslationUnit::LatestDiagnostics() { - std::vector< Diagnostic > diagnostics; - if ( !clang_translation_unit_ ) - return diagnostics; + return std::vector< Diagnostic >(); unique_lock< mutex > lock( diagnostics_mutex_ ); - - // 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; + return latest_diagnostics_; } diff --git a/python/ycm/client/command_request.py b/python/ycm/client/command_request.py index 0efbbae7..f2b92ab3 100644 --- a/python/ycm/client/command_request.py +++ b/python/ycm/client/command_request.py @@ -30,7 +30,7 @@ class CommandRequest( BaseRequest ): self._completer_target = ( completer_target if completer_target else 'filetype_default' ) self._is_goto_command = ( - True if arguments and arguments[ 0 ].startswith( 'GoTo' ) else False ) + arguments and arguments[ 0 ].startswith( 'GoTo' ) ) self._response = None diff --git a/python/ycm/client/diagnostics_request.py b/python/ycm/client/diagnostics_request.py new file mode 100644 index 00000000..505f927c --- /dev/null +++ b/python/ycm/client/diagnostics_request.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013 Strahinja Val Markovic +# +# 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 . + +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 + } + diff --git a/python/ycm/completers/completer.py b/python/ycm/completers/completer.py index c44b5572..99daa29b 100644 --- a/python/ycm/completers/completer.py +++ b/python/ycm/completers/completer.py @@ -233,11 +233,7 @@ class Completer( object ): pass - def DiagnosticsForCurrentFileReady( self ): - return False - - - def GetDiagnosticsForCurrentFile( self ): + def GetDiagnosticsForCurrentFile( self, request_data ): return [] diff --git a/python/ycm/completers/cpp/clang_completer.py b/python/ycm/completers/cpp/clang_completer.py index 31a0bf10..e2f48718 100644 --- a/python/ycm/completers/cpp/clang_completer.py +++ b/python/ycm/completers/cpp/clang_completer.py @@ -44,7 +44,6 @@ class ClangCompleter( Completer ): self._max_diagnostics_to_display = user_options[ 'max_diagnostics_to_display' ] self._completer = ycm_core.ClangCompleter() - self._last_prepared_diagnostics = [] self._flags = Flags() self._diagnostic_store = None self._logger = logging.getLogger( __name__ ) @@ -213,13 +212,6 @@ class ClangCompleter( Completer ): 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 ): return self._completer.UpdatingTranslationUnit( ToUtf8IfNeeded( request_data[ 'filepath' ] ) ) @@ -227,19 +219,11 @@ class ClangCompleter( Completer ): def GetDiagnosticsForCurrentFile( self, request_data ): filename = request_data[ 'filepath' ] - if self.DiagnosticsForCurrentFileReady(): - diagnostics = self._completer.DiagnosticsForFile( - ToUtf8IfNeeded( filename ) ) - self._diagnostic_store = DiagnosticsToDiagStructure( diagnostics ) - self._last_prepared_diagnostics = [ - 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 + diagnostics = self._completer.DiagnosticsForFile( + ToUtf8IfNeeded( filename ) ) + self._diagnostic_store = DiagnosticsToDiagStructure( diagnostics ) + return [ ConvertToDiagnosticResponse( x ) for x in + diagnostics[ : self._max_diagnostics_to_display ] ] def GetDetailedDiagnostic( self, request_data ): @@ -279,6 +263,7 @@ class ClangCompleter( Completer ): source, list( flags ) ) + def _FlagsForRequest( self, request_data ): filename = request_data[ 'filepath' ] if 'compilation_flags' in request_data: @@ -286,20 +271,6 @@ class ClangCompleter( Completer ): 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 ): return responses.BuildCompletionData( @@ -326,3 +297,11 @@ def InCFamilyFile( filetypes ): return ClangAvailableForFiletypes( filetypes ) +def ConvertToDiagnosticResponse( diagnostic ): + return responses.BuildDiagnosticData( diagnostic.filename_, + diagnostic.line_number_ - 1, + diagnostic.column_number_ - 1, + diagnostic.text_, + diagnostic.kind_ ) + + diff --git a/python/ycm/server/default_settings.json b/python/ycm/server/default_settings.json index 0ae36f9b..8dc9005e 100644 --- a/python/ycm/server/default_settings.json +++ b/python/ycm/server/default_settings.json @@ -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" } } \ No newline at end of file +{ "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" } } \ No newline at end of file diff --git a/python/ycm/server/tests/basic_test.py b/python/ycm/server/tests/basic_test.py index eebce437..4f71da82 100644 --- a/python/ycm/server/tests/basic_test.py +++ b/python/ycm/server/tests/basic_test.py @@ -21,7 +21,8 @@ from webtest import TestApp from .. import ycmd from ..responses import BuildCompletionData 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 bottle.debug( True ) @@ -281,6 +282,47 @@ def DefinedSubcommands_WorksWhenNoExplicitCompleterTargetSpecified_test(): 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 ) def FiletypeCompletionAvailable_Works_test(): app = TestApp( ycmd.app ) diff --git a/python/ycm/server/ycmd.py b/python/ycm/server/ycmd.py index b23d0b34..ef467058 100755 --- a/python/ycm/server/ycmd.py +++ b/python/ycm/server/ycmd.py @@ -94,18 +94,35 @@ def GetCompletions(): 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' ) def GetUserOptions(): LOGGER.info( 'Received user options GET request') return _JsonResponse( dict( SERVER_STATE.user_options ) ) +@app.get( '/healthy' ) +def GetHealthy(): + LOGGER.info( 'Received health request') + return _JsonResponse( True ) + + @app.post( '/user_options' ) def SetUserOptions(): LOGGER.info( 'Received user options POST request') _SetUserOptions( request.json ) +# TODO: Rename this to 'semantic_completion_available' @app.post( '/filetype_completion_available') def FiletypeCompletionAvailable(): LOGGER.info( 'Received filetype completion available request') diff --git a/python/ycm/utils.py b/python/ycm/utils.py index f861947b..72dfd268 100644 --- a/python/ycm/utils.py +++ b/python/ycm/utils.py @@ -21,6 +21,7 @@ import tempfile import os import sys import signal +import functools def IsIdentifierChar( 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, 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 diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index 78047003..d46ec868 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -18,6 +18,7 @@ # along with YouCompleteMe. If not, see . import vim +import os def CurrentLineAndColumn(): """Returns the 0-based current line and 0-based current column.""" @@ -75,6 +76,12 @@ def GetUnsavedAndCurrentBufferData(): 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 def JumpToLocation( filename, line, column ): # Add an entry to the jumplist diff --git a/python/ycm/youcompleteme.py b/python/ycm/youcompleteme.py index 7777f390..9741e741 100644 --- a/python/ycm/youcompleteme.py +++ b/python/ycm/youcompleteme.py @@ -29,6 +29,7 @@ from ycm.completers.general import syntax_parse from ycm.client.base_request import BaseRequest, BuildRequestData from ycm.client.command_request import SendCommandRequest from ycm.client.completion_request import CompletionRequest +from ycm.client.diagnostics_request import DiagnosticsRequest from ycm.client.event_notification import SendEventNotificationAsync try: @@ -43,7 +44,8 @@ class YouCompleteMe( object ): def __init__( self, user_options ): self._user_options = 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_stderr = None self._server_popen = None @@ -91,8 +93,8 @@ class YouCompleteMe( object ): # We have to store a reference to the newly created CompletionRequest # because VimScript can't store a reference to a Python object across # function calls... Thus we need to keep this request somewhere. - self._current_completion_request = CompletionRequest() - return self._current_completion_request + self._latest_completion_request = CompletionRequest() + return self._latest_completion_request def SendCommandRequest( self, arguments, completer ): @@ -105,7 +107,7 @@ class YouCompleteMe( object ): def GetCurrentCompletionRequest( self ): - return self._current_completion_request + return self._latest_completion_request def GetOmniCompleter( self ): @@ -114,8 +116,7 @@ class YouCompleteMe( object ): def NativeFiletypeCompletionAvailable( self ): try: - return BaseRequest.PostDataToHandler( BuildRequestData(), - 'filetype_completion_available') + return _NativeFiletypeCompletionAvailableForFile( vim.current.buffer.name ) except: return False @@ -174,17 +175,25 @@ class YouCompleteMe( object ): SendEventNotificationAsync( 'CurrentIdentifierFinished' ) - # TODO: Make this work again. def DiagnosticsForCurrentFileReady( self ): - # if self.FiletypeCompletionUsable(): - # return self.GetFiletypeCompleter().DiagnosticsForCurrentFileReady() - return False + return bool( self._latest_diagnostics_request and + self._latest_diagnostics_request.Done() ) - # TODO: Make this work again. - def GetDiagnosticsForCurrentFile( self ): - # if self.FiletypeCompletionUsable(): - # return self.GetFiletypeCompleter().GetDiagnosticsForCurrentFile() + def RequestDiagnosticsForCurrentFile( self ): + self._latest_diagnostics_request = DiagnosticsRequest() + self._latest_diagnostics_request.Start() + + + 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 [] @@ -260,3 +269,12 @@ def _AddUltiSnipsDataIfNeeded( extra_data ): } 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') + diff --git a/third_party/retries/retries.py b/third_party/retries/retries.py new file mode 100644 index 00000000..1d7131d5 --- /dev/null +++ b/third_party/retries/retries.py @@ -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