From 01b9b7974e107f58b509301e558790754e8fcdb1 Mon Sep 17 00:00:00 2001 From: Zalan Blenessy Date: Sun, 1 Mar 2026 13:27:06 +0100 Subject: [PATCH] Support microsecond timestamp resolution The dateTimeStamp XML standard type supports fractional second notation: https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp Before this patch, the tool crashed when fractional timestamps were given in e.g. CreationInfo. Current implementation is limited to microsecond resolution. More fine-grained timestamps (nanosecond), will be silently truncated to microsecond resolution. Signed-off-by: Zalan Blenessy --- src/spdx_tools/spdx/datetime_conversions.py | 11 +++++++++-- tests/spdx/test_datetime_conversions.py | 16 ++++++++++++++-- .../writer/tag_value/test_write_document.py | 5 +---- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/spdx_tools/spdx/datetime_conversions.py b/src/spdx_tools/spdx/datetime_conversions.py index cce624d57..f30cb9587 100644 --- a/src/spdx_tools/spdx/datetime_conversions.py +++ b/src/spdx_tools/spdx/datetime_conversions.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2022 spdx contributors # # SPDX-License-Identifier: Apache-2.0 +import re from datetime import datetime, timezone @@ -8,8 +9,14 @@ def datetime_from_str(date_str: str) -> datetime: if not isinstance(date_str, str): raise TypeError(f"Could not convert str to datetime, invalid type: {type(date_str).__name__}") - date = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ") # raises ValueError if format does not match - return date + if "." not in date_str: + return datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ") # raises ValueError if format does not match + + # Based on the https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp + # The secondFrag allows fractional second notation as well. + # normalize to micro seconds so that we can use %f with strptime + date_str = re.sub(r"\.(\d{1,6})\d*Z$", r".\1Z", date_str) + return datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ") # raises ValueError if format does not match def datetime_to_iso_string(date: datetime) -> str: diff --git a/tests/spdx/test_datetime_conversions.py b/tests/spdx/test_datetime_conversions.py index 3967d593b..fb2e579df 100644 --- a/tests/spdx/test_datetime_conversions.py +++ b/tests/spdx/test_datetime_conversions.py @@ -43,10 +43,22 @@ def test_local_datetime_to_iso_string(): def test_datetime_from_str(): date_str = "2010-03-04T05:45:11Z" + assert datetime_from_str(date_str) == datetime(2010, 3, 4, 5, 45, 11) - date = datetime_from_str(date_str) + date_str = "2010-03-04T05:45:11.0Z" + assert datetime_from_str(date_str) == datetime(2010, 3, 4, 5, 45, 11) - assert date == datetime(2010, 3, 4, 5, 45, 11) + # implicit notation + date_str = "2010-03-04T05:45:11.1Z" + assert datetime_from_str(date_str) == datetime(2010, 3, 4, 5, 45, 11, 100000) + + # explicity notation + date_str = "2010-03-04T05:45:11.123456Z" + assert datetime_from_str(date_str) == datetime(2010, 3, 4, 5, 45, 11, 123456) + + # truncation of nano seconds + date_str = "2010-03-04T05:45:11.1234567Z" + assert datetime_from_str(date_str) == datetime(2010, 3, 4, 5, 45, 11, 123456) @pytest.mark.parametrize( diff --git a/tests/spdx3/writer/tag_value/test_write_document.py b/tests/spdx3/writer/tag_value/test_write_document.py index 580c61b81..10d5d54c6 100644 --- a/tests/spdx3/writer/tag_value/test_write_document.py +++ b/tests/spdx3/writer/tag_value/test_write_document.py @@ -29,9 +29,7 @@ def test_render_creation_info(): output_str = io.StringIO() write_spdx_document(spdx_document, text_output=output_str) - assert ( - output_str.getvalue() - == """\ + assert output_str.getvalue() == """\ ## SPDX Document SPDXID: SPDXRef-FOO name: BAR @@ -42,4 +40,3 @@ def test_render_creation_info(): data license: CC0-1.0 elements: """ # noqa: W291 # elements: are printed with a space - )