BREAKING CHANGE in v26: check if Timestamp is valid.

Seconds should be in range [-62135596800, 253402300799]
Nanos should be in range [0, 999999999]

PiperOrigin-RevId: 592365636
pull/15154/head
Jie Luo 11 months ago committed by Copybara-Service
parent f75fe9e5f5
commit 1250d5f6cc
  1. 9
      python/google/protobuf/internal/json_format_test.py
  2. 56
      python/google/protobuf/internal/well_known_types.py
  3. 24
      python/google/protobuf/internal/well_known_types_test.py

@ -1060,7 +1060,14 @@ class JsonFormatTest(JsonFormatBase):
json_format.Parse, text, message)
# Time bigger than maximum time.
message.value.seconds = 253402300800
self.assertRaisesRegex(OverflowError, 'date value out of range',
self.assertRaisesRegex(json_format.SerializeToJsonError,
'Timestamp is not valid',
json_format.MessageToJson, message)
# Nanos smaller than 0
message.value.seconds = 0
message.value.nanos = -1
self.assertRaisesRegex(json_format.SerializeToJsonError,
'Timestamp is not valid',
json_format.MessageToJson, message)
# Lower case t does not accept.
text = '{"value": "0001-01-01t00:00:00Z"}'

@ -33,6 +33,8 @@ _MILLIS_PER_SECOND = 1000
_MICROS_PER_SECOND = 1000000
_SECONDS_PER_DAY = 24 * 3600
_DURATION_SECONDS_MAX = 315576000000
_TIMESTAMP_SECONDS_MIN = -62135596800
_TIMESTAMP_SECONDS_MAX = 253402300799
_EPOCH_DATETIME_NAIVE = datetime.datetime(1970, 1, 1, tzinfo=None)
_EPOCH_DATETIME_AWARE = _EPOCH_DATETIME_NAIVE.replace(
@ -85,10 +87,10 @@ class Timestamp(object):
and uses 3, 6 or 9 fractional digits as required to represent the
exact time. Example of the return format: '1972-01-01T10:00:20.021Z'
"""
nanos = self.nanos % _NANOS_PER_SECOND
total_sec = self.seconds + (self.nanos - nanos) // _NANOS_PER_SECOND
seconds = total_sec % _SECONDS_PER_DAY
days = (total_sec - seconds) // _SECONDS_PER_DAY
_CheckTimestampValid(self.seconds, self.nanos)
nanos = self.nanos
seconds = self.seconds % _SECONDS_PER_DAY
days = (self.seconds - seconds) // _SECONDS_PER_DAY
dt = datetime.datetime(1970, 1, 1) + datetime.timedelta(days, seconds)
result = dt.isoformat()
@ -166,6 +168,7 @@ class Timestamp(object):
else:
seconds += (int(timezone[1:pos])*60+int(timezone[pos+1:]))*60
# Set seconds and nanos
_CheckTimestampValid(seconds, nanos)
self.seconds = int(seconds)
self.nanos = int(nanos)
@ -175,39 +178,53 @@ class Timestamp(object):
def ToNanoseconds(self):
"""Converts Timestamp to nanoseconds since epoch."""
_CheckTimestampValid(self.seconds, self.nanos)
return self.seconds * _NANOS_PER_SECOND + self.nanos
def ToMicroseconds(self):
"""Converts Timestamp to microseconds since epoch."""
_CheckTimestampValid(self.seconds, self.nanos)
return (self.seconds * _MICROS_PER_SECOND +
self.nanos // _NANOS_PER_MICROSECOND)
def ToMilliseconds(self):
"""Converts Timestamp to milliseconds since epoch."""
_CheckTimestampValid(self.seconds, self.nanos)
return (self.seconds * _MILLIS_PER_SECOND +
self.nanos // _NANOS_PER_MILLISECOND)
def ToSeconds(self):
"""Converts Timestamp to seconds since epoch."""
_CheckTimestampValid(self.seconds, self.nanos)
return self.seconds
def FromNanoseconds(self, nanos):
"""Converts nanoseconds since epoch to Timestamp."""
self.seconds = nanos // _NANOS_PER_SECOND
self.nanos = nanos % _NANOS_PER_SECOND
seconds = nanos // _NANOS_PER_SECOND
nanos = nanos % _NANOS_PER_SECOND
_CheckTimestampValid(seconds, nanos)
self.seconds = seconds
self.nanos = nanos
def FromMicroseconds(self, micros):
"""Converts microseconds since epoch to Timestamp."""
self.seconds = micros // _MICROS_PER_SECOND
self.nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND
seconds = micros // _MICROS_PER_SECOND
nanos = (micros % _MICROS_PER_SECOND) * _NANOS_PER_MICROSECOND
_CheckTimestampValid(seconds, nanos)
self.seconds = seconds
self.nanos = nanos
def FromMilliseconds(self, millis):
"""Converts milliseconds since epoch to Timestamp."""
self.seconds = millis // _MILLIS_PER_SECOND
self.nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND
seconds = millis // _MILLIS_PER_SECOND
nanos = (millis % _MILLIS_PER_SECOND) * _NANOS_PER_MILLISECOND
_CheckTimestampValid(seconds, nanos)
self.seconds = seconds
self.nanos = nanos
def FromSeconds(self, seconds):
"""Converts seconds since epoch to Timestamp."""
_CheckTimestampValid(seconds, 0)
self.seconds = seconds
self.nanos = 0
@ -229,6 +246,7 @@ class Timestamp(object):
# https://github.com/python/cpython/issues/109849) or full range (on some
# platforms, see https://github.com/python/cpython/issues/110042) of
# datetime.
_CheckTimestampValid(self.seconds, self.nanos)
delta = datetime.timedelta(
seconds=self.seconds,
microseconds=_RoundTowardZero(self.nanos, _NANOS_PER_MICROSECOND),
@ -252,8 +270,22 @@ class Timestamp(object):
# manipulated into a long value of seconds. During the conversion from
# struct_time to long, the source date in UTC, and so it follows that the
# correct transformation is calendar.timegm()
self.seconds = calendar.timegm(dt.utctimetuple())
self.nanos = dt.microsecond * _NANOS_PER_MICROSECOND
seconds = calendar.timegm(dt.utctimetuple())
nanos = dt.microsecond * _NANOS_PER_MICROSECOND
_CheckTimestampValid(seconds, nanos)
self.seconds = seconds
self.nanos = nanos
def _CheckTimestampValid(seconds, nanos):
if seconds < _TIMESTAMP_SECONDS_MIN or seconds > _TIMESTAMP_SECONDS_MAX:
raise ValueError(
'Timestamp is not valid: Seconds {0} must be in range '
'[-62135596800, 253402300799].'.format(seconds))
if nanos < 0 or nanos >= _NANOS_PER_SECOND:
raise ValueError(
'Timestamp is not valid: Nanos {} must be in a range '
'[0, 999999].'.format(nanos))
class Duration(object):

@ -352,27 +352,15 @@ class TimeUtilTest(TimeUtilTestBase):
)
def testNanosOneSecond(self):
# TODO: b/301980950 - Test error behavior instead once ToDatetime validates
# that nanos are in expected range.
tz = _TZ_PACIFIC
ts = timestamp_pb2.Timestamp(nanos=1_000_000_000)
self.assertEqual(ts.ToDatetime(), datetime.datetime(1970, 1, 1, 0, 0, 1))
self.assertEqual(
ts.ToDatetime(tz), datetime.datetime(1969, 12, 31, 16, 0, 1, tzinfo=tz)
)
self.assertRaisesRegex(ValueError, 'Timestamp is not valid',
ts.ToDatetime)
def testNanosNegativeOneSecond(self):
# TODO: b/301980950 - Test error behavior instead once ToDatetime validates
# that nanos are in expected range.
tz = _TZ_PACIFIC
ts = timestamp_pb2.Timestamp(nanos=-1_000_000_000)
self.assertEqual(
ts.ToDatetime(), datetime.datetime(1969, 12, 31, 23, 59, 59)
)
self.assertEqual(
ts.ToDatetime(tz),
datetime.datetime(1969, 12, 31, 15, 59, 59, tzinfo=tz),
)
self.assertRaisesRegex(ValueError, 'Timestamp is not valid',
ts.ToDatetime)
def testTimedeltaConversion(self):
message = duration_pb2.Duration()
@ -421,8 +409,10 @@ class TimeUtilTest(TimeUtilTestBase):
self.assertRaisesRegex(ValueError, 'year (0 )?is out of range',
message.FromJsonString, '0000-01-01T00:00:00Z')
message.seconds = 253402300800
self.assertRaisesRegex(OverflowError, 'date value out of range',
self.assertRaisesRegex(ValueError, 'Timestamp is not valid',
message.ToJsonString)
self.assertRaisesRegex(ValueError, 'Timestamp is not valid',
message.FromSeconds, -62135596801)
def testInvalidDuration(self):
message = duration_pb2.Duration()

Loading…
Cancel
Save