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)
```

Comments:

Patch:
diff --git a/.travis.yml b/.travis.yml
index f829e95..416d131 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,15 @@
 language: python
 
+dist: xenial
+
 python:
-  - 2.6
   - 2.7
-  - 3.3
   - 3.4
   - 3.5
   - 3.6
+  - 3.7
+  - 3.8-dev
 
-script: py.test --cov=fiscalyear
+script: pytest --cov=fiscalyear
 
 after_success: codecov
diff --git a/LICENSE b/LICENSE
index 247c0b8..f2c31c2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2017 Adam J. Stewart
+Copyright (c) 2017-2019 Adam J. Stewart
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.rst b/README.rst
index b7d0874..30cf856 100644
--- a/README.rst
+++ b/README.rst
@@ -7,6 +7,12 @@
 .. image:: https://readthedocs.org/projects/fiscalyear/badge/?version=latest
    :target: https://fiscalyear.readthedocs.io
 
+.. image:: https://badge.fury.io/py/fiscalyear.svg
+   :target: https://pypi.org/project/fiscalyear/
+
+.. image:: https://anaconda.org/conda-forge/fiscalyear/badges/version.svg
+   :target: https://anaconda.org/conda-forge/fiscalyear
+
 
 Overview
 ========
diff --git a/docs/installation.rst b/docs/installation.rst
index 8355ecf..41a9b4c 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -17,6 +17,16 @@ The recommended way to install ``fiscalyear`` is with ``pip``.
 The fiscalyear module will now appear with your base Python installation.
 
 
+Anaconda
+--------
+
+You can also install ``fiscalyear`` with the ``conda`` package manager.
+
+.. code-block:: console
+
+   $ conda install -c conda-forge fiscalyear
+
+
 Spack
 -----
 
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'" % (
diff --git a/setup.cfg b/setup.cfg
index 4bca33d..d928416 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,3 +4,6 @@ test=pytest
 [build_sphinx]
 source-dir=docs
 build-dir=docs/_build
+
+[metadata]
+license-file = LICENSE
diff --git a/setup.py b/setup.py
index 7939d81..05153b5 100755
--- a/setup.py
+++ b/setup.py
@@ -42,6 +42,8 @@ setuptools.setup(
         'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
 
         # Type of package
         'Topic :: Office/Business :: Financial :: Accounting',


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