PEP 495: Addressed several (not all yet) mailing list commments.

This commit is contained in:
Alexander Belopolsky 2015-08-21 22:28:19 -04:00
parent 104dc4b901
commit 173e09ac8b
1 changed files with 181 additions and 96 deletions

View File

@ -21,15 +21,15 @@ between two moments in time for which local times are the same.
.. sidebar:: US public service advertisement .. sidebar:: US public service advertisement
.. image:: pep-0495-daylightsavings.png .. image:: pep-0495-daylightsavings.png
:align: right :align: center
:width: 15% :width: 95%
Rationale Rationale
========= =========
In the most world locations there have been and will be times when In the most world locations there have been and will be times when
local clocks are moved back. In those times, intervals are introduced local clocks are moved back. [#]_ In those times, intervals are introduced
in which local clocks show the same time twice in the same day. In in which local clocks show the same time twice in the same day. In
these situations, the information displayed on a local clock (or these situations, the information displayed on a local clock (or
stored in a Python datetime instance) is insufficient to identify a stored in a Python datetime instance) is insufficient to identify a
@ -37,14 +37,27 @@ particular moment in time. The proposed solution is to add a boolean
flag to the ``datetime`` instances that will distinguish between the flag to the ``datetime`` instances that will distinguish between the
two ambiguous times. two ambiguous times.
.. [#] People who live in locations observing the Daylight Saving
Time (DST) move their clocks back (usually one hour) every Fall.
It is less common, but occasionally clocks can be moved back for
other reasons. For example, Ukraine skipped the spring-forward
transition in March 1990 and instead, moved their clocks back on
July 1, 1990, switching from Moscow Time to Eastern European Time.
In that case, standard (winter) time was in effect before and after
the transition.
Both DST and standard time changes may result in time shifts other
than an hour.
Terminology Terminology
=========== ===========
When clocks are moved back, we say that a *fold* is created in the When clocks are moved back, we say that a *fold* is created in time.
fabric of time. When the clock are moved forward, a *gap* is created. When the clocks are moved forward, a *gap* is created. A local time
A local time that falls in the fold is called *ambiguous*. A local that falls in the fold is called *ambiguous*. A local time that falls
time that falls in the gap is called *missing*. in the gap is called *missing*.
Proposal Proposal
@ -55,7 +68,7 @@ The "first" flag
We propose adding a boolean member called ``first`` to the instances We propose adding a boolean member called ``first`` to the instances
of ``datetime.time`` and ``datetime.datetime`` classes. This member of ``datetime.time`` and ``datetime.datetime`` classes. This member
should have the value True for all instances except those that should have the value ``True`` for all instances except those that
represent the second (chronologically) moment in time in an ambiguous represent the second (chronologically) moment in time in an ambiguous
case. [#]_ case. [#]_
@ -143,6 +156,18 @@ second, microsecond and first.
Affected Behaviors Affected Behaviors
------------------ ------------------
What time is it?
................
The ``datetime.now()`` method called with no arguments, will set
``first=False`` when returning the second of the two ambiguous times
in a fold. When called with a ``tzinfo`` argument, the value of the
``first`` will be determined by the ``tzinfo.fromutc()``
implementation. If an instance of the built-in ``datetime.timezone``
is passed as ``tzinfo``, the returned datetime instance will always
have ``first=True``.
Conversion from naive to aware Conversion from naive to aware
.............................. ..............................
@ -159,16 +184,31 @@ For example, on a system set to US/Eastern timezone::
>>> dt.replace(first=False).astimezone().strftime('%D %T %Z%z') >>> dt.replace(first=False).astimezone().strftime('%D %T %Z%z')
'11/02/14 01:30:00 EST-0500' '11/02/14 01:30:00 EST-0500'
Conversion from POSIX seconds from EPOCH
........................................
The ``fromtimestamp()`` static method of ``datetime.datetime`` will
set the ``first`` attribute appropriately in the returned object.
For example, on a system set to US/Eastern timezone::
>>> datetime.fromtimestamp(1414906200)
datetime.datetime(2014, 11, 2, 1, 30)
>>> datetime.fromtimestamp(1414906200 + 3600)
datetime.datetime(2014, 11, 2, 1, 30, first=False)
Conversion to POSIX seconds from EPOCH Conversion to POSIX seconds from EPOCH
...................................... ......................................
The ``timestamp()`` method of ``datetime.datetime`` will return different The ``timestamp()`` method of ``datetime.datetime`` will return different
values for ``datetime.datetime`` instances that differ only by the value values for ``datetime.datetime`` instances that differ only by the value
of their ``first`` attribute if and only if these instances represent an of their ``first`` attribute if and only if these instances represent an
ambiguous or a non-existent time. ambiguous or a missing time.
When a ``datetime.datetime`` instance ``dt`` represents an ambiguous When a ``datetime.datetime`` instance ``dt`` represents an ambiguous
(repeated) time, there are two values ``s0`` and ``s1`` such that:: time, there are two values ``s0`` and ``s1`` such that::
datetime.fromtimestamp(s0) == datetime.fromtimestamp(s1) == dt datetime.fromtimestamp(s0) == datetime.fromtimestamp(s1) == dt
@ -208,21 +248,6 @@ For example, on a system set to US/Eastern timezone::
1425796200.0 1425796200.0
Conversion from POSIX seconds from EPOCH
........................................
The ``fromtimestamp()`` static method of ``datetime.datetime`` will
set the ``first`` attribute appropriately in the returned object.
For example, on a system set to US/Eastern timezone::
>>> datetime.fromtimestamp(1414906200)
datetime.datetime(2014, 11, 2, 1, 30)
>>> datetime.fromtimestamp(1414906200 + 3600)
datetime.datetime(2014, 11, 2, 1, 30, first=False)
Combining and splitting date and time Combining and splitting date and time
..................................... .....................................
@ -233,11 +258,27 @@ The ``datetime.datetime.time()`` method will copy the value of the
``first`` attribute to the resulting ``datetime.time`` instance. ``first`` attribute to the resulting ``datetime.time`` instance.
Pickles
.......
Pickle sizes for the ``datetime.datetime`` and ``datetime.time``
objects will not change. The ``first`` flag will be encoded in the
first bit of the 5th byte of the ``datetime.datetime`` pickle payload
or the 2nd byte of the datetime.time. In the `current implementation`_
these bytes are used to store minute value (0-59) and the first bit is
always 0. Note that ``first=True`` will be encoded as 0 in the first
bit and ``first=False`` as 1. (This change only affects pickle
format. In the C implementation, the "first" member will get a full byte
to store the actual boolean value.)
.. _current implementation: https://hg.python.org/cpython/file/d3b20bff9c5d/Include/datetime.h#l17
Implementations of tzinfo in stdlib Implementations of tzinfo in stdlib
................................... ===================================
No new implementations of ``datetime.tzinfo`` abstract class are No new implementations of ``datetime.tzinfo`` abstract class are
introduced in this PEP. The existing (fixed offset) timezones do proposed in this PEP. The existing (fixed offset) timezones do
not introduce ambiguous local times and their ``utcoffset()`` not introduce ambiguous local times and their ``utcoffset()``
implementation will return the same constant value as they do now implementation will return the same constant value as they do now
regardless of the value of ``first``. regardless of the value of ``first``.
@ -248,27 +289,32 @@ used anywhere in the stdlib because the only included ``tzinfo``
implementation (the ``datetime.timzeone`` class implementing fixed implementation (the ``datetime.timzeone`` class implementing fixed
offset timezones) override ``fromutc()``. offset timezones) override ``fromutc()``.
New guidelines will be published for implementing concrete timezones
with variable UTC offset.
Guidelines for New tzinfo Implementations
Guidelines for new tzinfo implementations =========================================
-----------------------------------------
Implementors of concrete ``datetime.tzinfo`` subclasses who want to Implementors of concrete ``datetime.tzinfo`` subclasses who want to
support variable UTC offsets (due to DST and other causes) must follow support variable UTC offsets (due to DST and other causes) should follow
these guidelines. these guidelines.
New subclasses must override the base-class ``fromutc()`` method and
Ignorance is Bliss
------------------
New implementations of ``utcoffset()``, ``tzname()`` and ``dst()``
methods should ignore the value of ``first`` unless they are called on
the ambiguous or missing times.
In the DST Fold
---------------
New subclasses should override the base-class ``fromutc()`` method and
implement it so that in all cases where two UTC times ``u1`` and implement it so that in all cases where two UTC times ``u1`` and
``u2`` (``u1`` <``u2``) correspond to the same local time ``u2`` (``u1`` <``u2``) correspond to the same local time
``fromutc(u1)`` will return an instance with ``first=True`` and ``fromutc(u1)`` will return an instance with ``first=True`` and
``fromutc(u2)`` will return an instance with ``first=False``. In all ``fromutc(u2)`` will return an instance with ``first=False``. In all
other cases the returned instance must have ``first=True``. other cases the returned instance should have ``first=True``.
New implementations of ``utcoffset()`` and ``dst()`` methods should
ignore the value of ``first`` unless they are called on the ambiguous
or missing times.
On an ambiguous time introduced at the end of DST, the values returned On an ambiguous time introduced at the end of DST, the values returned
by ``utcoffset()`` and ``dst()`` methods should be as follows by ``utcoffset()`` and ``dst()`` methods should be as follows
@ -276,30 +322,37 @@ by ``utcoffset()`` and ``dst()`` methods should be as follows
+-----------------+----------------+------------------+ +-----------------+----------------+------------------+
| | first=True | first=False | | | first=True | first=False |
+=================+================+==================+ +=================+================+==================+
| utcoff() | stdoff + hour | stdoff | | utcoffset() | stdoff + dstoff| stdoff |
+-----------------+----------------+------------------+ +-----------------+----------------+------------------+
| dst() | hour | zero | | dst() | dstoff | zero |
+-----------------+----------------+------------------+ +-----------------+----------------+------------------+
where ``stdoff`` is the standard (non-DST) offset, where ``stdoff`` is the standard (non-DST) offset, ``dstoff`` is the
``hour = timedelta(hours=1)`` and ``zero = timedelta(0)``. DST correction (typically ``dstoff = timedelta(hours=1)``) and ``zero
= timedelta(0)``.
Mind the DST Gap
----------------
On a missing time introduced at the start of DST, the values returned On a missing time introduced at the start of DST, the values returned
by ``utcoffset()`` and ``dst()`` methods should be as follows by ``utcoffset()`` and ``dst()`` methods should be as follows
+-----------------+----------------+------------------+ +-----------------+----------------+------------------+
| | first=True | first=False | | | first=True | first=False |
+=================+================+==================+ +=================+================+==================+
| utcoff() | stdoff | stdoff + hour | | utcoffset() | stdoff | stdoff + dstoff |
+-----------------+----------------+------------------+ +-----------------+----------------+------------------+
| dst() | zero | hour | | dst() | zero | dstoff |
+-----------------+----------------+------------------+ +-----------------+----------------+------------------+
Non-DST Folds and Gaps
----------------------
On ambiguous/missing times introduced by the change in the standard time On ambiguous/missing times introduced by the change in the standard time
offset, the ``dst()`` method should return the same value regardless of offset, the ``dst()`` method should return the same value regardless of
the value of ``first`` and the ``utcoff()`` should return values the value of ``first`` and the ``utcoffset()`` should return values
according to the following table: according to the following table:
+-----------------+----------------+-----------------------------+ +-----------------+----------------+-----------------------------+
@ -310,28 +363,11 @@ according to the following table:
| missing | oldoff | newoff = oldoff + delta | | missing | oldoff | newoff = oldoff + delta |
+-----------------+----------------+-----------------------------+ +-----------------+----------------+-----------------------------+
where ``delta`` is the size of the fold or the gap.
Pickle size Temporal Arithmetic
----------- ===================
Pickle sizes for the ``datetime.datetime`` and ``datetime.time``
objects will not change. The ``first`` flag will be encoded in the
first bit of the 5th byte of the ``datetime.datetime`` pickle payload
or the 2nd byte of the datetime.time. In the `current implementation`_
these bytes are used to store minute value (0-59) and the first bit is
always 0. Note that ``first=True`` will be encoded as 0 in the first
bit and ``first=False`` as 1. (This change only affects pickle
format. In C implementation, the "first" member will get a full byte
to store the actual boolean value.)
We chose the minute byte to store the the "first" bit because this
choice preserves the natural ordering.
.. _current implementation: https://hg.python.org/cpython/file/d3b20bff9c5d/Include/datetime.h#l17
Temporal Arithmetics
--------------------
The value of "first" will be ignored in all operations except those The value of "first" will be ignored in all operations except those
that involve conversion between timezones. [#]_ As a consequence, that involve conversion between timezones. [#]_ As a consequence,
@ -345,15 +381,16 @@ The result of addition (subtraction) of a timedelta to (from) a
datetime will always have ``first`` set to ``True`` even if the datetime will always have ``first`` set to ``True`` even if the
original datetime instance had ``first=False``. original datetime instance had ``first=False``.
.. [#] As of Python 3.5, ``tzinfo`` is ignored whenever timedelta is .. [#] Computing a difference between two aware datetime instances
added or subtracted from a ``datetime.datetime`` instance or when with different values of ``tzinfo`` involves an implicit timezone
one ``datetime.datetime`` instance is subtracted from another with conversion. In this case, the result may depend on the value of
the same (even not-None) ``tzinfo``. This may change in the future, the ``first`` flag in either of the instances, but only if the
but such changes are outside of the scope of this PEP. instance has ``tzinfo`` that accounts for the value of ``first``
in its ``utcoffset()`` method.
Backward and Forward Compatibility Backward and Forward Compatibility
---------------------------------- ==================================
This proposal will have little effect on the programs that do not read This proposal will have little effect on the programs that do not read
the ``first`` flag explicitly or use tzinfo implementations that do. the ``first`` flag explicitly or use tzinfo implementations that do.
@ -368,16 +405,19 @@ in the new versions will become unreadable by the older Python
versions. Pickles of instances with ``first=True`` (which is the versions. Pickles of instances with ``first=True`` (which is the
default) will remain unchanged. default) will remain unchanged.
Questions and Answers Questions and Answers
===================== =====================
1. Why not call the new flag "isdst"? Why not call the new flag "isdst"?
----------------------------------
------- A non-technical answer
......................
* Alice: Bob - let's have a stargazing party at 01:30 AM tomorrow! * Alice: Bob - let's have a stargazing party at 01:30 AM tomorrow!
* Bob: Should I presume initially that summer time (for example, Daylight * Bob: Should I presume initially that Daylight Saving Time is or is
Saving Time) is or is not (respectively) in effect for the specified time? not in effect for the specified time?
* Alice: Huh? * Alice: Huh?
------- -------
@ -386,32 +426,77 @@ Questions and Answers
* Alice: You know, Bob, 01:30 AM will happen twice tomorrow. Which time do you have in mind? * Alice: You know, Bob, 01:30 AM will happen twice tomorrow. Which time do you have in mind?
* Bob: I did not think about it, but let's pick the first. * Bob: I did not think about it, but let's pick the first.
-------
2. Why "first"? A technical reason
..................
* Rejections: While the ``tm_isdst`` field of the ``time.struct_time`` object can be
used to disambiguate local times in the fold, the semantics of such
disambiguation are completely different from the proposal in this PEP.
**second** The main problem with the ``tm_isdst`` field is that it is impossible
rejected because "second" is already there. to know what value is appropriate for ``tm_isdst`` without knowing the
details about the time zone that are only available to the ``tzinfo``
implementation. Thus while ``tm_isdst`` is useful in the *output* of
methods such as ``time.localtime``, it is cumbursome as an *input* of
methods such as ``time.mktime``.
If the programmer misspecifies a non-negative value of ``tm_isdst`` to
``time.mktime``, the result will be time that is 1 hour off and since
there is rarely a way to know anything about DST *before* a call to
``time.mktime`` is made, the only sane choice is usually
``tm_isdst=-1``.
Unlike ``tm_isdst``, the proposed ``first`` flag has no effect on the
interpretation of the datetime instance unless without that flag two
(or no) interpretations are possible.
Since it would be very confusing to have something called ``isdst``
that does not have the same semantics as ``tm_isdst``, we need a
different name. Moreover, the ``datetime.datetime`` class already has
a method called ``dst()`` and if we called ``first`` "isdst", we would
necessarily have situations when "isdst" and ``bool(dst())`` values
are different.
Why "first"?
------------
This is a working name chosen initially because the obvious
alternative ("second") conflicts with the existing attribute. It has
since became clear that it is desirable to have a flag with the
default value ``False`` and such that chronological ordering of
disambiguated (datetime, flag) pairs would match their lexicographical
order.
The following alternative names have been proposed:
**fold**
Suggested by Guido van Rossum and favored by the author. Has
correct connotations and easy mnemonic rules, but at the same
time does not invite unbased assumptions.
**later** **later**
rejected because "later" is confusable with "latter". A close contender to "fold". The author dislikes it because
it is confusable with equally fitting "latter," but in the age
**earlier** of autocompletion everywhere this is a small consideration. A
rejected because "earlier" has the same issue as "first" (requires stronger objection may be that in the case of missing time, we
default to be True) but is two characters longer. will have ``later=True`` instance converted to an earlier time by
``.astimezone(timezone.utc)`` that that with ``later=False``.
* Remaining possibilities: Yet again, this can be interpreted as a desirable indication that
the original time is invalid.
**repeated** **repeated**
this is a strong candidate Did not receive any support on the mailing list.
**is_first**
arguably more grammatically correct than "first"
**ltdf** **ltdf**
(Local Time Disambiguation Flag) - short and no-one will (Local Time Disambiguation Flag) - short and no-one will attempt
attempt to guess what it means without reading the docs. to guess what it means without reading the docs. (Feel free to
use it in discussions with the meaning ltdf=False is the
earlier if you don't want to endorse any of the alternatives
above.)
Implementation Implementation
============== ==============