Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New core strategies: datetimes, dates, times, timedeltas #621

Merged
merged 9 commits into from
May 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ Hypothesis APIs come in three flavours:
You should generally assume that an API is internal unless you have specific
information to the contrary.

-------------------
3.11.0 - 2017-05-23
-------------------

This is a feature release, adding datetime-related strategies to the core strategies.

``extra.pytz.timezones`` allows you to sample pytz timezones from
the Olsen database. Use directly in a recipe for tz-aware datetimes, or
compose with `st.none()` to allow a mix of aware and naive output.

The new ``dates``, ``times``, ``datetimes``, and ``timedeltas`` strategies
in ``hypothesis.strategies`` are all constrained by objects of their type.
This means that you can generate dates bounded by a single day
(i.e. a single date), or datetimes constrained to the microsecond.

``times`` and ``datetimes`` take an optional ``timezones=`` argument, which
defaults to ``none()`` for naive times. You can use our extra strategy
based on pytz, or roll your own timezones strategy with dateutil or even
the standard library.

The old ``dates``, ``times``, and ``datetimes`` strategies in
``hypothesis.extra.datetimes`` are deprecated in favor of the new
core strategies, which are more flexible and have no dependencies.

-------------------
3.10.0 - 2017-05-22
-------------------
Expand Down
100 changes: 8 additions & 92 deletions docs/extras.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,103 +17,19 @@ compatible version and each package will note the expected compatibility range.
you run into a bug with any of these please specify the dependency version.

--------------------
hypothesis[datetime]
hypothesis[pytz]
--------------------

As might be expected, this provides strategies for which generating instances
of objects from the ``datetime`` module: ``datetime``\s, ``date``\s, and
``time``\s. It depends on ``pytz`` to work.

It should work with just about any version of ``pytz``. ``pytz`` has a very
stable API and Hypothesis works around a bug or two in older versions.

It lives in the ``hypothesis.extra.datetime`` package.


.. method:: datetimes(allow_naive=None, timezones=None, min_year=None, \
max_year=None)

This strategy generates ``datetime`` objects. For example:

.. code-block:: pycon

