Skip to content

Commit

Permalink
Add a way to convert datetime to UTC before formatting (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
Delgan committed Oct 26, 2019
1 parent 505bfdf commit 78b93b6
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 16 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

- Fix incompatibility with ``multiprocessing`` on Windows by entirely refactoring the internal structure of the ``logger`` so it can be inherited by child processes along with added handlers (`#108 <https://github.com/Delgan/loguru/issues/108>`_).
- Fix an error using a ``filter`` function "by name" while receiving a log with ``record["name"]`` equals to ``None``.
- Fix ``AttributeError`` while using a file sink on some distributions (like Alpine Linux) missing the ``os.getxattr`` and ``os.setxattr`` functions (`#34 <https://github.com/Delgan/loguru/pull/158>`_, thanks `@joshgordon <https://github.com/joshgordon>`_).
- Fix ``AttributeError`` while using a file sink on some distributions (like Alpine Linux) missing the ``os.getxattr`` and ``os.setxattr`` functions (`#158 <https://github.com/Delgan/loguru/pull/158>`_, thanks `@joshgordon <https://github.com/joshgordon>`_).
- Add support for ``copy.deepcopy()`` of the ``logger`` allowing multiple independent loggers with separate set of handlers (`#72 <https://github.com/Delgan/loguru/issues/72>`_).
- Prevent hypothetical ``ImportError`` if a Python installation is missing the built-in ``distutils`` module (`#118 <https://github.com/Delgan/loguru/issues/118>`_).
- Add the possibility to convert ``datetime`` to UTC before formatting (in logs and filenames) by adding ``"!UTC"`` at the end of the time format specifier (`#128 <https://github.com/Delgan/loguru/issues/128>`_).
- Add the level ``name`` as the first argument of namedtuple returned by the ``.level()`` method.


Expand Down
23 changes: 15 additions & 8 deletions loguru/_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,31 @@
from datetime import timedelta, timezone
from time import localtime, strftime


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))
pattern = re.compile(r"(?:{0})|\[(?:{0}|!UTC)\]".format(tokens))


class datetime(datetime_):
def __format__(self, spec):
if spec.endswith("!UTC"):
dt = self.astimezone(timezone.utc)
spec = spec[:-4]
else:
dt = self

if not spec:
spec = "%Y-%m-%dT%H:%M:%S.%f%z"

