diff --git a/loguru/_datetime.py b/loguru/_datetime.py new file mode 100644 index 000000000..ec77d9f8c --- /dev/null +++ b/loguru/_datetime.py @@ -0,0 +1,82 @@ +from datetime import datetime as datetime_, timezone, timedelta +from calendar import day_name, day_abbr, month_name, month_abbr +from time import time, localtime +import re + + +tokens = r"H{1,2}|h{1,2}|m{1,2}|s{1,2}|S{1,6}|YYYY|YY|M{1,4}|D{1,4}|Z{1,2}|zz|A|X|x|E|Q|dddd|ddd|d" + +pattern = re.compile(r"(?:{0})|\[(?:{0})\]".format(tokens)) + + +def now(): + t = time() + local = localtime(t) + microsecond = int(abs(t) * 1e6 % 1e6) + tzinfo = timezone(timedelta(seconds=local.tm_gmtoff), local.tm_zone) + year, month, day, hour, minute, second, _, _, _ = local + return datetime(year, month, day, hour, minute, second, microsecond, tzinfo) + + +class datetime(datetime_): + + def __format__(self, spec): + if not spec: + spec = "%Y-%m-%dT%H:%M:%S.%f%z" + + if '%' in spec: + return super().__format__(spec) + + year, month, day, hour, minute, second, weekday, yearday, _ = self.timetuple() + microsecond = self.microsecond + timestamp = self.timestamp() + tzinfo = self.tzinfo or timezone(timedelta(seconds=0)) + offset = tzinfo.utcoffset(self).total_seconds() + sign = ('-', '+')[offset >= 0] + h, m = divmod(abs(offset // 60), 60) + + rep = { + 'YYYY': '%04d' % year, + 'YY': '%02d' % (year % 100), + 'Q': '%d' % ((month - 1) // 3 + 1), + 'MMMM': month_name[month - 1], + 'MMM': month_abbr[month - 1], + 'MM': '%02d' % month, + 'M': '%d' % month, + 'DDDD': '%03d' % yearday, + 'DDD': '%d' % yearday, + 'DD': '%02d' % day, + 'D': '%d' % day, + 'dddd': day_name[weekday], + 'ddd': day_abbr[weekday], + 'd': '%d' % weekday, + 'E': '%d' % (weekday + 1), + 'HH': '%02d' % hour, + 'H': '%d' % hour, + 'hh': '%02d' % ((hour - 1) % 12 + 1), + 'h': '%d' % ((hour - 1) % 12 + 1), + 'mm': '%02d' % minute, + 'm': '%d' % minute, + 'ss': '%02d' % second, + 's': '%d' % second, + 'S': '%d' % (microsecond // 100000), + 'SS': '%02d' % (microsecond // 10000), + 'SSS': '%03d' % (microsecond // 1000), + 'SSSS': '%04d' % (microsecond // 100), + 'SSSSS': '%05d' % (microsecond // 10), + 'SSSSSS': '%06d' % microsecond, + 'A': ('AM', 'PM')[hour // 12], + 'Z': '%s%02d:%02d' % (sign, h, m), + 'ZZ': '%s%02d%02d' % (sign, h, m), + 'zz': tzinfo.tzname(self) or '', + 'X': '%d' % timestamp, + 'x': '%d' % (int(timestamp) * 1000000 + microsecond), + } + + def get(m): + try: + return rep[m[0]] + except KeyError: + return m[0][1:-1] + + return pattern.sub(get, spec) diff --git a/loguru/_file_sink.py b/loguru/_file_sink.py index e7de198c0..3fc60b973 100644 --- a/loguru/_file_sink.py +++ b/loguru/_file_sink.py @@ -6,24 +6,19 @@ import shutil import string -import pendulum -from pendulum import now as pendulum_now +from . import _string_parsers as string_parsers +from ._datetime import now -from . import _string_parsers +class FileDateFormatter: -class FileDateTime(pendulum.DateTime): + def __init__(self): + self.datetime = now() def __format__(self, spec): if not spec: - spec = "YYYY-MM-DD_HH-mm-ss_SSSSSS" - return super().__format__(spec) - - @classmethod - def now(cls): - # TODO: Use FileDateTime.now() instead, when pendulum/#203 fixed - t = pendulum_now() - return cls(t.year, t.month, t.day, t.hour, t.minute, t.second, t.microsecond, t.tzinfo, fold=t.fold) + spec = "%Y-%m-%d_%H-%M-%S_%f" + return self.datetime.__format__(spec) class FileSink: @@ -78,14 +73,14 @@ def initialize_file(self, *, rename_existing): if rename_existing and os.path.isfile(new_path): root, ext = os.path.splitext(new_path) - renamed_path = "{}.{:YYYY-MM-DD_HH-mm-ss_SSSSSS}{}".format(root, pendulum_now(), ext) + renamed_path = "{}.{}{}".format(root, FileDateFormatter(), ext) os.rename(new_path, renamed_path) self.file = open(new_path, mode=self.mode, buffering=self.buffering, **self.kwargs) self.file_path = new_path def format_path(self): - path = self.path.format_map({'time': FileDateTime.now()}) + path = self.path.format_map({'time': FileDateFormatter()}) return os.path.abspath(path) @staticmethod @@ -108,15 +103,17 @@ def rotation_function(message, file): return rotation_function def make_from_time(step_forward, time_init=None): - start_time = time_limit = pendulum_now() + start_time = time_limit = now().replace(tzinfo=None) if time_init is not None: - t = time_init - time_limit = time_limit.at(t.hour, t.minute, t.second, t.microsecond) + time_limit = time_limit.replace(hour=time_init.hour, + minute=time_init.minute, + second=time_init.second, + microsecond=time_init.microsecond) if time_limit <= start_time: time_limit = step_forward(time_limit) def rotation_function(message, file): nonlocal time_limit - record_time = message.record['time'] + record_time = message.record['time'].replace(tzinfo=None) if record_time >= time_limit: while time_limit <= record_time: time_limit = step_forward(time_limit) @@ -127,40 +124,38 @@ def rotation_function(message, file): if rotation is None: return None elif isinstance(rotation, str): - size = _string_parsers.parse_size(rotation) + size = string_parsers.parse_size(rotation) if size is not None: return self.make_rotation_function(size) - interval = _string_parsers.parse_duration(rotation) + interval = string_parsers.parse_duration(rotation) if interval is not None: return self.make_rotation_function(interval) - frequency = _string_parsers.parse_frequency(rotation) + frequency = string_parsers.parse_frequency(rotation) if frequency is not None: return make_from_time(frequency) - daytime = _string_parsers.parse_daytime(rotation) + daytime = string_parsers.parse_daytime(rotation) if daytime is not None: day, time = daytime if day is None: return self.make_rotation_function(time) if time is None: - time = pendulum.parse('00:00', exact=True, strict=False) + time = datetime.time(0, 0, 0) def next_day(t): - return t.next(day, keep_time=True) + while True: + t += datetime.timedelta(days=1) + if t.weekday() == day: + return t return make_from_time(next_day, time_init=time) raise ValueError("Cannot parse rotation from: '%s'" % rotation) elif isinstance(rotation, (numbers.Real, decimal.Decimal)): return make_from_size(rotation) elif isinstance(rotation, datetime.time): - time = pendulum.Time(hour=rotation.hour, minute=rotation.minute, - second=rotation.second, microsecond=rotation.microsecond, - tzinfo=rotation.tzinfo, fold=rotation.fold) def next_day(t): - return t.add(days=1) - return make_from_time(next_day, time_init=time) + return t + datetime.timedelta(days=1) + return make_from_time(next_day, time_init=rotation) elif isinstance(rotation, datetime.timedelta): - interval = pendulum.Duration(days=rotation.days, seconds=rotation.seconds, - microseconds=rotation.microseconds) def add_interval(t): - return t + interval + return t + rotation return make_from_time(add_interval) elif callable(rotation): return rotation @@ -178,7 +173,7 @@ def retention_function(logs): if retention is None: return None elif isinstance(retention, str): - interval = _string_parsers.parse_duration(retention) + interval = string_parsers.parse_duration(retention) if interval is None: raise ValueError("Cannot parse retention from: '%s'" % retention) return self.make_retention_function(interval) @@ -191,7 +186,7 @@ def filter_logs(logs): elif isinstance(retention, datetime.timedelta): seconds = retention.total_seconds() def filter_logs(logs): - t = pendulum_now().timestamp() + t = now().timestamp() return [log for log in logs if os.stat(log).st_mtime <= t - seconds] return make_from_filter(filter_logs) elif callable(retention): @@ -259,8 +254,9 @@ def compression_function(path_in): path_out = "{}.{}".format(path_in, ext) if os.path.isfile(path_out): root, ext_before = os.path.splitext(path_in) - renamed_template = "{}.{:YYYY-MM-DD_HH-mm-ss_SSSSSS}{}.{}" - renamed_path = renamed_template.format(root, pendulum_now(), ext_before, ext) + renamed_template = "{}.{}{}.{}" + date = FileDateFormatter() + renamed_path = renamed_template.format(root, date, ext_before, ext) os.rename(path_out, renamed_path) compress(path_in, path_out) os.remove(path_in) diff --git a/loguru/_handler.py b/loguru/_handler.py index 77f1c9a19..64bc73998 100644 --- a/loguru/_handler.py +++ b/loguru/_handler.py @@ -66,7 +66,7 @@ def serialize_record(text, record): 'name': record['name'], 'process': dict(id=record['process'].id, name=record['process'].name), 'thread': dict(id=record['thread'].id, name=record['thread'].name), - 'time': dict(repr=record['time'], timestamp=record['time'].float_timestamp), + 'time': dict(repr=record['time'], timestamp=record['time'].timestamp()), } } diff --git a/loguru/_logger.py b/loguru/_logger.py index a6b3b5fa0..c15307bbe 100644 --- a/loguru/_logger.py +++ b/loguru/_logger.py @@ -4,17 +4,17 @@ import os import threading from collections import namedtuple +from datetime import timedelta from inspect import isclass from multiprocessing import current_process from os import PathLike from os.path import basename, normcase, splitext from threading import current_thread -import pendulum -from pendulum import now as pendulum_now from colorama import AnsiToWin32 from . import _defaults +from ._datetime import now from ._file_sink import FileSink from ._get_frame import get_frame from ._handler import Handler @@ -22,7 +22,7 @@ Level = namedtuple('Level', ['no', 'color', 'icon']) -start_time = pendulum_now() +start_time = now() class Logger: @@ -384,7 +384,7 @@ def log_function(_self, _message, *args, **kwargs): return _self._enabled[name] = True - now = pendulum_now() + current_datetime = now() if level_id is None: level_no, level_color, level_icon = level, '', ' ' @@ -402,8 +402,8 @@ def log_function(_self, _message, *args, **kwargs): file_name = basename(file_path) thread = current_thread() process = current_process() - diff = now - start_time - elapsed = pendulum.Duration(microseconds=diff.microseconds) + diff = current_datetime - start_time + elapsed = timedelta(microseconds=diff.microseconds) level_recattr = LevelRecattr(level_name) level_recattr.no, level_recattr.name, level_recattr.icon = level_no, level_name, level_icon @@ -435,7 +435,7 @@ def log_function(_self, _message, *args, **kwargs): 'name': name, 'process': process_recattr, 'thread': thread_recattr, - 'time': now, + 'time': current_datetime, } if _self._lazy: diff --git a/loguru/_string_parsers.py b/loguru/_string_parsers.py index 9a5b79b47..c4536738a 100644 --- a/loguru/_string_parsers.py +++ b/loguru/_string_parsers.py @@ -1,20 +1,23 @@ import datetime import re -import pendulum - def parse_size(size): size = size.strip() - reg = r'([e\+\-\.\d]+)\s*([kmgtpezy])?(i)?(b)' - match = re.fullmatch(reg, size, flags=re.I) + reg = re.compile(r'([e\+\-\.\d]+)\s*([kmgtpezy])?(i)?(b)', flags=re.I) + + match = reg.fullmatch(size) + if not match: return None + s, u, i, b = match.groups() + try: s = float(s) except ValueError: raise ValueError("Invalid float value while parsing size: '%s'" % s) + u = 'kmgtpezy'.index(u.lower()) + 1 if u else 0 i = 1024 if i else 1000 b = {'b': 8, 'B': 1}[b] if b else 1 @@ -25,6 +28,7 @@ def parse_size(size): def parse_duration(duration): duration = duration.strip() + reg = r'(?:([e\+\-\.\d]+)\s*([a-z]+)[\s\,]*)' units = [ ('y|years?', 31536000), @@ -38,7 +42,6 @@ def parse_duration(duration): ('us|microseconds?', 0.000001), ] - reg = r'(?:([e\+\-\.\d]+)\s*([a-z]+)[\s\,]*)' if not re.fullmatch(reg + '+', duration, flags=re.I): return None @@ -57,7 +60,7 @@ def parse_duration(duration): seconds += value * unit - return pendulum.Duration(seconds=seconds) + return datetime.timedelta(seconds=seconds) def parse_frequency(frequency): @@ -65,68 +68,107 @@ def parse_frequency(frequency): if frequency == 'hourly': def hourly(t): - return t.add(hours=1).start_of('hour') + dt = t + datetime.timedelta(hours=1) + return dt.replace(minute=0, second=0, microsecond=0) return hourly elif frequency == 'daily': def daily(t): - return t.add(days=1).start_of('day') + dt = t + datetime.timedelta(days=1) + return dt.replace(hour=0, minute=0, second=0, microsecond=0) return daily elif frequency == 'weekly': def weekly(t): - return t.add(weeks=1).start_of('week') + dt = t + datetime.timedelta(days=7 - t.weekday()) + return dt.replace(hour=0, minute=0, second=0, microsecond=0) return weekly elif frequency == 'monthly': def monthly(t): - return t.add(months=1).start_of('month') + if t.month == 12: + y, m = t.year + 1, 1 + else: + y, m = t.year, t.month + 1 + return t.replace(year=y, month=m, day=1, hour=0, minute=0, second=0, microsecond=0) return monthly elif frequency == 'yearly': def yearly(t): - return t.add(years=1).start_of('year') + y = t.year + 1 + return t.replace(year=y, month=1, day=1, hour=0, minute=0, second=0, microsecond=0) return yearly return None -def parse_daytime(daytime): - daytime = daytime.strip() - - daytime_reg = re.compile(r'^(.*?)\s+at\s+(.*)$', flags=re.I) - day_reg = re.compile(r'^w\d+$', flags=re.I) - time_reg = re.compile(r'^[\d\.\:\,]+(?:\s*[ap]m)?$', flags=re.I) - +def parse_day(day): + day = day.strip().lower() days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] - pdays = [getattr(pendulum, day.upper()) for day in days] - - daytime_match = daytime_reg.match(daytime) - if daytime_match: - day, time = daytime_match.groups() - elif time_reg.match(daytime): - day, time = None, daytime - elif day_reg.match(daytime) or daytime.lower() in days: - day, time = daytime, None + reg = re.compile(r'^w\d+$', flags=re.I) + + if day in days: + day = days.index(day) + elif reg.match(day): + day = int(day[1:]) + if not 0 <= day < 7: + raise ValueError("Invalid weekday value while parsing day (expected [0-6]): '%d'" % day) else: + day = None + + return day + + +def parse_time(time): + time = time.strip() + reg = re.compile(r'^[\d\.\:]+\s*(?:[ap]m)?$', flags=re.I) + + if not reg.match(time): return None - if day is not None: - day_ = day.lower() - if day_reg.match(day): - d = int(day[1:]) - if not 0 <= d < len(days): - raise ValueError("Invalid weekday index while parsing daytime: '%d'" % d) - day = pdays[d] - elif day_ in days: - day = pdays[days.index(day_)] - else: - raise ValueError("Invalid weekday value while parsing daytime: '%s'" % day) + formats = [ + "%H", + "%H:%M", + "%H:%M:%S", + "%H:%M:%S.%f", + "%I %p", + "%I:%M %S", + "%I:%M:%S %p", + "%I:%M:%S.%f %p" + ] - if time is not None: - time_ = time + for format_ in formats: try: - time = pendulum.parse(time, exact=True, strict=False) - except Exception as e: - raise ValueError("Invalid time while parsing daytime: '%s'" % time) from e + dt = datetime.datetime.strptime(time, format_) + except ValueError: + pass else: - if not isinstance(time, datetime.time): - raise ValueError("Cannot strictly parse time from: '%s'" % time_) + return dt.time() + + raise ValueError("Unrecognized format while parsing time: '%s'" % time) + + +def parse_daytime(daytime): + daytime = daytime.strip() + reg = re.compile(r'^(.*?)\s+at\s+(.*)$', flags=re.I) + + match = reg.match(daytime) + if match: + day, time = match.groups() + else: + day = time = daytime + + try: + day = parse_day(day) + if match and day is None: + raise ValueError + except ValueError as e: + raise ValueError("Invalid day while parsing daytime: '%s'" % day) from e + + try: + time = parse_time(time) + if match and time is None: + raise ValueError + except ValueError as e: + raise ValueError("Invalid time while parsing daytime: '%s'" % time) from e + + if day is None and time is None: + return None return day, time diff --git a/setup.py b/setup.py index 2d234696d..fdf72a33b 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,6 @@ 'better_exceptions_fork>=0.2.1.post5', 'colorama>=0.3.9', 'notifiers>=0.7.4', - 'pendulum>=2.0.1', ], extras_require={ 'dev': [ diff --git a/tests/conftest.py b/tests/conftest.py index ac55acfc2..7c54cbaf9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,9 @@ import py import os import subprocess +import datetime +import time +import calendar default_levels = loguru._logger.Logger._levels.copy() @@ -66,13 +69,24 @@ def pyexec(code, import_loguru=False, *, pyfile=None): return pyexec @pytest.fixture -def monkeypatch_now(monkeypatch): +def monkeypatch_date(monkeypatch): - def monkeypatch_now(func): - monkeypatch.setattr(loguru._logger, 'pendulum_now', func) - monkeypatch.setattr(loguru._file_sink, 'pendulum_now', func) + def monkeypatch_date(year, month, day, hour, minute, second, microsecond, zone="UTC", offset=0): + tzinfo = datetime.timezone(datetime.timedelta(seconds=offset), zone) + dt = datetime.datetime(year, month, day, hour, minute, second, microsecond, tzinfo=tzinfo) + struct = time.struct_time([*dt.timetuple()] + [zone, offset]) + secs = dt.timestamp() - return monkeypatch_now + def patched_time(): + return secs + + def patched_localtime(t): + return struct + + monkeypatch.setattr(loguru._datetime, 'time', patched_time) + monkeypatch.setattr(loguru._datetime, 'localtime', patched_localtime) + + return monkeypatch_date @pytest.fixture def make_logging_logger(): diff --git a/tests/test_datetime.py b/tests/test_datetime.py new file mode 100644 index 000000000..eb3b11036 --- /dev/null +++ b/tests/test_datetime.py @@ -0,0 +1,42 @@ +import pytest +from loguru import logger + +@pytest.fixture(params=["log", "file"]) +def format_tester(request, writer, tmpdir): + + mode = request.param + + def test_log(fmt): + logger.start(writer, format=fmt) + logger.debug("X") + result = writer.read().rstrip("\n") + return result + + def test_file(fmt): + logger.start(tmpdir.join(fmt)) + logger.debug("X") + files = tmpdir.listdir() + assert len(files) == 1 + return files[0].basename + + def format_tester(fmt): + tests = dict(log=test_log, file=test_file) + return tests[mode]("{time:%s}" % fmt) + + return format_tester + +@pytest.mark.parametrize('time_format, date, expected', [ + ('%Y-%m-%d %H-%M-%S %f %Z', (2018, 6, 9, 1, 2, 3, 45, 'UTC', 0), "2018-06-09 01-02-03 000045 UTC"), + ('YYYY-MM-DD HH-mm-ss SSSSSS zz', (2018, 6, 9, 1, 2, 3, 45, 'UTC', 0), "2018-06-09 01-02-03 000045 UTC"), + ('YY-M-D H-m-s SSS Z', (2005, 4, 7, 9, 3, 8, 2320, 'A', 3600), "05-4-7 9-3-8 002 +01:00"), + ('Q_DDDD_DDD d_E h_hh A SS ZZ', (2000, 1, 1, 14, 0, 0, 900000, 'B', -1800), "1_001_1 5_6 2_02 PM 90 -0030"), + ('hh A', (2018, 1, 1, 0, 1, 2, 3), "12 AM"), + ('hh A', (2018, 1, 1, 12, 0, 0, 0), "12 PM"), + ('hh A', (2018, 1, 1, 23, 0, 0, 0), "11 PM"), + ('[YYYY] MM [DD]', (2018, 2, 3, 11, 9, 0, 2), "YYYY 02 DD"), + ('[YYYY MM DD]', (2018, 1, 3, 11, 3, 4, 2), "[2018 01 03]"), +]) +def test_formatting(format_tester, monkeypatch_date, time_format, date, expected): + monkeypatch_date(*date) + assert format_tester(time_format) == expected + diff --git a/tests/test_filesink_compression.py b/tests/test_filesink_compression.py index 364462ca7..da4795945 100644 --- a/tests/test_filesink_compression.py +++ b/tests/test_filesink_compression.py @@ -1,7 +1,6 @@ import pytest import os from loguru import logger -import pendulum @pytest.mark.parametrize('compression', [ 'gz', 'bz2', 'zip', 'xz', 'lzma', @@ -64,8 +63,8 @@ def test_no_compression_at_stop_with_rotation(tmpdir, mode): assert len(tmpdir.listdir()) == 1 assert tmpdir.join("test.log").check(exists=1) -def test_rename_existing_before_compression(monkeypatch_now, tmpdir): - monkeypatch_now(lambda *a, **k: pendulum.parse("2018-01-01 00:00:00.000000")) +def test_rename_existing_before_compression(monkeypatch_date, tmpdir): + monkeypatch_date(2018, 1, 1, 0, 0, 0, 0) i = logger.start(tmpdir.join('test.log'), compression="tar.gz") logger.debug("test") logger.stop(i) diff --git a/tests/test_filesink_retention.py b/tests/test_filesink_retention.py index 0397a9a88..f3f43458f 100644 --- a/tests/test_filesink_retention.py +++ b/tests/test_filesink_retention.py @@ -1,19 +1,19 @@ import pytest -import pendulum import datetime import os from loguru import logger -@pytest.mark.parametrize('retention', ['1 hour', '1H', ' 1 h ', datetime.timedelta(hours=1), pendulum.Duration(hours=1.0)]) -def test_retention_time(monkeypatch_now, tmpdir, retention): +@pytest.mark.parametrize('retention', ['1 hour', '1H', ' 1 h ', datetime.timedelta(hours=1)]) +def test_retention_time(monkeypatch_date, tmpdir, retention): i = logger.start(tmpdir.join('test.log.x'), retention=retention) logger.debug("test") logger.stop(i) assert len(tmpdir.listdir()) == 1 - monkeypatch_now(lambda *a, **k: pendulum.now().add(hours=24)) + future = datetime.datetime.now() + datetime.timedelta(days=1) + monkeypatch_date(future.year, future.month, future.day, future.hour, future.minute, future.second, future.microsecond) i = logger.start(tmpdir.join('test.log'), retention=retention) logger.debug("test") @@ -92,8 +92,8 @@ def test_not_managed_files(tmpdir): assert len(tmpdir.listdir()) == len(others) -def test_manage_formatted_files(monkeypatch_now, tmpdir): - monkeypatch_now(lambda *a, **k: pendulum.parse("2018-01-01 00:00:00")) +def test_manage_formatted_files(monkeypatch_date, tmpdir): + monkeypatch_date(2018, 1, 1, 0, 0, 0, 0) f1 = tmpdir.join('temp/2018/file.log') f2 = tmpdir.join('temp/file2018.log') diff --git a/tests/test_filesink_rotation.py b/tests/test_filesink_rotation.py index 77bc5f470..fdb526a64 100644 --- a/tests/test_filesink_rotation.py +++ b/tests/test_filesink_rotation.py @@ -1,40 +1,39 @@ import pytest -import pendulum import datetime import os from loguru import logger -def test_renaming(monkeypatch_now, tmpdir): +def test_renaming(monkeypatch_date, tmpdir): i = logger.start(tmpdir.join("file.log"), rotation=0, format="{message}") - monkeypatch_now(lambda *a, **k: pendulum.parse("2018-01-01 00:00:00.000000")) + monkeypatch_date(2018, 1, 1, 0, 0, 0, 0) logger.debug("a") assert tmpdir.join("file.log").read() == "a\n" assert tmpdir.join("file.2018-01-01_00-00-00_000000.log").read() == "" - monkeypatch_now(lambda *a, **k: pendulum.parse("2019-01-01 00:00:00.000000")) + monkeypatch_date(2019, 1, 1, 0, 0, 0, 0) logger.debug("b") assert tmpdir.join("file.log").read() == "b\n" assert tmpdir.join("file.2019-01-01_00-00-00_000000.log").read() == "a\n" assert tmpdir.join("file.2018-01-01_00-00-00_000000.log").read() == "" -def test_no_renaming(monkeypatch_now, tmpdir): - monkeypatch_now(lambda *a, **k: pendulum.parse("2018-01-01 00:00:00.000000")) +def test_no_renaming(monkeypatch_date, tmpdir): + monkeypatch_date(2018, 1, 1, 0, 0, 0, 0) i = logger.start(tmpdir.join("file_{time}.log"), rotation=0, format="{message}") - monkeypatch_now(lambda *a, **k: pendulum.parse("2019-01-01 00:00:00.000000")) + monkeypatch_date(2019, 1, 1, 0, 0, 0, 0) logger.debug("a") assert tmpdir.join("file_2018-01-01_00-00-00_000000.log").read() == "" assert tmpdir.join("file_2019-01-01_00-00-00_000000.log").read() == "a\n" - monkeypatch_now(lambda *a, **k: pendulum.parse("2020-01-01 00:00:00.000000")) + monkeypatch_date(2020, 1, 1, 0, 0, 0, 0) logger.debug("b") assert tmpdir.join("file_2018-01-01_00-00-00_000000.log").read() == "" assert tmpdir.join("file_2019-01-01_00-00-00_000000.log").read() == "a\n" assert tmpdir.join("file_2020-01-01_00-00-00_000000.log").read() == "b\n" -def test_delayed(monkeypatch_now, tmpdir): - monkeypatch_now(lambda *a, **k: pendulum.parse("2018-01-01 00:00:00.000000")) +def test_delayed(monkeypatch_date, tmpdir): + monkeypatch_date(2018, 1, 1, 0, 0, 0, 0) i = logger.start(tmpdir.join('file.log'), rotation=0, delay=True, format="{message}") logger.debug('a') logger.stop(i) @@ -43,8 +42,8 @@ def test_delayed(monkeypatch_now, tmpdir): assert tmpdir.join("file.log").read() == "a\n" assert tmpdir.join("file.2018-01-01_00-00-00_000000.log").read() == "" -def test_delayed_early_stop(monkeypatch_now, tmpdir): - monkeypatch_now(lambda *a, **k: pendulum.parse("2018-01-01 00:00:00.000000")) +def test_delayed_early_stop(monkeypatch_date, tmpdir): + monkeypatch_date(2018, 1, 1, 0, 0, 0, 0) i = logger.start(tmpdir.join('file.log'), rotation=0, delay=True, format="{message}") logger.stop(i) @@ -53,48 +52,49 @@ def test_delayed_early_stop(monkeypatch_now, tmpdir): @pytest.mark.parametrize('size', [ 8, 8.0, 7.99, "8 B", "8e-6MB", "0.008 kiB", "64b" ]) -def test_size_rotation(monkeypatch_now, tmpdir, size): - date = pendulum.parse("2018-01-01 00:00:00.000000") - - def patch(*a, **k): - nonlocal date - date = date.add(seconds=1) - return date - - monkeypatch_now(patch) +def test_size_rotation(monkeypatch_date, tmpdir, size): + monkeypatch_date(2018, 1, 1, 0, 0, 0, 0) file = tmpdir.join("test_{time}.log") i = logger.start(file.realpath(), format='{message}', rotation=size, mode='w') + monkeypatch_date(2018, 1, 1, 0, 0, 1, 0) logger.debug("abcde") + + monkeypatch_date(2018, 1, 1, 0, 0, 2, 0) logger.debug("fghij") + + monkeypatch_date(2018, 1, 1, 0, 0, 3, 0) logger.debug("klmno") + monkeypatch_date(2018, 1, 1, 0, 0, 4, 0) logger.stop(i) - assert [f.read() for f in sorted(tmpdir.listdir())] == ['abcde\n', 'fghij\n', 'klmno\n'] + assert len(tmpdir.listdir()) == 3 + assert tmpdir.join("test_2018-01-01_00-00-00_000000.log").read() == 'abcde\n' + assert tmpdir.join("test_2018-01-01_00-00-02_000000.log").read() == 'fghij\n' + assert tmpdir.join("test_2018-01-01_00-00-03_000000.log").read() == 'klmno\n' -# TODO: Decomment test cases once pendulum/#211 is fixed @pytest.mark.parametrize('when, hours', [ # hours = [Should not trigger, should trigger, should not trigger, should trigger, should trigger] - #('13', [0, 1, 20, 4, 24]), - #('13:00', [0.2, 0.9, 23, 1, 48]), + ('13', [0, 1, 20, 4, 24]), + ('13:00', [0.2, 0.9, 23, 1, 48]), ('13:00:00', [0.5, 1.5, 10, 15, 72]), ('13:00:00.123456', [0.9, 2, 10, 15, 256]), - #('11:00', [22.9, 0.2, 23, 1, 24]), + ('11:00', [22.9, 0.2, 23, 1, 24]), ('w0', [11, 1, 24 * 7 - 1, 1, 24 * 7]), - #('W0 at 00:00', [10, 24 * 7 - 5, 0.1, 24 * 30, 24 * 14]), + ('W0 at 00:00', [10, 24 * 7 - 5, 0.1, 24 * 30, 24 * 14]), ('W6', [24, 24 * 28, 24 * 5, 24, 364 * 24]), ('saturday', [25, 25 * 12, 0, 25 * 12, 24 * 8]), - #('w6 at 00', [8, 24 * 7, 24 * 6, 24, 24 * 8]), - #(' W6 at 13 ', [0.5, 1, 24 * 6, 24 * 6, 365 * 24]), - ('w2 at 11:00:00', [48 + 22, 3, 24 * 6, 24, 366 * 24]), + ('w6 at 00', [8, 24 * 7, 24 * 6, 24, 24 * 8]), + (' W6 at 13 ', [0.5, 1, 24 * 6, 24 * 6, 365 * 24]), + ('w2 at 11:00:00 AM', [48 + 22, 3, 24 * 6, 24, 366 * 24]), ('MoNdAy at 11:00:30.123', [22, 24, 24, 24 * 7, 24 * 7]), ('sunday', [0.1, 24 * 7 - 10, 24, 24 * 6, 24 * 7]), - #('SUNDAY at 11:00', [1, 24 * 7, 2, 24*7, 30*12]), - ('sunDAY at 13:00:00', [0.9, 0.2, 24 * 7 - 2, 3, 24 * 8]), + ('SUNDAY at 11:00', [1, 24 * 7, 2, 24*7, 30*12]), + ('sunDAY at 1:0:0.0 pm', [0.9, 0.2, 24 * 7 - 2, 3, 24 * 8]), (datetime.time(15), [2, 3, 19, 5, 24]), - (pendulum.Time(18, 30, 11, 123), [1, 5.51, 20, 24, 40]), + (datetime.time(18, 30, 11, 123), [1, 5.51, 20, 24, 40]), ("2 h", [1, 2, 0.9, 0.5, 10]), ("1 hour", [0.5, 1, 0.1, 100, 1000]), ("7 days", [24 * 7 - 1, 1, 48, 24 * 10, 24 * 365]), @@ -103,41 +103,61 @@ def patch(*a, **k): ("1.5d", [30, 10, 0.9, 48, 35]), ("1.222 hours, 3.44s", [1.222, 0.1, 1, 1.2, 2]), (datetime.timedelta(hours=1), [0.9, 0.2, 0.7, 0.5, 3]), - (pendulum.Duration(minutes=30), [0.48, 0.04, 0.07, 0.44, 0.5]), + (datetime.timedelta(minutes=30), [0.48, 0.04, 0.07, 0.44, 0.5]), ('hourly', [0.9, 0.2, 0.8, 3, 1]), ('daily', [11, 1, 23, 1, 24]), ('WEEKLY', [11, 2, 24 * 6, 24, 24 * 7]), - (' mOnthLY', [0, 24 * 13, 29 * 24, 60 * 24, 24 * 35]), - ('yearly ', [100, 24 * 7 * 30, 24 * 300, 24 * 100, 24 * 400]), + ('mOnthLY', [0, 24 * 13, 29 * 24, 60 * 24, 24 * 35]), + ('monthly', [10 * 24, 30 * 24 * 6, 24, 24 * 7, 24 * 31]), + ('Yearly ', [100, 24 * 7 * 30, 24 * 300, 24 * 100, 24 * 400]), ]) -def test_time_rotation(monkeypatch_now, tmpdir, when, hours): - now = pendulum.parse("2017-06-18 12:00:00") # Sunday +def test_time_rotation(monkeypatch_date, tmpdir, when, hours): + now = datetime.datetime(2017, 6, 18, 12, 0, 0) # Sunday - monkeypatch_now(lambda *a, **k: now) + monkeypatch_date(now.year, now.month, now.day, now.hour, now.minute, now.second, now.microsecond) i = logger.start(tmpdir.join('test_{time}.log').realpath(), format='{message}', rotation=when, mode='w') + from loguru._datetime import now as nownow + for h, m in zip(hours, ['a', 'b', 'c', 'd', 'e']): - now = now.add(hours=h) + now += datetime.timedelta(hours=h) + monkeypatch_date(now.year, now.month, now.day, now.hour, now.minute, now.second, now.microsecond) logger.debug(m) logger.stop(i) - assert len(tmpdir.listdir()) == 4 assert [f.read() for f in sorted(tmpdir.listdir())] == ['a\n', 'b\nc\n', 'd\n', 'e\n'] -def test_function_rotation(monkeypatch_now, tmpdir): - monkeypatch_now(lambda *a, **k: pendulum.parse("2018-01-01 00:00:00.000000")) +def test_time_rotation_dst(monkeypatch_date, tmpdir): + monkeypatch_date(2018, 10, 27, 5, 0, 0, 0, "CET", 3600) + i = logger.start(tmpdir.join("test_{time}.log").realpath(), format='{message}', rotation="1 day") + logger.debug("First") + + monkeypatch_date(2018, 10, 28, 5, 30, 0, 0, "CEST", 7200) + logger.debug("Second") + + monkeypatch_date(2018, 10, 29, 6, 0, 0, 0, "CET", 3600) + logger.debug("Third") + logger.stop(i) + + assert len(tmpdir.listdir()) == 3 + assert tmpdir.join("test_2018-10-27_05-00-00_000000.log").read() == "First\n" + assert tmpdir.join("test_2018-10-28_05-30-00_000000.log").read() == "Second\n" + assert tmpdir.join("test_2018-10-29_06-00-00_000000.log").read() == "Third\n" + +def test_function_rotation(monkeypatch_date, tmpdir): + monkeypatch_date(2018, 1, 1, 0, 0, 0, 0) x = iter([False, True, False]) i = logger.start(tmpdir.join("test_{time}.log"), rotation=lambda *_: next(x), format="{message}") logger.debug("a") assert tmpdir.join("test_2018-01-01_00-00-00_000000.log").read() == "a\n" - monkeypatch_now(lambda *a, **k: pendulum.parse("2019-01-01 00:00:00.000000")) + monkeypatch_date(2019, 1, 1, 0, 0, 0, 0) logger.debug("b") assert tmpdir.join("test_2019-01-01_00-00-00_000000.log").read()== "b\n" - monkeypatch_now(lambda *a, **k: pendulum.parse("2020-01-01 00:00:00.000000")) + monkeypatch_date(2020, 1, 1, 0, 0, 0, 0) logger.debug("c") assert len(tmpdir.listdir()) == 2 @@ -145,9 +165,8 @@ def test_function_rotation(monkeypatch_now, tmpdir): assert tmpdir.join("test_2019-01-01_00-00-00_000000.log").read()== "b\nc\n" @pytest.mark.parametrize('mode', ['w', 'x']) -def test_rotation_at_stop(monkeypatch_now, tmpdir, mode): - monkeypatch_now(lambda *a, **k: pendulum.parse("2018-01-01 00:00:00")) - +def test_rotation_at_stop(monkeypatch_date, tmpdir, mode): + monkeypatch_date(2018, 1, 1, 0, 0, 0, 0) i = logger.start(tmpdir.join("test_{time:YYYY}.log"), rotation="10 MB", mode=mode, format="{message}") logger.debug("test") logger.stop(i) @@ -170,7 +189,8 @@ def test_no_rotation_at_stop(tmpdir, mode): "K", "tufy MB", "111.111.111 kb", "3 Ki", "2017.11.12", "11:99", "monday at 2017", "e days", "2 days 8 pouooi", "foobar", - object(), os, pendulum.Date(2017, 11, 11), pendulum.now(), 1j, + "w5 at [not|a|time]", "[not|a|day] at 12:00", + object(), os, datetime.date(2017, 11, 11), datetime.datetime.now(), 1j, ]) def test_invalid_rotation(rotation): with pytest.raises(ValueError): diff --git a/tests/test_formatting.py b/tests/test_formatting.py index b1368e554..64cc38e6d 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -5,11 +5,8 @@ @pytest.mark.parametrize('format, validator', [ ('{name}', lambda r: r == 'tests.test_formatting'), - ('{time}', lambda r: re.fullmatch(r'\d+-\d+-\d+T\d+:\d+:\d+[.,]\d+[+-]\d+:\d+', r)), - ('{time:HH[h] mm[m] ss[s]}', lambda r: re.fullmatch(r'\d+h \d+m \d+s', r)), - ('{time:HH}x{time:mm}x{time:ss}', lambda r: re.fullmatch(r'\d+x\d+x\d+', r)), - ('{time.int_timestamp}', lambda r: re.fullmatch(r'\d+', r)), - ('{elapsed}', lambda r: re.fullmatch(r'(\s*([\d.]+) ([a-zA-Z]+)\s*)+', r)), + ('{time}', lambda r: re.fullmatch(r'\d+-\d+-\d+T\d+:\d+:\d+[.,]\d+[+-]\d{4}', r)), + ('{elapsed}', lambda r: re.fullmatch(r'\d:\d{2}:\d{2}\.\d{6}', r)), ('{elapsed.seconds}', lambda r: re.fullmatch(r'\d+', r)), ('{line}', lambda r: re.fullmatch(r'\d+', r)), ('{level}', lambda r: r == 'DEBUG'), @@ -46,8 +43,6 @@ def test_log_formatters(format, validator, writer, use_log_function): @pytest.mark.parametrize('format, validator', [ ('{time}.log', lambda r: re.fullmatch(r'\d+-\d+-\d+_\d+-\d+-\d+\_\d+.log', r)), - ('{time:HH[h] mm[m] ss[s]}.log', lambda r: re.fullmatch(r'\d+h \d+m \d+s\.log', r)), - ('{time:HH}x{time:mm}x{time:ss}.log', lambda r: re.fullmatch(r'\d+x\d+x\d+\.log', r)), ('%s_{{a}}_天_{{1}}_%d', lambda r: r == '%s_{a}_天_{1}_%d'), ]) @pytest.mark.parametrize('part', ["file", "dir", "both"]) diff --git a/tests/test_interception.py b/tests/test_interception.py index 2e5d315fb..21e211298 100644 --- a/tests/test_interception.py +++ b/tests/test_interception.py @@ -1,7 +1,7 @@ import sys import logging import pytest -import pendulum + from loguru import logger class InterceptHandler(logging.Handler): diff --git a/tests/test_parser.py b/tests/test_parser.py index 499343992..b2ad1f6fb 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -3,8 +3,7 @@ import re import pathlib import io - -import pendulum +from datetime import datetime from loguru import parser @@ -76,8 +75,8 @@ def test_greedy_pattern(): def test_cast(): log = dict(num="123", val="1.1", date="2017-03-29 11:11:11") - result = parser.cast(log, num=int, val=float, date=pendulum.parse) - assert result == dict(num=123, val=1.1, date=pendulum.parse("2017-03-29 11:11:11")) + result = parser.cast(log, num=int, val=float, date=lambda d: datetime.strptime(d, '%Y-%m-%d %H:%M:%S')) + assert result == dict(num=123, val=1.1, date=datetime(2017, 3, 29, 11, 11, 11)) def test_cast_with_irrelevant_arg(): log = dict(a="1")