[Git][debian-gis-team/cftime][upstream] New upstream version 1.5.1+ds

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Sat Sep 25 05:34:11 BST 2021



Bas Couwenberg pushed to branch upstream at Debian GIS Project / cftime


Commits:
238c33a5 by Bas Couwenberg at 2021-09-25T06:24:53+02:00
New upstream version 1.5.1+ds
- - - - -


5 changed files:

- Changelog
- README.md
- docs/index.rst
- src/cftime/_cftime.pyx
- test/test_cftime.py


Changes:

=====================================
Changelog
=====================================
@@ -1,3 +1,21 @@
+version 1.5.1 (not yet released)
+======================================
+ * added support for "common_year" and "common_years" units for "noleap" 
+   and "365_day" calendars (issue #5, PR #246)
+ * check consistency of year arg and has_year_zero kwarg in cftime.datetime
+   (issue #248).  Also assume if has_year_zero not specified it should be True
+   if year=0. Allow replace method to change has_year_zero. Issue UserWarning
+   if year set to zero and calendar default is changed from False to True
+   (so that user is aware the resulting instance will not be CF compliant).
+ * '360_day' was missing from list of 'idealized' calendars.
+ * fixed a bug that led to subclasses losing their type identity upon
+   pickling (issue #251, PR #252).
+ * Change default behavior of proleptic_gregorian to has_year_zero=T
+   (to be consistent with ISO-8601 since CF does not specify the year zero convention
+   for this calendar). Issue warning when trying to
+   to create a cftime.datetime instance that is not allowed in CF (PR #238).
+
+
 version 1.5.0 (release tag v1.5.0.rel)
 ======================================
  * clean-up deprecated calendar specific subclasses (PR #231).
@@ -52,7 +70,7 @@ version 1.3.1 (release tag v1.3.1rel)
 version 1.3.0 (release tag v1.3.0rel)
 =====================================
  * zero pad years in strtime (issue #194)
- * have cftime.datetime constuctor create 'calendar-aware' instances (default is
+ * have cftime.datetime constructor create 'calendar-aware' instances (default is
    'standard' calendar, if calendar='' or None the instance is not calendar aware and some
    methods, like dayofwk, dayofyr, __add__ and __sub__, will not work). Fixes issue #198.
    The calendar specific sub-classes are now deprecated, but remain for now


=====================================
README.md
=====================================
@@ -12,6 +12,14 @@ Time-handling functionality from netcdf4-python
 ## News
 For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog).
 
+10/1/2021:  Version 1.5.1 released. Changed default behavior of ``proleptic_gregorian``
+to has_year_zero=T (since it is allowed in ISO-8601 and CF does not specify the
+year zero convention for this calendar). Raise warning message when trying
+to create a calendar that is not supported by CF version 1.9 (no years < 1
+allowed for 'standard'/'gregorian' or 'julian'  calendars).
+Added support for "common_year" and "common_years" units for "noleap" 
+and "365_day" calendars.
+ 
 5/20/2021:  Version 1.5.0 released.  Includes support for astronomical year numbering
 (including the year zero) for real-world calendars ('julian', 'gregorian'/'standard',
 and 'proleptic_gregorian') using 'has_year_zero' `cftime.datetime` kwarg.


=====================================
docs/index.rst
=====================================
@@ -2,7 +2,7 @@ cftime
 ======
 
 Python library for decoding time units and variable values in a netCDF file
-conforming to the Climate and Forecasting (CF) netCDF conventions.
+conforming to the `Climate and Forecasting (CF) netCDF conventions <http://cfconventions.org/cf-conventions/cf-conventions#time-coordinate>`__.
 
 Contents
 --------


=====================================
src/cftime/_cftime.pyx
=====================================
@@ -22,14 +22,15 @@ min_units =      ['minute', 'minutes', 'min', 'mins']
 hr_units =       ['hour', 'hours', 'hr', 'hrs', 'h']
 day_units =      ['day', 'days', 'd']
 month_units =    ['month', 'months'] # only allowed for 360_day calendar
+year_units =     ['common_year', 'common_years'] # only allowed for 365_day and noleap calendars
 _units = microsec_units+millisec_units+sec_units+min_units+hr_units+day_units
 # supported calendars. Includes synonyms ('standard'=='gregorian',
 # '366_day'=='all_leap','365_day'=='noleap')
-# see http://cfconventions.org/cf-conventions/cf-conventions.html#calendar
+# see http://cfconventions.org/cf-conventions/cf-conventions#calendar
 # for definitions.
 _calendars = ['standard', 'gregorian', 'proleptic_gregorian',
               'noleap', 'julian', 'all_leap', '365_day', '366_day', '360_day']
-_idealized_calendars= ['all_leap','noleap','366_day','365_day']
+_idealized_calendars= ['all_leap','noleap','366_day','365_day','360_day']
 # Following are number of days per month
 cdef int[12] _dayspermonth      = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
 cdef int[12] _dayspermonth_leap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
@@ -37,7 +38,7 @@ cdef int[12] _dayspermonth_leap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 3
 cdef int[13] _cumdayspermonth = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
 cdef int[13] _cumdayspermonth_leap = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
 
-__version__ = '1.5.0'
+__version__ = '1.5.1'
 
 # Adapted from http://delete.me.uk/2005/03/iso8601.html
 # Note: This regex ensures that all ISO8601 timezone formats are accepted - but, due to legacy support for other timestrings, not all incorrect formats can be rejected.
@@ -46,7 +47,7 @@ ISO8601_REGEX = re.compile(r"(?P<year>[+-]?[0-9]+)(-(?P<month>[0-9]{1,2})(-(?P<d
                            r"(((?P<separator1>.)(?P<hour>[0-9]{1,2}):(?P<minute>[0-9]{1,2})(:(?P<second>[0-9]{1,2})(\.(?P<fraction>[0-9]+))?)?)?"
                            r"((?P<separator2>.?)(?P<timezone>Z|(([-+])([0-9]{2})((:([0-9]{2}))|([0-9]{2}))?)))?)?)?)?"
                            )
-# Note: The re module apparently does not support branch reset groups that allow redifinition of the same group name in alternative branches as PCRE does.
+# Note: The re module apparently does not support branch reset groups that allow redefinition of the same group name in alternative branches as PCRE does.
 #       Using two different group names is also somewhat ugly, but other solutions might hugely inflate the expression. feel free to contribute a better solution.
 TIMEZONE_REGEX = re.compile(
     "(?P<prefix>[+-])(?P<hours>[0-9]{2})(?:(?::(?P<minutes1>[0-9]{2}))|(?P<minutes2>[0-9]{2}))?")
@@ -93,19 +94,20 @@ def _dateparse(timestr,calendar,has_year_zero=None):
     if has_year_zero is None:
         has_year_zero = _year_zero_defaults(calendar)
     (units, isostring) = _datesplit(timestr)
-    if not ((units in month_units and calendar=='360_day') or units in _units):
+    if not ((units in month_units and calendar=='360_day') or (units in year_units and calendar in {'365_day', 'noleap'}) or units in _units):
         if units in month_units and calendar != '360_day':
             raise ValueError("'months since' units only allowed for '360_day' calendar")
+        if units in year_units and calendar not in {'365_day', 'noleap'}:    
+            raise ValueError("'%s' units only allowed for '365_day' and 'noleap' calendars" % units) 
         else:
             raise ValueError(
             "units must be one of 'seconds', 'minutes', 'hours' or 'days' (or singular version of these), got '%s'" % units)
     # parse the date string.
     year, month, day, hour, minute, second, microsecond, utc_offset =\
         _parse_date( isostring.strip() )
-    if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
-        if year == 0:
-            msg='zero not allowed as a reference year, does not exist in Julian or Gregorian calendars'
-            raise ValueError(msg)
+    if year == 0 and not has_year_zero and calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
+        msg='zero not allowed as a reference year when has_year_zero=False'
+        raise ValueError(msg)
     if calendar in ['noleap', '365_day'] and month == 2 and day == 29:
         raise ValueError(
             'cannot specify a leap day as the reference time with the noleap calendar')
@@ -142,11 +144,12 @@ def date2num(dates,units,calendar=None,has_year_zero=None):
     **units**: a string of the form **<time units> since <reference time>**
     describing the time units. **<time units>** can be days, hours, minutes,
     seconds, milliseconds or microseconds. **<reference time>** is the time
-    origin. **months_since** is allowed *only* for the **360_day** calendar.
+    origin. **months since** is allowed *only* for the **360_day** calendar
+    and **common_years since** is allowed *only* for the **365_day** calendar.
 
     **calendar**: describes the calendar to be used in the time calculations.
     All the values currently defined in the
-    `CF metadata convention <http://cfconventions.org>`__ are supported.
+    `CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
     Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
     Default is `None` which means the calendar associated with the first
@@ -156,8 +159,13 @@ def date2num(dates,units,calendar=None,has_year_zero=None):
     is used and the year zero exists.  If set to False for real-world
     calendars, then historical year numbering is used and the year 1 is
     preceded by year -1 and no year zero exists.
-    The defaults are False for real-world calendars
-    and True for idealized calendars.
+    The defaults are set to conform with
+    CF version 1.9 conventions (False for 'julian', 'gregorian'/'standard', True
+    for 'proleptic_gregorian' (ISO 8601) and True for the idealized
+    calendars 'noleap'/'365_day', '360_day', 366_day'/'all_leap')
+    Note that CF v1.9 does not specifically mention whether year zero
+    is allowed in the proleptic_gregorian calendar, but ISO-8601 has
+    a year zero so we have adopted this as the default.
     The defaults can only be over-ridden for the real-world calendars,
     for the the idealized calendars the year zero 
     always exists and the has_year_zero kwarg is ignored.
@@ -320,7 +328,10 @@ UNIT_CONVERSION_FACTORS = {
     "days": 86400 * 1000000,
     "d": 86400 * 1000000,
     "month": 30 * 86400 * 1000000,  # Only allowed for 360_day calendar
-    "months": 30 * 86400 * 1000000
+    "months": 30 * 86400 * 1000000,
+    "common_year": 365 * 86400 * 1000000, # Only allowed for 365_day and no_leap calendars
+    "common_years": 365 * 86400 * 1000000 # Only allowed for 365_day and no_leap calendars
+    
 }
 
 DATE_TYPES = {
@@ -433,11 +444,12 @@ def num2date(
     **units**: a string of the form **<time units> since <reference time>**
     describing the time units. **<time units>** can be days, hours, minutes,
     seconds, milliseconds or microseconds. **<reference time>** is the time
-    origin. **months_since** is allowed *only* for the **360_day** calendar.
+    origin. **months since** is allowed *only* for the **360_day** calendar
+    and **common_years since** is allowed *only* for the **365_day** calendar.
 
     **calendar**: describes the calendar used in the time calculations.
     All the values currently defined in the
-    `CF metadata convention <http://cfconventions.org>`__ are supported.
+    `CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
     Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
     Default is **'standard'**, which is a mixed Julian/Gregorian calendar.
@@ -454,8 +466,10 @@ def num2date(
     is used and the year zero exists.  If set to False for real-world
     calendars, then historical year numbering is used and the year 1 is
     preceded by year -1 and no year zero exists.
-    The defaults are False for real-world calendars
-    and True for idealized calendars.
+    The defaults are set to conform with
+    CF version 1.9 conventions (False for 'julian', 'gregorian'/'standard', True
+    for 'proleptic_gregorian' (ISO 8601) and True for the idealized
+    calendars 'noleap'/'365_day', '360_day', 366_day'/'all_leap')
     The defaults can only be over-ridden for the real-world calendars,
     for the the idealized calendars the year zero 
     always exists and the has_year_zero kwarg is ignored.
@@ -499,7 +513,7 @@ def num2date(
 
     use_python_datetime = False
     if only_use_python_datetimes and not only_use_cftime_datetimes:
-        # only_use_cftime_datetimes takes precendence
+        # only_use_cftime_datetimes takes precedence
         use_python_datetime = True
     if not only_use_python_datetimes and not only_use_cftime_datetimes and can_use_python_datetime:
         # if only_use_cftimes_datetimes and only_use_python_datetimes are False
@@ -543,7 +557,7 @@ def date2index(dates, nctime, calendar=None, select='exact', has_year_zero=None)
 
     **calendar**: describes the calendar to be used in the time calculations.
     All the values currently defined in the
-    `CF metadata convention <http://cfconventions.org>`__ are supported.
+    `CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
     Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
     Default is `None` which means the calendar associated with the first
@@ -560,8 +574,10 @@ def date2index(dates, nctime, calendar=None, select='exact', has_year_zero=None)
     is used and the year zero exists.  If set to False for real-world
     calendars, then historical year numbering is used and the year 1 is
     preceded by year -1 and no year zero exists.
-    The defaults are False for real-world calendars
-    and True for idealized calendars.
+    The defaults are set to conform with
+    CF version 1.9 conventions (False for 'julian', 'gregorian'/'standard', True
+    for 'proleptic_gregorian' (ISO 8601) and True for the idealized
+    calendars 'noleap'/'365_day', '360_day', 366_day'/'all_leap')
     The defaults can only be over-ridden for the real-world calendars,
     for the the idealized calendars the year zero 
     always exists and the has_year_zero kwarg is ignored.
@@ -771,8 +787,10 @@ def _date2index(dates, nctime, calendar=None, select='exact', has_year_zero=None
     is used and the year zero exists.  If set to False for real-world
     calendars, then historical year numbering is used and the year 1 is
     preceded by year -1 and no year zero exists.
-    The defaults are False for real-world calendars
-    and True for idealized calendars.
+    The defaults are set to conform with
+    CF version 1.9 conventions (False for 'julian', 'gregorian'/'standard', True
+    for 'proleptic_gregorian' (ISO 8601) and True for the idealized
+    calendars 'noleap'/'365_day', '360_day', 366_day'/'all_leap')
     The defaults can only be over-ridden for the real-world calendars,
     for the the idealized calendars the year zero 
     always exists and the has_year_zero kwarg is ignored.
@@ -913,16 +931,16 @@ cdef _year_zero_defaults(calendar):
     if calendar in ['standard','gregorian','julian']:
        return False
     elif calendar in ['proleptic_gregorian']:
-       #return True # ISO 8601 year zero=1 BC
-       return False
+       return True # ISO 8601 year zero=1 BC
     elif calendar in _idealized_calendars:
        return True
     else:
        return False
 
 # factory function without optional kwargs that can be used in datetime.__reduce__
-def _create_datetime(args, kwargs): return datetime(*args, **kwargs)
+def _create_datetime(date_type, args, kwargs): return date_type(*args, **kwargs)
 # custorm warning for invalid CF dates.
+cfwarnmsg="this date/calendar/year zero convention is not supported by CF"
 class CFWarning(UserWarning):
     pass
 
@@ -940,7 +958,7 @@ for cftime.datetime instances using
 'gregorian' and 'proleptic_gregorian' calendars.
 
 All the calendars currently defined in the
-`CF metadata convention <http://cfconventions.org>`__ are supported.
+`CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
 Valid calendars are 'standard', 'gregorian', 'proleptic_gregorian'
 'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
 Default is 'standard', which is a mixed Julian/Gregorian calendar.
@@ -954,8 +972,10 @@ If the has_year_zero kwarg is set to True, astronomical year numbering
 is used and the year zero exists.  If set to False for real-world
 calendars, then historical year numbering is used and the year 1 is
 preceded by year -1 and no year zero exists.
-The defaults are False for real-world calendars
-and True for idealized calendars.
+The defaults are set to conform with
+CF version 1.9 conventions (False for 'julian', 'gregorian'/'standard', True
+for 'proleptic_gregorian' (ISO 8601) and True for the idealized
+calendars 'noleap'/'365_day', '360_day', 366_day'/'all_leap')
 The defaults can only be over-ridden for the real-world calendars,
 for the the idealized calendars the year zero 
 always exists and the has_year_zero kwarg is ignored.
@@ -1002,13 +1022,30 @@ The default format of the string produced by strftime is controlled by self.form
             calendar = calendar.lower()
         # set calendar-specific defaults for has_year_zero
         if has_year_zero is None:
-            has_year_zero = _year_zero_defaults(calendar)
+            if year == 0:
+                # assume if user sets year to zero, the calendar should
+                # include the year zero (issue #248)
+                # warn if calendar is being set to non-CF calendar
+                msg="year=0 was specified - this date/calendar/year zero convention is not supported by CF"
+                if calendar is not None and not _year_zero_defaults(calendar):
+                    warnings.warn(msg,category=CFWarning)
+                has_year_zero=True
+            else:
+                has_year_zero = _year_zero_defaults(calendar)
+        # warn if requested date not allowed by CF
+        # (no years < 1 in mixed Julian/Gregorian calendar).
+        # CF version 1.9 does not specify whether a year zero should exist
+        # for the proleptic_gregorian calendar. IS0 8601 uses proleptic_gregorian
+        # and has a year zero, so for now this is the default in cftime.
+        if calendar in ['julian','gregorian','standard'] and year <= 0:
+            warnings.warn(cfwarnmsg,category=CFWarning)
+        # raise exception if year zero requested but has_year_zero set
+        # to False (issue #248).
+        if year == 0 and has_year_zero==False:
+            msg='year zero requested, but has_year_zero=False'
+            raise ValueError(msg)
         if not has_year_zero and calendar in _idealized_calendars:
             warnings.warn('has_year_zero kwarg ignored for idealized calendars (always True)')
-        #if (calendar in ['julian','gregorian','standard'] and year <= 0) or\
-        #   (calendar == 'proleptic_gregorian' and not has_year_zero and year < 1):
-        #    msg="this date/calendar/year zero convention is not supported by CF"
-        #    warnings.warn(msg,category=CFWarning)
         self.has_year_zero = has_year_zero
         if calendar == 'gregorian' or calendar == 'standard':
             # dates after 1582-10-15 can be converted to and compared to
@@ -1119,6 +1156,7 @@ The default format of the string produced by strftime is controlled by self.form
                 "minute": self.minute,
                 "second": self.second,
                 "microsecond": self.microsecond,
+                "has_year_zero": self.has_year_zero,
                 "calendar": self.calendar}
 
         if 'dayofyr' in kwargs or 'dayofwk' in kwargs:
@@ -1129,6 +1167,12 @@ The default format of the string produced by strftime is controlled by self.form
             raise ValueError('Replacing the calendar of a datetime is '
                              'not supported.')
 
+        # if attempting to set year to zero, also set has_year_zero=True
+        # (issue #248)
+        if 'year' in kwargs:
+            if kwargs['year']==0 and 'has_year_zero' not in kwargs:
+                kwargs['has_year_zero']=True
+
         for name, value in kwargs.items():
             args[name] = value
 
@@ -1247,7 +1291,8 @@ The default format of the string produced by strftime is controlled by self.form
     def __reduce__(self):
         """special method that allows instance to be pickled"""
         args, kwargs = self._getstate()
-        return (_create_datetime, (args, kwargs))
+        date_type = type(self)
+        return (_create_datetime, (date_type, args, kwargs))
 
     cdef _add_timedelta(self, other):
         return NotImplemented
@@ -1277,9 +1322,9 @@ The default format of the string produced by strftime is controlled by self.form
         else:
             units = 'days since 0-1-1-12'
         # suppress warning about invalid CF date (year <= 0)
-        #with warnings.catch_warnings():
-        #    warnings.simplefilter("ignore",category=CFWarning)
-        jd = num2date(jday,units=units,calendar=calendar,has_year_zero=has_year_zero)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=CFWarning)
+            jd = num2date(jday,units=units,calendar=calendar,has_year_zero=has_year_zero)
         return jd
 
     def toordinal(self,fractional=False):
@@ -1287,7 +1332,7 @@ The default format of the string produced by strftime is controlled by self.form
 
         Day 0 starts at noon January 1 of the year -4713 for the
         julian, gregorian and standard calendars (year -4712 if year
-        zero allowd).
+        zero allowed).
 
         Day 0 starts at noon on November 24 of the year -4714 for the
         proleptic gregorian calendar (year -4713 if year zero allowed).
@@ -1335,7 +1380,7 @@ The default format of the string produced by strftime is controlled by self.form
             delta = self
         else:
             return NotImplemented
-        # return calendar-specific subclasses for backward compatbility,
+        # return calendar-specific subclasses for backward compatibility,
         # even though after 1.3.0 this is no longer necessary.
         if calendar == '360_day':
             #return dt.__class__(*add_timedelta_360_day(dt, delta),calendar=calendar)
@@ -1391,7 +1436,7 @@ datetime object."""
                 return dt._to_real_datetime() - other
             elif isinstance(other, timedelta):
                 # datetime - timedelta
-                # return calendar-specific subclasses for backward compatbility,
+                # return calendar-specific subclasses for backward compatibility,
                 # even though after 1.3.0 this is no longer necessary.
                 has_year_zero=self.has_year_zero
                 if self.calendar == '360_day':
@@ -1754,8 +1799,10 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
     is used and the year zero exists.  If set to False for real-world
     calendars, then historical year numbering is used and the year 1 is
     preceded by year -1 and no year zero exists.
-    The defaults are False for real-world calendars
-    and True for idealized calendars.
+    The defaults are set to conform with
+    CF version 1.9 conventions (False for 'julian', 'gregorian'/'standard', True
+    for 'proleptic_gregorian' (ISO 8601) and True for the idealized
+    calendars 'noleap'/'365_day', '360_day', 366_day'/'all_leap')
     The defaults can only be over-ridden for the real-world calendars,
     for the the idealized calendars the year zero 
     always exists and the has_year_zero kwarg is ignored.


=====================================
test/test_cftime.py
=====================================
@@ -167,7 +167,7 @@ class cftimeTestCase(unittest.TestCase):
         # check num2date method.
         d2 = self.cdftime_mixed.num2date(t1)
         self.assertTrue(str(d) == str(d2))
-        # this is a non-existant date, should raise ValueError.
+        # this is a non-existent date, should raise ValueError.
         d = datetime(1582, 10, 5, 0)
         self.assertRaises(ValueError, self.cdftime_mixed.date2num, d)
         # check date2num/num2date with date after switch.
@@ -210,7 +210,7 @@ class cftimeTestCase(unittest.TestCase):
         ndayr = d.timetuple()[7]
         self.assertTrue(ndayr == 125)
         # check noleap calendar.
-        # this is a non-existant date, should raise ValueError.
+        # this is a non-existent date, should raise ValueError.
         self.assertRaises(
             ValueError, utime, 'days since 1600-02-29 00:00:00', calendar='noleap')
         self.assertTrue(self.cdftime_noleap.units == 'days')
@@ -236,7 +236,7 @@ class cftimeTestCase(unittest.TestCase):
         # check day of year.
         ndayr = d2.timetuple()[7]
         self.assertTrue(ndayr == 59)
-        # non-existant date, should raise ValueError.
+        # non-existent date, should raise ValueError.
         date = datetime(2000, 2, 29)
         self.assertRaises(ValueError, self.cdftime_noleap.date2num, date)
         # check all_leap calendar.
@@ -407,7 +407,7 @@ class cftimeTestCase(unittest.TestCase):
         units = 'hours since 2013-12-12T12:00:00'
         assert(1.0 == date2num(num2date(1.0, units), units))
 
-        # test rountrip accuracy
+        # test roundtrip accuracy
         # also tests error found in issue #349
         dateref = datetime(2015,2,28,12)
         verbose = True # print out max error diagnostics
@@ -575,7 +575,7 @@ class cftimeTestCase(unittest.TestCase):
         except ValueError:
             pass
         # test fix for issue #596 - julian day calculations wrong for negative years,
-        # caused incorrect rountrip num2date(date2num(date)) roundtrip for dates with year
+        # caused incorrect roundtrip num2date(date2num(date)) roundtrip for dates with year
         # < 0.
         u = utime("seconds since 1-1-1",calendar='julian')
         with warnings.catch_warnings():
@@ -900,6 +900,25 @@ class cftimeTestCase(unittest.TestCase):
                     assert(d.toordinal() == jdref)
                     d2 = cftime.datetime.fromordinal(jd,calendar=calendar,has_year_zero=has_year_zero)
                     assert(d2 == d)
+        # issue #248.  Set has_year_zero=True if year zero requested
+        # on instance creation, or by using replace method.
+        d=cftime.datetime(0, 0, 0, calendar=None)
+        assert(d.has_year_zero==True)
+        d=cftime.datetime(1, 0, 0, calendar=None)
+        assert(d.has_year_zero==False)
+        d = d.replace(year=0)
+        assert(d.has_year_zero==True)
+        # this should raise a warning, since the default has_year_zero
+        # is changed if year specified as zero. (issue #248, PR #249)
+        self.assertWarns(UserWarning, cftime.datetime, 0, 1, 1,\
+                calendar='standard')
+        # check that for idealized calendars has_year_zero is always True
+        d=cftime.datetime(0, 1, 1, calendar='360_day')
+        assert(d.has_year_zero==True)
+        d=cftime.datetime(1, 1, 1, calendar='360_day')
+        assert(d.has_year_zero==True)
+        d = d.replace(year=0)
+        assert(d.has_year_zero==True)
 
 
 class TestDate2index(unittest.TestCase):
@@ -921,7 +940,7 @@ class TestDate2index(unittest.TestCase):
             self.calendar = calendar
             t0 = date2num(start, units, calendar)
             self._data = (t0 + np.arange(n) * step).astype('float')
-            self.dtype = np.float
+            self.dtype = float
 
         def __getitem__(self, item):
             return self._data[item]
@@ -1305,8 +1324,15 @@ class DateTime(unittest.TestCase):
         import pickle
 
         date = Datetime360Day(year=1, month=2, day=3, hour=4, minute=5, second=6, microsecond=7)
-        self.assertEqual(date, pickle.loads(pickle.dumps(date)))
-
+        deserialized = pickle.loads(pickle.dumps(date))
+        self.assertEqual(date, deserialized)
+        self.assertEqual(type(date), type(deserialized))
+
+        date = datetimex(1, 2, 3, 4, 5, 6, 7, calendar="360_day")
+        deserialized = pickle.loads(pickle.dumps(date))
+        self.assertEqual(date, deserialized)
+        self.assertEqual(type(date), type(deserialized))
+        
     def test_misc(self):
         "Miscellaneous tests."
         # make sure repr succeeds
@@ -1319,7 +1345,7 @@ class DateTime(unittest.TestCase):
         def invalid_year():
             with warnings.catch_warnings():
                 warnings.simplefilter("ignore",category=cftime.CFWarning)
-                DatetimeGregorian(0, 1, 1) + self.delta
+                DatetimeGregorian(0, 1, 1, has_year_zero=False) + self.delta
 
         def invalid_month():
             DatetimeGregorian(1, 13, 1) + self.delta
@@ -1372,67 +1398,6 @@ class DateTime(unittest.TestCase):
         for func in [not_comparable_1, not_comparable_2, not_comparable_3, not_comparable_4, not_comparable_5]:
             self.assertRaises(TypeError, func)
 
-    @pytest.mark.skipif(sys.version_info.major != 2,
-                        reason='python2 specific, non-comparable test')
-    def test_richcmp_py2(self):
-        class Rich(object):
-            """Dummy class with traditional rich comparison support."""
-            def __lt__(self, other):
-                raise NotImplementedError('__lt__')
-            def __le__(self, other):
-                raise NotImplementedError('__le__')
-            def __eq__(self, other):
-                raise NotImplementedError('__eq__')
-            def __ne__(self, other):
-                raise NotImplementedError('__ne__')
-            def __gt__(self, other):
-                raise NotImplementedError('__gt__')
-            def __ge__(self, other):
-                raise NotImplementedError('__ge__')
-
-        class CythonRich(object):
-            """Dummy class with spoof cython rich comparison support."""
-            def __richcmp__(self, other):
-                """
-                This method is never called. However it is introspected
-                by the cftime.datetime.__richcmp__ method, which will then
-                return NotImplemented, causing Python to call this classes
-                __cmp__ method as a back-stop, and hence spoofing the
-                cython specific rich comparison behaviour.
-                """
-                pass
-            def __cmp__(self, other):
-                raise NotImplementedError('__richcmp__')
-
-        class Pass(object):
-            """Dummy class with no rich comparison support whatsoever."""
-            pass
-
-        class Pass___cmp__(object):
-            """Dummy class that delegates all comparisons."""
-            def __cmp__(self, other):
-                return NotImplemented
-
-        # Test LHS operand comparison operator processing.
-        for op, expected in [(operator.gt, '__lt__'), (operator.ge, '__le__'),
-                             (operator.eq, '__eq__'), (operator.ne, '__ne__'),
-                             (operator.lt, '__gt__'), (operator.le, '__ge__')]:
-            with self.assertRaisesRegex(NotImplementedError, expected):
-                op(self.date1_365_day, Rich())
-
-            with self.assertRaisesRegex(NotImplementedError, '__richcmp__'):
-                op(self.date1_365_day, CythonRich())
-
-        # Test RHS operand comparison operator processing.
-        for op in [operator.gt, operator.ge, operator.eq, operator.ne,
-                   operator.lt, operator.le]:
-            with self.assertRaisesRegex(TypeError, 'cannot compare'):
-                op(Pass(), self.date1_365_day)
-
-            with self.assertRaisesRegex(TypeError, 'cannot compare'):
-                op(Pass___cmp__(), self.date1_365_day)
-
-
 class issue17TestCase(unittest.TestCase):
     """Regression tests for issue #17/#669."""
     # issue 17 / 699: timezone formats not supported correctly
@@ -1529,13 +1494,14 @@ def test_zero_year(date_type):
     # Proleptic Gregorian calendars by default.
     with warnings.catch_warnings():
         warnings.simplefilter("ignore",category=cftime.CFWarning)
-        #if date_type in [DatetimeNoLeap, DatetimeAllLeap, Datetime360Day,
-        #        DatetimeProlepticGregorian]:
-        if date_type in [DatetimeNoLeap, DatetimeAllLeap, Datetime360Day]: 
+        if date_type in [DatetimeNoLeap, DatetimeAllLeap, Datetime360Day,
+                DatetimeProlepticGregorian]:
             date_type(0, 1, 1)
         else:
+            d=date_type(0,1,1) # has_year_zero=True set if year 0 specified
+            assert(d.has_year_zero) # (issue #248) 
             with pytest.raises(ValueError):
-                date_type(0, 1, 1)
+                date_type(0, 1, 1, has_year_zero=False)
 
 
 def test_invalid_month(date_type):
@@ -1755,6 +1721,7 @@ _MINUTE_UNITS = ["minutes", "minute", "min", "mins"]
 _HOUR_UNITS = ["hours", "hour", "hr", "hrs", "h"]
 _DAY_UNITS = ["day", "days", "d"]
 _MONTH_UNITS = ["month", "months"]
+_YEAR_UNITS = ["common_years", "common_year"]
 _DTYPES = [np.dtype("int64"), np.dtype("float64")]
 _STANDARD_CALENDARS = [
     "standard",
@@ -1767,6 +1734,11 @@ _REAL_WORLD_CALENDARS = [
     "gregorian",
     "proleptic_gregorian"
 ]
+_NO_YEAR_ZERO_CALENDARS = [
+    "julian",
+    "standard",
+    "gregorian",
+]
 _ARTIFICIAL_CALENDARS = ["noleap", "all_leap", "360_day"]
 
 
@@ -1880,6 +1852,23 @@ def test_num2date_month_units(calendar, unit, shape, dtype):
         result = num2date(numeric_times, units=units, calendar=calendar)
         np.testing.assert_equal(result, expected)
 
+ at pytest.mark.parametrize("unit", _YEAR_UNITS)
+def test_num2date_year_units(calendar, unit, shape, dtype):
+    date_type = _EXPECTED_DATE_TYPES[calendar]
+    expected = np.array([date_type(2001, 1, 1, 0, 0, 0, 0),
+                         date_type(2002, 1, 1, 0, 0, 0, 0),
+                         date_type(2003, 1, 1, 0, 0, 0, 0),
+                         date_type(2004, 1, 1, 0, 0, 0, 0)]).reshape(shape)
+    numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype)
+    units = "{} since 2000-01-01".format(unit)
+
+    if calendar not in {"365_day", "noleap"}:
+        with pytest.raises(ValueError):
+            num2date(numeric_times, units=units, calendar=calendar)
+    else:
+        result = num2date(numeric_times, units=units, calendar=calendar)
+        np.testing.assert_equal(result, expected)
+
 
 def test_num2date_only_use_python_datetimes(calendar, shape, dtype):
     date_type = real_datetime
@@ -1941,7 +1930,7 @@ def test_num2date_only_use_python_datetimes_invalid_basedate(
         )
 
 
- at pytest.mark.parametrize("real_world_calendar", _REAL_WORLD_CALENDARS)
+ at pytest.mark.parametrize("real_world_calendar", _NO_YEAR_ZERO_CALENDARS)
 def test_num2date_invalid_zero_reference_year(real_world_calendar):
     units = "days since 0000-01-01"
     numeric_times = np.array([1, 2, 3, 4])



View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/commit/238c33a53d529ef623a5a3b6f4158c57e66ed52c

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/commit/238c33a53d529ef623a5a3b6f4158c57e66ed52c
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20210925/7c41a72a/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list