if "%" in spec:
return super().__format__(spec)
return datetime_.__format__(dt, 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()
year, month, day, hour, minute, second, weekday, yearday, _ = dt.timetuple()
microsecond = dt.microsecond
timestamp = dt.timestamp()
tzinfo = dt.tzinfo or timezone(timedelta(seconds=0))
offset = tzinfo.utcoffset(dt).total_seconds()
sign = ("-", "+")[offset >= 0]
h, m = divmod(abs(offset // 60), 60)

Expand Down Expand Up @@ -58,7 +65,7 @@ def __format__(self, spec):
"A": ("AM", "PM")[hour // 12],
"Z": "%s%02d:%02d" % (sign, h, m),
"ZZ": "%s%02d%02d" % (sign, h, m),
"zz": tzinfo.tzname(self) or "",
"zz": tzinfo.tzname(dt) or "",
"X": "%d" % timestamp,
"x": "%d" % (int(timestamp) * 1000000 + microsecond),
}
Expand Down
4 changes: 4 additions & 0 deletions loguru/_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ def add(
of the one used by the `Pendulum`_ library of `@sdispater`_. To escape a token, just add
square brackets around it, for example ``"[YY]"`` would display literally ``"YY"``.
If you prefer to display UTC rather than local time, you can add ``"!UTC"`` at the very end
of the time format, like ``{time:HH:mm:ss!UTC}``. Doing so will convert the ``datetime``
to UTC before formatting.
If no time formatter specifier is used, like for example if ``format="{time} {message}"``,
the default one will use ISO 8601.
Expand Down
66 changes: 59 additions & 7 deletions tests/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,54 @@
import sys
import re

if sys.version_info < (3, 6):
UTC_NAME = "UTC+00:00"
else:
UTC_NAME = "UTC"


@pytest.mark.parametrize(
"time_format, date, expected",
[
(
"%Y-%m-%d %H-%M-%S %f %Z",
"%Y-%m-%d %H-%M-%S %f %Z %z",
(2018, 6, 9, 1, 2, 3, 45, "UTC", 0),
"2018-06-09 01-02-03 000045 UTC +0000",
),
(
"YYYY-MM-DD HH-mm-ss SSSSSS zz ZZ",
(2018, 6, 9, 1, 2, 3, 45, "UTC", 0),
"2018-06-09 01-02-03 000045 UTC +0000",
),
(
"%Y-%m-%d %H-%M-%S %f %Z %z",
(2018, 6, 9, 1, 2, 3, 45, "EST", -18000),
"2018-06-09 01-02-03 000045 EST -0500",
),
(
"YYYY-MM-DD HH-mm-ss SSSSSS zz ZZ",
(2018, 6, 9, 1, 2, 3, 45, "EST", -18000),
"2018-06-09 01-02-03 000045 EST -0500",
),
(
"%Y-%m-%d %H-%M-%S %f %Z!UTC",
(2018, 6, 9, 1, 2, 3, 45, "UTC", 0),
"2018-06-09 01-02-03 000045 UTC",
"2018-06-09 01-02-03 000045 %s" % UTC_NAME,
),
(
"YYYY-MM-DD HH-mm-ss SSSSSS zz",
"YYYY-MM-DD HH-mm-ss SSSSSS zz!UTC",
(2018, 6, 9, 1, 2, 3, 45, "UTC", 0),
"2018-06-09 01-02-03 000045 UTC",
"2018-06-09 01-02-03 000045 %s" % UTC_NAME,
),
(
"%Y-%m-%d %H-%M-%S %f %Z %z!UTC",
(2018, 6, 9, 1, 2, 3, 45, "EST", -18000),
"2018-06-09 06-02-03 000045 %s +0000" % UTC_NAME,
),
(
"YYYY-MM-DD HH-mm-ss SSSSSS zz ZZ!UTC",
(2018, 6, 9, 1, 2, 3, 45, "UTC", -18000),
"2018-06-09 06-02-03 000045 %s +0000" % UTC_NAME,
),
("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"),
(
Expand All @@ -31,6 +67,25 @@
("[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]"),
("[[YY]]", (2018, 1, 3, 11, 3, 4, 2), "[YY]"),
("[]", (2018, 1, 3, 11, 3, 4, 2), "[]"),
# ("[HHmmss", (2018, 1, 3, 11, 3, 4, 2), "[110304"), # Fail PyPy
("HHmmss]", (2018, 1, 3, 11, 3, 4, 2), "110304]"),
("HH:mm:ss!UTC", (2018, 1, 1, 11, 30, 0, 0, "A", 7200), "09:30:00"),
("UTC! HH:mm:ss", (2018, 1, 1, 11, 30, 0, 0, "A", 7200), "UTC! 11:30:00"),
("!UTC HH:mm:ss", (2018, 1, 1, 11, 30, 0, 0, "A", 7200), "!UTC 11:30:00"),
(
"hh:mm:ss A - Z ZZ !UTC",
(2018, 1, 1, 12, 30, 0, 0, "A", 5400),
"11:00:00 AM - +00:00 +0000 ",
),
(
"YYYY-MM-DD HH:mm:ss[Z]!UTC",
(2018, 1, 3, 11, 3, 4, 2, "XYZ", -7200),
"2018-01-03 13:03:04Z",
),
("HH:mm:ss[!UTC]", (2018, 1, 1, 11, 30, 0, 0, "A", 7200), "11:30:00!UTC"),
("", (2018, 2, 3, 11, 9, 0, 2, "Z", 1800), "2018-02-03T11:09:00.000002+0030"),
("!UTC", (2018, 2, 3, 11, 9, 0, 2, "Z", 1800), "2018-02-03T10:39:00.000002+0000"),
],
)
def test_formatting(writer, monkeypatch_date, time_format, date, expected):
Expand Down Expand Up @@ -69,9 +124,7 @@ def test_file_formatting(monkeypatch_date, tmpdir):


def test_missing_struct_time_fields(writer, monkeypatch, monkeypatch_date):

class struct_time:

def __init__(self, struct):
self._struct = struct

Expand All @@ -80,7 +133,6 @@ def __getattr__(self, attr):
raise AttributeError
return getattr(self._struct, attr)


def localtime(*args, **kwargs):
local = time.localtime(*args, **kwargs)
return struct_time(local)
Expand Down

0 comments on commit 78b93b6

Please sign in to comment.