diff --git a/.travis.yml b/.travis.yml index ef885f0..4eb47fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,17 +19,27 @@ env: - DISABLE_LOGBOOK_CEXT=True - CYBUILD=True + script: - pip install -e .[all] +- if [[ $GEVENT == 'True' ]] ; then pip install gevent; fi - py.test --cov=logbook -r s tests + matrix: exclude: - python: pypy env: CYBUILD=True - python: pypy3 env: CYBUILD=True + include: + - python: "3.6" + env: GEVENT=True CYBUILD=True + - python: "2.7" + env: GEVENT=True CYBUILD=True + after_success: - coveralls + notifications: email: recipients: diff --git a/CHANGES b/CHANGES index 11781b1..7530095 100644 --- a/CHANGES +++ b/CHANGES @@ -2,6 +2,21 @@ ================= Here you can see the full list of changes between each Logbook release. + +Version 1.4.1 + +Released on October 14th, 2018 + +- Fixed deprecated regular expression pattern (thanks Tsuyoshi Hombashi) +- Fixed TimedRotatingFileHandler rotation (thanks Tucker Beck) + +Version 1.4.0 +------------- + +Released on May 15th, 2018 + +- Added support for checking if trace logs have been emitted in TestHandler (thanks @thedrow) + Version 1.3.0 ------------- diff --git a/logbook/__version__.py b/logbook/__version__.py index 67bc602..bf25615 100644 --- a/logbook/__version__.py +++ b/logbook/__version__.py @@ -1 +1 @@ -__version__ = "1.3.0" +__version__ = "1.4.1" diff --git a/logbook/concurrency.py b/logbook/concurrency.py index b7a6758..f0d5373 100644 --- a/logbook/concurrency.py +++ b/logbook/concurrency.py @@ -28,10 +28,16 @@ if has_gevent: - from gevent._threading import (Lock as ThreadLock, - RLock as ThreadRLock, - get_ident as thread_get_ident, - local as thread_local) + from gevent.monkey import get_original as _get_original + ThreadLock = _get_original('threading', 'Lock') + ThreadRLock = _get_original('threading', 'RLock') + try: + thread_get_ident = _get_original('threading', 'get_ident') + except AttributeError: + # In 2.7, this is called _get_ident + thread_get_ident = _get_original('threading', '_get_ident') + thread_local = _get_original('threading', 'local') + from gevent.thread import get_ident as greenlet_get_ident from gevent.local import local as greenlet_local from gevent.lock import BoundedSemaphore diff --git a/logbook/handlers.py b/logbook/handlers.py index e00b6c0..3533445 100644 --- a/logbook/handlers.py +++ b/logbook/handlers.py @@ -28,7 +28,7 @@ from textwrap import dedent from logbook.base import ( - CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG, NOTSET, level_name_property, + CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG, TRACE, NOTSET, level_name_property, _missing, lookup_level, Flags, ContextObject, ContextStackManager, _datetime_factory) from logbook.helpers import ( @@ -901,10 +901,14 @@ self.timed_filename_for_current = timed_filename_for_current self._timestamp = self._get_timestamp(_datetime_factory()) - timed_filename = self.generate_timed_filename(self._timestamp) - if self.timed_filename_for_current: - filename = timed_filename + filename = self.generate_timed_filename(self._timestamp) + elif os.path.exists(filename): + self._timestamp = self._get_timestamp( + datetime.fromtimestamp( + os.stat(filename).st_mtime + ) + ) FileHandler.__init__(self, filename, mode, encoding, level, format_string, True, filter, bubble) @@ -932,14 +936,14 @@ """ directory = os.path.dirname(self._filename) files = [] + rollover_regex = re.compile(self.rollover_format.format( + basename=re.escape(self.basename), + timestamp='.+', + ext=re.escape(self.ext), + )) for filename in os.listdir(directory): filename = os.path.join(directory, filename) - regex = self.rollover_format.format( - basename=re.escape(self.basename), - timestamp='.+', - ext=re.escape(self.ext), - ) - if re.match(regex, filename): + if rollover_regex.match(filename): files.append((os.path.getmtime(filename), filename)) files.sort() if self.backup_count > 1: @@ -951,15 +955,19 @@ if self.stream is not None: self.stream.close() + if ( + not self.timed_filename_for_current + and os.path.exists(self._filename) + ): + filename = self.generate_timed_filename(self._timestamp) + os.rename(self._filename, filename) + if self.backup_count > 0: for time, filename in self.files_to_delete(): os.remove(filename) if self.timed_filename_for_current: self._filename = self.generate_timed_filename(new_timestamp) - else: - filename = self.generate_timed_filename(self._timestamp) - os.rename(self._filename, filename) self._timestamp = new_timestamp self._open('w') @@ -1055,6 +1063,11 @@ """`True` if any :data:`DEBUG` records were found.""" return any(r.level == DEBUG for r in self.records) + @property + def has_traces(self): + """`True` if any :data:`TRACE` records were found.""" + return any(r.level == TRACE for r in self.records) + def has_critical(self, *args, **kwargs): """`True` if a specific :data:`CRITICAL` log record exists. @@ -1101,6 +1114,14 @@ See :ref:`probe-log-records` for more information. """ kwargs['level'] = DEBUG + return self._test_for(*args, **kwargs) + + def has_trace(self, *args, **kwargs): + """`True` if a specific :data:`TRACE` log record exists. + + See :ref:`probe-log-records` for more information. + """ + kwargs['level'] = TRACE return self._test_for(*args, **kwargs) def _test_for(self, message=None, channel=None, level=None): diff --git a/logbook/more.py b/logbook/more.py index 2d15f80..10b06ca 100644 --- a/logbook/more.py +++ b/logbook/more.py @@ -38,7 +38,7 @@ else: from urllib.parse import parse_qsl, urlencode -_ws_re = re.compile(r'(\s+)(?u)') +_ws_re = re.compile(r'(\s+)', re.UNICODE) TWITTER_FORMAT_STRING = u( '[{record.channel}] {record.level_name}: {record.message}') TWITTER_ACCESS_TOKEN_URL = 'https://twitter.com/oauth/access_token' diff --git a/tests/test_file_handler.py b/tests/test_file_handler.py index 2c0848a..9585832 100644 --- a/tests/test_file_handler.py +++ b/tests/test_file_handler.py @@ -1,5 +1,6 @@ import os import pytest +import time from datetime import datetime import logbook @@ -167,16 +168,27 @@ assert f.readline().rstrip() == '[01:00] Third One' assert f.readline().rstrip() == '[02:00] Third One' + @pytest.mark.parametrize("backup_count", [1, 3]) -def test_timed_rotating_file_handler__not_timed_filename_for_current(tmpdir, activation_strategy, backup_count): +@pytest.mark.parametrize("preexisting_file", [True, False]) +def test_timed_rotating_file_handler__not_timed_filename_for_current( + tmpdir, activation_strategy, backup_count, preexisting_file +): basename = str(tmpdir.join('trot.log')) + + if preexisting_file: + with open(basename, 'w') as file: + file.write('contents') + jan_first = time.mktime(datetime(2010, 1, 1).timetuple()) + os.utime(basename, (jan_first, jan_first)) + handler = logbook.TimedRotatingFileHandler( - basename, backup_count=backup_count, + basename, + format_string='[{record.time:%H:%M}] {record.message}', + backup_count=backup_count, rollover_format='{basename}{ext}.{timestamp}', timed_filename_for_current=False, ) - handler._timestamp = handler._get_timestamp(datetime(2010, 1, 5)) - handler.format_string = '[{record.time:%H:%M}] {record.message}' def fake_record(message, year, month, day, hour=0, minute=0, second=0): @@ -195,10 +207,15 @@ for x in xrange(20): handler.handle(fake_record('Last One', 2010, 1, 8, x + 1)) - files = sorted(x for x in os.listdir(str(tmpdir)) if x.startswith('trot')) - - assert files == ['trot.log'] + ['trot.log.2010-01-0{0}'.format(i) - for i in xrange(5, 8)][-backup_count:] + computed_files = [x for x in os.listdir(str(tmpdir)) if x.startswith('trot')] + + expected_files = ['trot.log.2010-01-01'] if preexisting_file else [] + expected_files += ['trot.log.2010-01-0{0}'.format(i) for i in xrange(5, 8)] + expected_files += ['trot.log'] + expected_files = expected_files[-backup_count:] + + assert sorted(computed_files) == sorted(expected_files) + with open(str(tmpdir.join('trot.log'))) as f: assert f.readline().rstrip() == '[01:00] Last One' assert f.readline().rstrip() == '[02:00] Last One' diff --git a/tests/test_test_handler.py b/tests/test_test_handler.py index 5c92854..4d651fa 100644 --- a/tests/test_test_handler.py +++ b/tests/test_test_handler.py @@ -1,11 +1,39 @@ import re +import pytest -def test_regex_matching(active_handler, logger): - logger.warn('Hello World!') - assert active_handler.has_warning(re.compile('^Hello')) - assert (not active_handler.has_warning(re.compile('world$'))) - assert (not active_handler.has_warning('^Hello World')) + +@pytest.mark.parametrize("level, method", [ + ("trace", "has_traces"), + ("debug", "has_debugs"), + ("info", "has_infos"), + ("notice", "has_notices"), + ("warning", "has_warnings"), + ("error", "has_errors"), + ("critical", "has_criticals"), +]) +def test_has_level(active_handler, logger, level, method): + log = getattr(logger, level) + log('Hello World') + assert getattr(active_handler, method) + + +@pytest.mark.parametrize("level, method", [ + ("trace", "has_trace"), + ("debug", "has_debug"), + ("info", "has_info"), + ("notice", "has_notice"), + ("warning", "has_warning"), + ("error", "has_error"), + ("critical", "has_critical"), +]) +def test_regex_matching(active_handler, logger, level, method): + log = getattr(logger, level) + log('Hello World') + has_level_method = getattr(active_handler, method) + assert has_level_method(re.compile('^Hello')) + assert (not has_level_method(re.compile('world$'))) + assert (not has_level_method('^Hello World')) def test_test_handler_cache(active_handler, logger):