Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TimeStamper() now uses TZ-aware objects #709

Merged
merged 4 commits into from
Mar 8, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
TimeStamper() now uses TZ-aware objects
The default output doesn't change but manual formatting allows for TZ
data now.

Fixes #703
hynek committed Mar 8, 2025
commit 7022f44c5d13f46ccb44ca49873763255ebb9ac6
7 changes: 4 additions & 3 deletions src/structlog/processors.py
Original file line number Diff line number Diff line change
@@ -525,8 +525,7 @@ def now() -> datetime.datetime:
else:

def now() -> datetime.datetime:
# A naive local datetime is fine here, because we only format it.
return datetime.datetime.now() # noqa: DTZ005
return datetime.datetime.now().astimezone()

if fmt is None:

@@ -540,7 +539,9 @@ def stamper_unix(event_dict: EventDict) -> EventDict:
if fmt.upper() == "ISO":

def stamper_iso_local(event_dict: EventDict) -> EventDict:
event_dict[key] = now().isoformat()
# We remove the timezone offset for backwards-compatibility. If the
# user wants a timezone, they have to set fmt manually.
event_dict[key] = now().isoformat().rsplit("+", 1)[0]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Counterexample:

>>> datetime.datetime.now().astimezone(datetime.timezone(datetime.timedelta(hours=-5))).isoformat().rsplit("+", 1)[0]
'2025-03-08T09:55:44.415778-05:00'

datetime.isoformat() states that its ISO time zone designator format is +HH:MM[:SS[.ffffff]], but right below they give us a sample with negative offset—'2009-11-27T00:00:00.000100-06:39'.

I.e. their + in +HH:MM is actually "plus or minus". Negative offsets are totally valid by ISO 8601 / RFC 3339 anyway.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bleh true. great, according to the docstring, the TZ is always +zz:zz so before we start imitating using strftime, just a [:-6]?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My proposal: datetime.datetime.now().astimezone().replace(tzinfo=None).isoformat(),
i.e. event_dict[key] = now().replace(tzinfo=None).isoformat()

datetime.astimezone() reads: "If you merely want to remove the timezone object from an aware datetime dt without conversion of date and time data, use dt.replace(tzinfo=None)." I think it is exactly our case: we want to convert aware time object back to naive, i.e. remove tzinfo.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait, this is stupid. with either approach, I'm adding and removing the timezone instead of adding it only when necessary. i.e. in stamper_fmt. behold the genius who finally read his own code: 9e0e47e

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, isoformat() uses _tzstr() which in turn uses _format_offset(), which gives us sing and possibly seconds and microseconds.

I can't imagine real worlds applications for TZ offset with seconds and its fractions (some crazy astronomy stuff??), but is valid per current standards, so just [:-6] is not enough because time zone designator has variable length.

Yes, docstrings for these functions are confusing indeed, they repeatedly use +hh:mm which really is just one possible option...

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we posted at almost the same time, check back my comment before. I think I solved it pretty much perfectly.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I do agree. I also tested it right now and it works as intended. Thanks to both of us:D

return event_dict

def stamper_iso_utc(event_dict: EventDict) -> EventDict:
15 changes: 13 additions & 2 deletions tests/processors/test_renderers.py
Original file line number Diff line number Diff line change
@@ -396,8 +396,8 @@ def test_inserts_utc_unix_timestamp_by_default(self):
@freeze_time("1980-03-25 16:00:00")
def test_local(self):
"""
Timestamp in local timezone work. We can't add a timezone to the
string without additional libraries.
Timestamp in local timezone work. Due to historic reasons, the default
format does not include a timezone.
"""
ts = TimeStamper(fmt="iso", utc=False)
d = ts(None, None, {})
@@ -414,6 +414,17 @@ def test_formats(self):

assert "1980" == d["timestamp"]

@freeze_time("1980-03-25 16:00:00")
def test_tz_aware(self):
"""
The timestamp that is used for formatting is timezone-aware.
"""
ts = TimeStamper(fmt="%z")
d = ts(None, None, {})

assert "" == datetime.datetime.now().strftime("%z") # noqa: DTZ005
assert "" != d["timestamp"]

@freeze_time("1980-03-25 16:00:00")
def test_adds_Z_to_iso(self):
"""