Skip to content
Open
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
14 changes: 14 additions & 0 deletions luigi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,22 @@
NumericalParameter,
OptionalBoolParameter,
OptionalChoiceParameter,
OptionalDateHourParameter,
OptionalDateIntervalParameter,
OptionalDateMinuteParameter,
OptionalDateParameter,
OptionalDateSecondParameter,
OptionalDictParameter,
OptionalFloatParameter,
OptionalIntParameter,
OptionalListParameter,
OptionalMonthParameter,
OptionalNumericalParameter,
OptionalParameter,
OptionalPathParameter,
OptionalStrParameter,
OptionalTupleParameter,
OptionalYearParameter,
Parameter,
PathParameter,
StrParameter,
Expand Down Expand Up @@ -126,6 +133,13 @@
"OptionalFloatParameter",
"OptionalBoolParameter",
"OptionalPathParameter",
"OptionalDateParameter",
"OptionalMonthParameter",
"OptionalYearParameter",
"OptionalDateHourParameter",
"OptionalDateMinuteParameter",
"OptionalDateSecondParameter",
"OptionalDateIntervalParameter",
"OptionalDictParameter",
"OptionalListParameter",
"OptionalTupleParameter",
Expand Down
42 changes: 42 additions & 0 deletions luigi/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,12 @@ def normalize(self, x):
return x - datetime.timedelta(days=delta)


class OptionalDateParameter(OptionalParameterMixin[datetime.date], DateParameter): # type: ignore[misc]
"""Class to parse optional date parameters."""

expected_type = datetime.date


class MonthParameter(DateParameter):
"""
Parameter whose value is a :py:class:`~datetime.date`, specified to the month
Expand Down Expand Up @@ -637,6 +643,12 @@ def normalize(self, x):
return self._add_months(self.start, months_since_start)


class OptionalMonthParameter(OptionalParameterMixin[datetime.date], MonthParameter): # type: ignore[misc]
"""Class to parse optional month parameters."""

expected_type = datetime.date


class YearParameter(DateParameter):
"""
Parameter whose value is a :py:class:`~datetime.date`, specified to the year
Expand All @@ -662,6 +674,12 @@ def normalize(self, x):
return datetime.date(year=x.year - delta, month=1, day=1)


class OptionalYearParameter(OptionalParameterMixin[datetime.date], YearParameter): # type: ignore[misc]
"""Class to parse optional year parameters."""

expected_type = datetime.date


class _DatetimeParameterBase(Parameter[datetime.datetime]):
"""
Base class Parameter for datetime
Expand Down Expand Up @@ -746,6 +764,12 @@ class DateHourParameter(_DatetimeParameterBase):
_timedelta = datetime.timedelta(hours=1)


class OptionalDateHourParameter(OptionalParameterMixin[datetime.datetime], DateHourParameter): # type: ignore[misc]
"""Class to parse optional date hour parameters."""

expected_type = datetime.datetime


class DateMinuteParameter(_DatetimeParameterBase):
"""
Parameter whose value is a :py:class:`~datetime.datetime` specified to the minute.
Expand All @@ -770,6 +794,12 @@ def parse(self, x):
return super().parse(x)


class OptionalDateMinuteParameter(OptionalParameterMixin[datetime.datetime], DateMinuteParameter): # type: ignore[misc]
"""Class to parse optional date minute parameters."""

expected_type = datetime.datetime


class DateSecondParameter(_DatetimeParameterBase):
"""
Parameter whose value is a :py:class:`~datetime.datetime` specified to the second.
Expand All @@ -785,6 +815,12 @@ class DateSecondParameter(_DatetimeParameterBase):
_timedelta = datetime.timedelta(seconds=1)


class OptionalDateSecondParameter(OptionalParameterMixin[datetime.datetime], DateSecondParameter): # type: ignore[misc]
"""Class to parse optional date second parameters."""

expected_type = datetime.datetime


class StrParameter(Parameter[str]):
"""
Parameter whose value is a ``str``.
Expand Down Expand Up @@ -943,6 +979,12 @@ def parse(self, x):
raise ValueError("Invalid date interval - could not be parsed")


class OptionalDateIntervalParameter(OptionalParameterMixin[date_interval.DateInterval], DateIntervalParameter): # type: ignore[misc]
"""Class to parse optional date interval parameters."""

expected_type = date_interval.DateInterval