>>> from hypothesis.extra.datetime import datetimes
>>> datetimes().example()
datetime.datetime(1705, 1, 20, 0, 32, 0, 973139, tzinfo=<DstTzInfo 'Israel...
>>> datetimes().example()
datetime.datetime(7274, 6, 9, 23, 0, 31, 75498, tzinfo=<DstTzInfo 'America...

As you can see, it produces years from quite a wide range. If you want to
narrow it down you can ask for a more specific range of years:

.. code-block:: pycon

>>> datetimes(min_year=2001, max_year=2010).example()
datetime.datetime(2010, 7, 7, 0, 15, 0, 614034, tzinfo=<DstTzInfo 'Pacif...
>>> datetimes(min_year=2001, max_year=2010).example()
datetime.datetime(2006, 9, 26, 22, 0, 0, 220365, tzinfo=<DstTzInfo 'Asia...

You can also specify timezones:

.. code-block:: pycon

>>> import pytz
>>> pytz.all_timezones[:3]
['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa']
>>> datetimes(timezones=pytz.all_timezones[:3]).example()
datetime.datetime(6257, 8, 21, 13, 6, 24, 8751, tzinfo=<DstTzInfo 'Africa/Accra' GMT0:00:00 STD>)
>>> datetimes(timezones=pytz.all_timezones[:3]).example()
datetime.datetime(7851, 2, 3, 0, 0, 0, 767400, tzinfo=<DstTzInfo 'Africa/Accra' GMT0:00:00 STD>)
>>> datetimes(timezones=pytz.all_timezones[:3]).example()
datetime.datetime(8262, 6, 22, 16, 0, 0, 154235, tzinfo=<DstTzInfo 'Africa/Abidjan' GMT0:00:00 STD>)

If the set of timezones is empty you will get a naive datetime:

.. code-block:: pycon
.. automodule:: hypothesis.extra.pytz
:members:

>>> datetimes(timezones=[]).example()
datetime.datetime(918, 11, 26, 2, 0, 35, 916439)

You can also explicitly get a mix of naive and non-naive datetimes if you
want:

.. code-block:: pycon

>>> datetimes(allow_naive=True).example()
datetime.datetime(2433, 3, 20, 0, 0, 44, 460383, tzinfo=<DstTzInfo 'Asia/Hovd' HOVT+7:00:00 STD>)
>>> datetimes(allow_naive=True).example()
datetime.datetime(7003, 1, 22, 0, 0, 52, 401259)


.. method:: dates(min_year=None, max_year=None)

This strategy generates ``date`` objects. For example:

.. code-block:: pycon

>>> from hypothesis.extra.datetime import dates
>>> dates().example()
datetime.date(1687, 3, 23)
>>> dates().example()
datetime.date(9565, 5, 2)

Again, you can restrict the range with the ``min_year`` and ``max_year``
arguments.


.. method:: times(allow_naive=None, timezones=None)

This strategy generates ``time`` objects. For example:

.. code-block:: pycon

>>> from hypothesis.extra.datetime import times
>>> times().example()
datetime.time(0, 15, 55, 188712, tzinfo=<DstTzInfo 'US/Hawaii' LMT-1 day, 13:29:00 STD>)
>>> times().example()
datetime.time(9, 0, 47, 959374, tzinfo=<DstTzInfo 'Pacific/Bougainville' BST+11:00:00 STD>)
--------------------
hypothesis[datetime]
--------------------

The ``allow_naive`` and ``timezones`` arguments act the same as the datetimes strategy.
.. automodule:: hypothesis.extra.datetime
:members:


-----------------------
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def local_file(name):

extras = {
'datetime': ['pytz'],
'pytz': ['pytz'],
'fakefactory': ['Faker>=0.7.0,<=0.7.1'],
'django': ['pytz', 'django>=1.8,<2'],
'numpy': ['numpy>=1.9.0'],
Expand Down
159 changes: 75 additions & 84 deletions src/hypothesis/extra/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,111 +15,102 @@
#
# END HEADER

"""This module provides deprecated time and date related strategies.

It depends on the ``pytz`` package, which is stable enough that almost any
version should be compatible - most updates are for the timezone database.

"""

from __future__ import division, print_function, absolute_import

import datetime as dt

import pytz

import hypothesis.internal.conjecture.utils as cu
import hypothesis.strategies as st
from hypothesis.errors import InvalidArgument
from hypothesis.strategies import defines_strategy
from hypothesis.searchstrategy.strategies import SearchStrategy


class DatetimeStrategy(SearchStrategy):

def __init__(self, allow_naive, timezones, min_year=None, max_year=None):
self.allow_naive = allow_naive
self.timezones = timezones
self.min_year = min_year or dt.MINYEAR
self.max_year = max_year or dt.MAXYEAR
for a in ['min_year', 'max_year']:
year = getattr(self, a)
if year < dt.MINYEAR:
raise InvalidArgument(u'%s out of range: %d < %d' % (
a, year, dt.MINYEAR
))
if year > dt.MAXYEAR:
raise InvalidArgument(u'%s out of range: %d > %d' % (
a, year, dt.MAXYEAR
))

def do_draw(self, data):
while True:
try:
result = dt.datetime(
year=cu.centered_integer_range(
data, self.min_year, self.max_year, 2000
),
month=cu.integer_range(data, 1, 12),
day=cu.integer_range(data, 1, 31),
hour=cu.integer_range(data, 0, 24),
minute=cu.integer_range(data, 0, 59),
second=cu.integer_range(data, 0, 59),
microsecond=cu.integer_range(data, 0, 999999)
)
if (
not self.allow_naive or
(self.timezones and cu.boolean(data))
):
result = cu.choice(data, self.timezones).localize(result)
return result

except (OverflowError, ValueError):
pass


@defines_strategy
from hypothesis._settings import note_deprecation
from hypothesis.extra.pytz import timezones as timezones_strategy

__all__ = ['datetimes', 'dates', 'times']


def tz_args_strat(allow_naive, tz_list, name):
if tz_list is None:
tz_strat = timezones_strategy()
else:
tz_strat = st.sampled_from([
tz if isinstance(tz, dt.tzinfo) else pytz.timezone(tz)
for tz in tz_list
])
if allow_naive or (allow_naive is None and tz_strat.is_empty):
tz_strat = st.none() | tz_strat
if tz_strat.is_empty:
raise InvalidArgument(
'Cannot create non-naive %s with no timezones allowed.' % name)
return tz_strat


def convert_year_bound(val, default):
if val is None:
return default
try:
return default.replace(val)
except ValueError:
raise InvalidArgument('Invalid year=%r' % (val,))


@st.defines_strategy
def datetimes(allow_naive=None, timezones=None, min_year=None, max_year=None):
"""Return a strategy for generating datetimes.

.. deprecated:: 3.9.0
use :py:func:`hypothesis.strategies.datetimes` instead.

allow_naive=True will cause the values to sometimes be naive.
timezones is the set of permissible timezones. If set to an empty
collection all timezones must be naive. If set to None all available
timezones will be used.
collection all datetimes will be naive. If set to None all timezones
available via pytz will be used.

All generated datetimes will be between min_year and max_year, inclusive.

"""
if timezones is None:
timezones = list(pytz.all_timezones)
timezones.remove(u'UTC')
timezones.insert(0, u'UTC')
timezones = [
tz if isinstance(tz, dt.tzinfo) else pytz.timezone(tz)
for tz in timezones
]
if allow_naive is None:
allow_naive = not timezones
if not (timezones or allow_naive):
raise InvalidArgument(
u'Cannot create non-naive datetimes with no timezones allowed'
)
return DatetimeStrategy(
allow_naive=allow_naive, timezones=timezones,
min_year=min_year, max_year=max_year,
)
note_deprecation('Use hypothesis.strategies.datetimes, which supports '
'full-precision bounds and has a simpler API.')
min_dt = convert_year_bound(min_year, dt.datetime.min)
max_dt = convert_year_bound(max_year, dt.datetime.max)
tzs = tz_args_strat(allow_naive, timezones, 'datetimes')
return st.datetimes(min_dt, max_dt, tzs)


@defines_strategy
@st.defines_strategy
def dates(min_year=None, max_year=None):
"""Return a strategy for generating dates."""
return datetimes(
allow_naive=True, timezones=[],
min_year=min_year, max_year=max_year,
).map(datetime_to_date)
"""Return a strategy for generating dates.

.. deprecated:: 3.9.0
use :py:func:`hypothesis.strategies.dates` instead.

def datetime_to_date(dt):
return dt.date()
All generated dates will be between min_year and max_year, inclusive.

"""
note_deprecation('Use hypothesis.strategies.dates, which supports bounds '
'given as date objects for single-day resolution.')
return st.dates(convert_year_bound(min_year, dt.date.min),
convert_year_bound(max_year, dt.date.max))

@defines_strategy

@st.defines_strategy
def times(allow_naive=None, timezones=None):
"""Return a strategy for generating times."""
return datetimes(
allow_naive=allow_naive, timezones=timezones,
).map(datetime_to_time)
"""Return a strategy for generating times.

.. deprecated:: 3.9.0
use :py:func:`hypothesis.strategies.times` instead.

The allow_naive and timezones arguments act the same as the datetimes
strategy above.

def datetime_to_time(dt):
return dt.timetz()
"""
note_deprecation('Use hypothesis.strategies.times, which supports '
'min_time and max_time arguments.')
return st.times(timezones=tz_args_strat(allow_naive, timezones, 'times'))
6 changes: 3 additions & 3 deletions src/hypothesis/extra/django/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

import hypothesis.strategies as st
from hypothesis.errors import InvalidArgument
from hypothesis.extra.datetime import datetimes
from hypothesis.extra.pytz import timezones
from hypothesis.utils.conventions import UniqueIdentifier
from hypothesis.searchstrategy.strategies import SearchStrategy

Expand All @@ -50,8 +50,8 @@ def referenced_models(model, seen=None):

def get_datetime_strat():
if getattr(django_settings, 'USE_TZ', False):
return datetimes(allow_naive=False)
return datetimes(timezones=[])
return st.datetimes(timezones=timezones())
return st.datetimes()


__default_field_mappings = None
Expand Down
Loading