Skip to content

Commit

Permalink
Datetime serialization and advice (quantumlib#5422)
Browse files Browse the repository at this point in the history
fixes quantumlib#5301 

See quantumlib#5301 for motivation
  • Loading branch information
mpharrigan authored and rht committed May 1, 2023
1 parent fc62b3d commit a2ae98a
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 17 deletions.
11 changes: 5 additions & 6 deletions cirq-core/cirq/json_resolver_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,12 @@ def _parallel_gate_op(gate, qubits):
return cirq.parallel_gate_op(gate, *qubits)

def _datetime(timestamp: float) -> datetime.datetime:
# As part of our serialization logic, we make sure we only serialize "aware"
# datetimes with the UTC timezone, so we implicitly add back in the UTC timezone here.
# We serialize datetimes (both with ("aware") and without ("naive") timezone information)
# as unix timestamps. The deserialized datetime will always refer to the
# same point in time, but will be re-constructed as a timezone-aware object.
#
# Please note: even if the assumption is somehow violated, the fact that we use
# unix timestamps should mean that the deserialized datetime should refer to the
# same point in time but may not satisfy o = read_json(to_json(o)) because the actual
# timezones, and hour fields will not be identical.
# If `o` is a naive datetime, o != read_json(to_json(o)) because Python doesn't
# let you compare aware and naive datetimes.
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)

def _symmetricalqidpair(qids):
Expand Down
7 changes: 0 additions & 7 deletions cirq-core/cirq/protocols/json_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,13 +398,6 @@ def default(self, o):

# datetime
if isinstance(o, datetime.datetime):
if o.tzinfo is None or o.tzinfo.utcoffset(o) is None:
# Otherwise, the deserialized object may change depending on local timezone.
raise TypeError(
"Can only serialize 'aware' datetime objects with `tzinfo`. "
"Consider using e.g. `datetime.datetime.now(tz=datetime.timezone.utc)`"
)

return {'cirq_type': 'datetime.datetime', 'timestamp': o.timestamp()}

return super().default(o) # coverage: ignore
Expand Down
14 changes: 10 additions & 4 deletions cirq-core/cirq/protocols/json_serialization_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,11 +945,17 @@ def test_basic_time_assertions():
def test_datetime():
naive_dt = datetime.datetime.now()

with pytest.raises(TypeError):
cirq.to_json(naive_dt)
re_naive_dt = cirq.read_json(json_text=cirq.to_json(naive_dt))
assert re_naive_dt != naive_dt, 'loads in with timezone'
assert re_naive_dt.timestamp() == naive_dt.timestamp()

utc_dt = naive_dt.astimezone(datetime.timezone.utc)
assert utc_dt == cirq.read_json(json_text=cirq.to_json(utc_dt))
re_utc_dt = cirq.read_json(json_text=cirq.to_json(utc_dt))
assert re_utc_dt == utc_dt
assert re_utc_dt == re_naive_dt

pst_dt = naive_dt.astimezone(tz=datetime.timezone(offset=datetime.timedelta(hours=-8)))
assert utc_dt == cirq.read_json(json_text=cirq.to_json(pst_dt))
re_pst_dt = cirq.read_json(json_text=cirq.to_json(pst_dt))
assert re_pst_dt == pst_dt
assert re_pst_dt == utc_dt
assert re_pst_dt == re_naive_dt
23 changes: 23 additions & 0 deletions docs/dev/style.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,26 @@ later in the same file.
Using consistent wording across Cirq is important for lowering users
cognitive load. For rule governing naming, see the
[nomenclature guidelines](nomenclature.md).

## Datetimes

Prefer using timezone-aware `datetime` objects.

```python
import datetime
dt = datetime.datetime.now(tz=datetime.timezone.utc)
```

Public components of Protobuf APIs will return "aware" `datetime` objects.
JSON de-serialization will promote values to "aware" `datetime` objects upon deserialization.

Comparing (or testing equality) between "naive" and "aware" `datetime` objects throws
an exception.
If you are implementing a class that has `datetime` member variables, delegate equality
and comparison operators to the built-in `datetime` equality and comparison operators.
If you're writing a function that compares `datetime` objects, you can defensively promote
them to "aware" objects or use their `.timestamp()` properties for a comparison that will
never throw an exception.

Absolutely do not use `datetime.utcnow()` as explained in the warnings in the
Python [documentation](https://docs.python.org/3/library/datetime.html).

0 comments on commit a2ae98a

Please sign in to comment.