class TimeDeltaParameter(Parameter[datetime.timedelta]):
"""
Class that maps to timedelta using strings in any of the following forms:
Expand Down
124 changes: 124 additions & 0 deletions test/optional_parameter_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import datetime
import warnings

import mock
from helpers import LuigiTestCase, with_config

import luigi
from luigi import date_interval


class OptionalParameterTest(LuigiTestCase):
Expand Down Expand Up @@ -98,6 +100,128 @@ def test_optional_choice_parameter(self):
self.actual_test(luigi.OptionalChoiceParameter, None, "expected value", "str", "bad data", choices=choices)
self.actual_test(luigi.OptionalChoiceParameter, "default value", "expected value", "str", "bad data", choices=choices)

def _test_optional_date_like(self, cls, config_key, config_value, expected_value, default_value):
"""Dedicated helper for date/datetime optional parameters.

Unlike actual_test, this skips the bad-data warning check because
date/datetime normalize() crashes on non-date types."""
param = cls()

# parse("") → None
self.assertIsNone(param.parse(""))

# parse(valid_string) → expected
self.assertEqual(param.parse(config_value), expected_value)

# serialize(None) → ""
self.assertEqual(param.serialize(None), "")

# serialize(value) → string
self.assertEqual(param.serialize(expected_value), config_value)

# next_in_enumeration → None
self.assertIsNone(param.next_in_enumeration(expected_value))
self.assertIsNone(param.next_in_enumeration(None))

# Setting None should not trigger a warning
with mock.patch("luigi.parameter.warnings") as mocked:

@with_config({"Cfg": {config_key: config_value, "empty": ""}})
def inner(self_inner):
class Cfg(luigi.Config):
param = cls()
empty = cls()

def run(inner_self):
self_inner.assertEqual(inner_self.param, expected_value)
self_inner.assertIsNone(inner_self.empty)

Cfg(param=None)
mocked.warn.assert_not_called()
self_inner.assertTrue(luigi.build([Cfg()], local_scheduler=True))

inner(self)

# Should also work with an explicit default value
@with_config({"Cfg2": {config_key: config_value, "empty": ""}})
def inner2(self_inner):
class Cfg2(luigi.Config):
param = cls(default=default_value)
empty = cls(default=default_value)

def run(inner_self):
self_inner.assertEqual(inner_self.param, expected_value)
self_inner.assertIsNone(inner_self.empty)

self_inner.assertTrue(luigi.build([Cfg2()], local_scheduler=True))

inner2(self)

def test_optional_date_parameter(self):
self._test_optional_date_like(
luigi.OptionalDateParameter,
"param",
"2013-07-10",
datetime.date(2013, 7, 10),
datetime.date(2000, 1, 1),
)

def test_optional_month_parameter(self):
self._test_optional_date_like(
luigi.OptionalMonthParameter,
"param",
"2013-07",
datetime.date(2013, 7, 1),
datetime.date(2000, 1, 1),
)

def test_optional_year_parameter(self):
self._test_optional_date_like(
luigi.OptionalYearParameter,
"param",
"2013",
datetime.date(2013, 1, 1),
datetime.date(2000, 1, 1),
)

def test_optional_date_hour_parameter(self):
self._test_optional_date_like(
luigi.OptionalDateHourParameter,
"param",
"2013-07-10T19",
datetime.datetime(2013, 7, 10, 19),
datetime.datetime(2000, 1, 1, 0),
)

def test_optional_date_minute_parameter(self):
self._test_optional_date_like(
luigi.OptionalDateMinuteParameter,
"param",
"2013-07-10T1907",
datetime.datetime(2013, 7, 10, 19, 7),
datetime.datetime(2000, 1, 1, 0, 0),
)

def test_optional_date_second_parameter(self):
self._test_optional_date_like(
luigi.OptionalDateSecondParameter,
"param",
"2013-07-10T190738",
datetime.datetime(2013, 7, 10, 19, 7, 38),
datetime.datetime(2000, 1, 1, 0, 0, 0),
)

def test_optional_date_interval_parameter(self):
expected = date_interval.Date.parse("2015-11-04")
default_val = date_interval.Date.parse("2000-01-01")
self._test_optional_date_like(
luigi.OptionalDateIntervalParameter,
"param",
"2015-11-04",
expected,
default_val,
)

@with_config({"TestConfig": {"param": "1", "empty_param": ""}})
def test_optional_choice_parameter_int(self):
choices = [0, 1, 2]
Expand Down
Loading