Skip to content

Commit

Permalink
Improve interoperability of TimeSpan dates.
Browse files Browse the repository at this point in the history
Attributes 'start_date' and 'end_date' must return a normal Python
`datetime.date`:class: instance under normal operation.

Returning an Infinity-comparable type should be regarded as an internal
implementation hack to facilitate the implementation of `__and__` and
`__or__`.

This allows normal code to do math with `start_date` and `end_date`.  For
instance::

   >>> from datetime import date
   >>> date.today() - timespan.start_end

That could fail in Python 2.7 because 'date' would not interoperate with the
subtype returned by 'start_date'.
  • Loading branch information
mvaled committed Sep 20, 2017
1 parent 321f35c commit 9948d48
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 9 deletions.
19 changes: 19 additions & 0 deletions tests/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,22 @@ def test_timespans_are_representable(value):
@given(time_span('none'))
def test_generate_valid_timespans(ts):
assert ts.valid


@given(time_span('none'))
def test_ts_returns_dates_not_subtypes(ts):
from datetime import date
assert type(ts.start_date) is date
assert type(ts.end_date) is date

from xoutil.context import context
from xoutil.datetime import infinity_extended_date, NEEDS_FLEX_DATE
with context(NEEDS_FLEX_DATE):
assert type(ts.start_date) is infinity_extended_date
assert type(ts.end_date) is infinity_extended_date


@given(time_span('none'), strategies.dates())
def test_operate_with_timespans(ts, d):
assert ts.start_date - d is not None
assert d - ts.start_date is not None
42 changes: 33 additions & 9 deletions xoutil/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,10 @@ def __init__(self, name, nullable=False):
self.nullable = nullable

def __get__(self, instance, owner):
from xoutil.context import context
if instance is not None:
res = instance.__dict__[self.name]
if res:
if res and NEEDS_FLEX_DATE in context:
return infinity_extended_date(res.year, res.month, res.day)
else:
return res
Expand Down Expand Up @@ -563,10 +564,12 @@ def valid(self):
Unbound time spans are always valid.
'''
if self.bound:
return self.start_date <= self.end_date
else:
return True
from xoutil.context import context
with context(NEEDS_FLEX_DATE):
if self.bound:
return self.start_date <= self.end_date
else:
return True

def __contains__(self, other):
'''Test if we completely cover `other` time span.
Expand Down Expand Up @@ -651,14 +654,22 @@ def __and__(self, other):
'''
import datetime
from .infinity import Infinity
from xoutil.context import context
if isinstance(other, _EmptyTimeSpan):
return other
elif isinstance(other, datetime.date):
other = TimeSpan.from_date(other)
elif not isinstance(other, TimeSpan):
raise TypeError
start = max(self.start_date or -Infinity, other.start_date or -Infinity)
end = min(self.end_date or Infinity, other.end_date or Infinity)
with context(NEEDS_FLEX_DATE):
start = max(
self.start_date or -Infinity,
other.start_date or -Infinity
)
end = min(
self.end_date or Infinity,
other.end_date or Infinity
)
if start <= end:
if start is -Infinity:
start = None
Expand All @@ -679,14 +690,22 @@ def __or__(self, other):
'Return the union of both time spans.'
import datetime
from .infinity import Infinity
from xoutil.context import context
if isinstance(other, _EmptyTimeSpan):
return self
elif isinstance(other, datetime.date):
other = TimeSpan.from_date(other)
elif not isinstance(other, TimeSpan):
raise TypeError
start = min(self.start_date or -Infinity, other.start_date or -Infinity)
end = max(self.end_date or Infinity, other.end_date or Infinity)
with context(NEEDS_FLEX_DATE):
start = min(
self.start_date or -Infinity,
other.start_date or -Infinity
)
end = max(
self.end_date or Infinity,
other.end_date or Infinity
)
if start <= end:
if start is -Infinity:
start = None
Expand Down Expand Up @@ -779,3 +798,8 @@ def __repr__(self):
EmptyTimeSpan = _EmptyTimeSpan()

_EmptyTimeSpan.__new__ = None # Disallow creating more instances


# A context to switch on/off returning a subtype of date from DateFields.
# Used within TimeSpan to allow comparison with Infinity.
NEEDS_FLEX_DATE = object()

0 comments on commit 9948d48

Please sign in to comment.