From 01b9b7974e107f58b509301e558790754e8fcdb1 Mon Sep 17 00:00:00 2001 From: Zalan Blenessy Date: Sun, 1 Mar 2026 13:27:06 +0100 Subject: [PATCH 1/3] 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 - ) From c14c92f6642ca7e9372b47c7599dbbca144bcd14 Mon Sep 17 00:00:00 2001 From: zbleness Date: Mon, 23 Mar 2026 18:17:39 +0100 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Arthit Suriyawongkul Signed-off-by: Zalan Blenessy --- src/spdx_tools/spdx/datetime_conversions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/spdx_tools/spdx/datetime_conversions.py b/src/spdx_tools/spdx/datetime_conversions.py index f30cb9587..dbf91eb63 100644 --- a/src/spdx_tools/spdx/datetime_conversions.py +++ b/src/spdx_tools/spdx/datetime_conversions.py @@ -16,7 +16,13 @@ def datetime_from_str(date_str: str) -> datetime: # 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 + warnings.warn( + "Invalid date format. Expected YYYY-MM-DDThh:mm:ssZ " + "Sub-second fractions have been discarded", + category=UserWarning, + stacklevel=2 + ) + return datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ") # raises ValueError if format does not match def datetime_to_iso_string(date: datetime) -> str: From 7539de95e7a5a1db11b2d63353d944a527ef889e Mon Sep 17 00:00:00 2001 From: Zalan Blenessy Date: Mon, 23 Mar 2026 18:41:03 +0100 Subject: [PATCH 3/3] Fix regex and import warnings Signed-off-by: Zalan Blenessy --- src/spdx_tools/spdx/datetime_conversions.py | 10 +++++----- tests/spdx/test_datetime_conversions.py | 13 ++----------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/spdx_tools/spdx/datetime_conversions.py b/src/spdx_tools/spdx/datetime_conversions.py index dbf91eb63..6f84c2cda 100644 --- a/src/spdx_tools/spdx/datetime_conversions.py +++ b/src/spdx_tools/spdx/datetime_conversions.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 import re +import warnings from datetime import datetime, timezone @@ -14,13 +15,12 @@ def datetime_from_str(date_str: str) -> datetime: # 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) + # truncate the microsecond part + date_str = re.sub(r"\.\d*Z$", "Z", date_str) warnings.warn( - "Invalid date format. Expected YYYY-MM-DDThh:mm:ssZ " - "Sub-second fractions have been discarded", + "Invalid date format. Expected YYYY-MM-DDThh:mm:ssZ. Sub-second fractions have been discarded.", category=UserWarning, - stacklevel=2 + stacklevel=2, ) return datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ") # raises ValueError if format does not match diff --git a/tests/spdx/test_datetime_conversions.py b/tests/spdx/test_datetime_conversions.py index fb2e579df..068a13769 100644 --- a/tests/spdx/test_datetime_conversions.py +++ b/tests/spdx/test_datetime_conversions.py @@ -48,17 +48,8 @@ def test_datetime_from_str(): date_str = "2010-03-04T05:45:11.0Z" assert datetime_from_str(date_str) == 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) + date_str = "2010-03-04T05:45:11.123456789Z" + assert datetime_from_str(date_str) == datetime(2010, 3, 4, 5, 45, 11) @pytest.mark.parametrize(