Repository: adamjstewart/fiscalyear

Issue:
Add a function for easily changing the global "START_*" parameters
I think it would make sense to have a function that would make changing the global parameters easier. E.g.

``` python
def setup_fiscal_year(start_year, start_month, start_day):
    global START_YEAR, START_MONTH, START_DAY
    START_YEAR = start_year
    START_MONTH = start_month
    START_DAY = start_day

def test_setup_fiscal_year():
    # test defaults
    day =  fiscalyear.FiscalDate(2017, 12, 1)
    assert day.fiscal_year == 2018
    assert day.quarter == 1

    # change fiscal year settings
    fiscalyear.setup_fiscal_year("same", 1, 1)
    assert day.fiscal_year == 2017
    assert day.quarter == 4

    # restore defaults and re-test
    fiscalyear.setup_fiscal_year("previous", 10, 1)
    assert day.fiscal_year == 2018
    assert day.quarter == 1
```
This could also make it possible to change the Fiscal Year settings even if you don't import the whole module. E.g.

``` python
In [4]: from fiscalyear import FiscalQuarter, setup_fiscal_year

In [5]: quarter = FiscalQuarter(2018, 1)

In [6]: quarter.start
Out[6]: FiscalDateTime(2017, 10, 1, 0, 0)

In [7]: setup_fiscal_year('same', 1, 1)

In [8]: quarter.start
Out[8]: FiscalDateTime(2018, 1, 1, 0, 0)
```

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/adamjstewart/fiscalyear/pull/5?src=pr&el=h1) Report
> Merging [#5](https://codecov.io/gh/adamjstewart/fiscalyear/pull/5?src=pr&el=desc) into [master](https://codecov.io/gh/adamjstewart/fiscalyear/commit/77c5c0c82a62de36e77284e924f744bb1e770a31?src=pr&el=desc) will **decrease** coverage by `0.43%`.
> The diff coverage is `93.75%`.

[![Impacted file tree graph](https://codecov.io/gh/adamjstewart/fiscalyear/pull/5/graphs/tree.svg?width=650&src=pr&token=bzOeexdvrN&height=150)](https://codecov.io/gh/adamjstewart/fiscalyear/pull/5?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master       #5      +/-   ##
==========================================
- Coverage     100%   99.56%   -0.44%     
==========================================
  Files           1        1              
  Lines         233      229       -4     
==========================================
- Hits          233      228       -5     
- Misses          0        1       +1
```


| [Impacted Files](https://codecov.io/gh/adamjstewart/fiscalyear/pull/5?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [fiscalyear.py](https://codecov.io/gh/adamjstewart/fiscalyear/pull/5/diff?src=pr&el=tree#diff-ZmlzY2FseWVhci5weQ==) | `99.56% <93.75%> (-0.44%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/adamjstewart/fiscalyear/pull/5?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/adamjstewart/fiscalyear/pull/5?src=pr&el=footer). Last update [77c5c0c...d01eec3](https://codecov.io/gh/adamjstewart/fiscalyear/pull/5?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/fiscalyear.py b/fiscalyear.py
index c6f8ddc..f3a7640 100644
--- a/fiscalyear.py
+++ b/fiscalyear.py
@@ -24,10 +24,41 @@ START_MONTH = 10
 START_DAY = 1
 
 
+def _validate_fiscal_calendar_params(start_year, start_month, start_day):
+    """Raise an Exception if the calendar parameters are invalid.
+
+    :param start_year: Relationship between the start of the fiscal year and
+        the calendar year. Possible values: ``'previous'`` or ``'same'``.
+    :type start_year: str
+    :param start_month: The first month of the fiscal year
+    :type start_month: int or str
+    :param start_day: The first day of the first month of the fiscal year
+    :type start_day: int or str
+    :raises TypeError: If ``start_year`` is not a ``str``.
+    :raises ValueError: If ``start_year`` is not ``'previous'`` or ``'same'``
+    :raises ValueError: If ``start_month`` or ``start_day`` is not an int or
+        int-like string
+    :raises ValueError: If ``start_month`` or ``start_day`` is out of range
+    """
+    if not isinstance(start_year, str):
+        raise TypeError("'start_year' must be a 'str', not: '%s'" % type(str))
+    if start_year not in ('previous',  'same'):
+        msg = "'start_year' must be either 'previous' or 'same', not: '%s'"
+        raise ValueError(msg % start_year)
+    _check_day(start_month, start_day)
+
+
+def setup_fiscal_calendar(start_year, start_month, start_day):
+    """Change the global calendar settings."""
+    _validate_fiscal_calendar_params(start_year, start_month, start_day)
+    global START_YEAR, START_MONTH, START_DAY
+    START_YEAR = start_year
+    START_MONTH = start_month
+    START_DAY = start_day
+
+
 @contextlib.contextmanager
-def fiscal_calendar(start_year=None,
-                    start_month=None,
-                    start_day=None):
+def fiscal_calendar(start_year=None, start_month=None, start_day=None):
     """A context manager that lets you modify the start of the fiscal calendar
     inside the scope of a with-statement.
 
@@ -38,43 +69,22 @@ def fiscal_calendar(start_year=None,
     :type start_month: int or str
     :param start_day: The first day of the first month of the fiscal year
     :type start_day: int or str
-    :raises AssertionError: If ``start_year`` is not ``'previous'`` or ``'same'``
-    :raises TypeError: If ``start_month`` or ``start_day`` is not an int or int-like string
+    :raises ValueError: If ``start_year`` is not ``'previous'`` or ``'same'``
+    :raises TypeError: If ``start_month`` or ``start_day`` is not an int or
+        int-like string
     :raises ValueError: If ``start_month`` or ``start_day`` is out of range
     """
-    global START_YEAR
-    global START_MONTH
-    global START_DAY
-
-    # Use default values if not changed
-    if start_year is None:
-        start_year = START_YEAR
-    if start_month is None:
-        start_month = START_MONTH
-    if start_day is None:
-        start_day = START_DAY
-
-    assert isinstance(start_year, str)
-    assert start_year == 'previous' or start_year == 'same'
-    start_month = _check_month(start_month)
-    start_day = _check_day(start_month, start_day)
-
-    # Backup previous values
-    old_start_year = START_YEAR
-    old_start_month = START_MONTH
-    old_start_day = START_DAY
+    # If arguments are omitted, use the currently active values.
+    start_year = START_YEAR if start_year is None else start_year
+    start_month = START_MONTH if start_month is None else start_month
+    start_day = START_DAY if start_day is None else start_day
 
     # Temporarily change global variables
-    START_YEAR = start_year
-    START_MONTH = start_month
-    START_DAY = start_day
-
+    previous_values = (START_YEAR, START_MONTH, START_DAY)
+    setup_fiscal_calendar(start_year, start_month, start_day)
     yield
-
     # Restore previous values
-    START_YEAR = old_start_year
-    START_MONTH = old_start_month
-    START_DAY = old_start_day
+    setup_fiscal_calendar(*previous_values)
 
 
 def _check_int(value):
@@ -225,11 +235,9 @@ class FiscalYear(object):
             return self == item
         elif isinstance(item, FiscalQuarter):
             return self._fiscal_year == item.fiscal_year
-        elif (isinstance(item, FiscalDateTime) or
-              isinstance(item, datetime.datetime)):
+        elif isinstance(item, datetime.datetime):
             return self.start <= item <= self.end
-        elif (isinstance(item, FiscalDate) or
-              isinstance(item, datetime.date)):
+        elif isinstance(item, datetime.date):
             return self.start.date() <= item <= self.end.date()
         else:
             raise TypeError("can't compare '%s' to '%s'" % (
@@ -405,11 +413,9 @@ class FiscalQuarter(object):
         """
         if isinstance(item, FiscalQuarter):
             return self == item
-        elif (isinstance(item, FiscalDateTime) or
-              isinstance(item, datetime.datetime)):
+        elif isinstance(item, datetime.datetime):
             return self.start <= item <= self.end
-        elif (isinstance(item, FiscalDate) or
-              isinstance(item, datetime.date)):
+        elif isinstance(item, datetime.date):
             return self.start.date() <= item <= self.end.date()
         else:
             raise TypeError("can't compare '%s' to '%s'" % (


Test patch:
diff --git a/test_fiscalyear.py b/test_fiscalyear.py
index 690835f..5b2779f 100644
--- a/test_fiscalyear.py
+++ b/test_fiscalyear.py
@@ -9,8 +9,143 @@ import pytest
 US_FEDERAL = ('previous', 10, 1)
 UK_PERSONAL = ('same', 4, 6)
 
-# Default to U.S.
-fiscalyear.START_YEAR, fiscalyear.START_MONTH, fiscalyear.START_DAY = US_FEDERAL
+
+class TestCheckInt(object):
+    @pytest.mark.parametrize("value, exception", [
+        ('asdf', TypeError),
+        ("-999", TypeError),
+        # Technically speaking, _check_int should accept negative integers
+        # but this isn't a public function + datetime doesn't handle them
+        # anyway.
+        (float(), TypeError),
+        (object(), TypeError),
+    ])
+    def test_invalid_input(self, value, exception):
+        with pytest.raises(exception):
+            fiscalyear._check_int(value)
+
+    @pytest.mark.parametrize("value", [1, 2, 0, -1, -2, "1", "0", "999"])
+    def test_valid_input(self, value):
+        assert int(value) == fiscalyear._check_int(value)
+
+
+class TestCheckYear(object):
+    @pytest.mark.parametrize("value, exception", [
+        ('asdf', TypeError),
+        (float(), TypeError),
+        (object(), TypeError),
+        ("-1", TypeError),
+        (-1, ValueError),
+        (0, ValueError),
+        ("0", ValueError),
+        (10000, ValueError),
+        ("10000", ValueError),
+    ])
+    def test_invalid_input(self, value, exception):
+        with pytest.raises(exception):
+            fiscalyear._check_year(value)
+
+    @pytest.mark.parametrize("value", [1, 2, "1", "999"])
+    def test_valid_input(self, value):
+        assert int(value) == fiscalyear._check_year(value)
+
+
+class TestCheckDay(object):
+    @pytest.mark.parametrize("month, day, exception", [
+        (1, 'asdf', TypeError),
+        (1, "-999", TypeError),
+        (1, float(), TypeError),
+        (1, object(), TypeError),
+        (1, -1, ValueError),
+        (1, "-1", TypeError),
+        (1, 0, ValueError),
+        (1, "0", ValueError),
+        (1, 32, ValueError),
+        (1, 32, ValueError),
+    ])
+    def test_invalid_input(self, month, day, exception):
+        with pytest.raises(exception):
+            fiscalyear._check_day(month, day)
+
+    @pytest.mark.parametrize("month, day", [(1, 1), (1, 2), (1, "1"), (1, 31), (1, "31")])
+    def test_valid_input(self, month, day):
+        assert int(day) == fiscalyear._check_day(month, day)
+
+
+class TestCheckQuarter(object):
+    @pytest.mark.parametrize("value, exception", [
+        ('asdf', TypeError),
+        (float(), TypeError),
+        (object(), TypeError),
+        ("-1", TypeError),
+        (-1, ValueError),
+        (0, ValueError),
+        ("0", ValueError),
+        (5, ValueError),
+        ("5", ValueError),
+    ])
+    def test_invalid_input(self, value, exception):
+        with pytest.raises(exception):
+            fiscalyear._check_quarter(value)
+
+    @pytest.mark.parametrize("value", [1, 2, "1", "4"])
+    def test_valid_input(self, value):
+        assert int(value) == fiscalyear._check_quarter(value)
+
+
+class TestCalendarSettingsValidator(object):
+    @pytest.mark.parametrize("arguments, exception", [
+        (dict(start_year='asdf', start_month=12, start_day=1), ValueError),
+        (dict(start_year=float(1999), start_month=12, start_day=1), TypeError),
+        (dict(start_year=object(), start_month=12, start_day=1), TypeError),
+
+        (dict(start_year='same', start_month='asdf', start_day=1), TypeError),
+        (dict(start_year='same', start_month=float(12), start_day=1), TypeError),
+        (dict(start_year='same', start_month=object(), start_day=1), TypeError),
+        (dict(start_year='same', start_month=-1, start_day=1), ValueError),
+        (dict(start_year='same', start_month=0, start_day=1), ValueError),
+        (dict(start_year='same', start_month=13, start_day=1), ValueError),
+
+        (dict(start_year='same', start_month=12, start_day='asdf'), TypeError),
+        (dict(start_year='same', start_month=12, start_day=float(1)), TypeError),
+        (dict(start_year='same', start_month=12, start_day=object()), TypeError),
+        (dict(start_year='same', start_month=12, start_day=0), ValueError),
+        (dict(start_year='same', start_month=12, start_day=-1), ValueError),
+        (dict(start_year='same', start_month=12, start_day=32), ValueError),
+    ])
+    def test_invalid_input(self, arguments, exception):
+        with pytest.raises(exception):
+            fiscalyear._validate_fiscal_calendar_params(**arguments)
+
+    @pytest.mark.parametrize("arguments", [
+        dict(start_year='same', start_month=1, start_day=1),
+        dict(start_year='same', start_month=1, start_day=31),
+        dict(start_year='same', start_month=12, start_day=1),
+        dict(start_year='previous', start_month=1, start_day=1),
+        dict(start_year='previous', start_month=1, start_day=31),
+        dict(start_year='previous', start_month=12, start_day=1),
+    ])
+    def test_valid_input(self, arguments):
+        fiscalyear._validate_fiscal_calendar_params(**arguments)
+
+
+class TestSetupFiscalCalendar(object):
+
+    def test_setup_fiscal_calendar(self):
+        # Test defaults
+        day =  fiscalyear.FiscalDate(2017, 12, 1)
+        assert day.fiscal_year == 2018
+        assert day.quarter == 1
+
+        # Change fiscal year settings
+        fiscalyear.setup_fiscal_calendar("same", 1, 1)
+        assert day.fiscal_year == 2017
+        assert day.quarter == 4
+
+        # Restore defaults and re-test
+        fiscalyear.setup_fiscal_calendar("previous", 10, 1)
+        assert day.fiscal_year == 2018
+        assert day.quarter == 1
 
 
 class TestFiscalCalendar:
@@ -86,6 +221,10 @@ class TestFiscalCalendar:
         assert fiscalyear.START_DAY == 1
 
     def test_wrong_type(self):
+        with pytest.raises(TypeError):
+            with fiscalyear.fiscal_calendar(start_year=6.5):
+                pass
+
         with pytest.raises(TypeError):
             with fiscalyear.fiscal_calendar(start_month=6.5):
                 pass