Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-cftime for openSUSE:Factory checked in at 2022-10-29 20:17:34 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-cftime (Old) and /work/SRC/openSUSE:Factory/.python-cftime.new.2275 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-cftime" Sat Oct 29 20:17:34 2022 rev:8 rq:1032181 version:1.6.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-cftime/python-cftime.changes 2022-09-27 20:14:04.201845455 +0200 +++ /work/SRC/openSUSE:Factory/.python-cftime.new.2275/python-cftime.changes 2022-10-29 20:18:42.114720600 +0200 @@ -1,0 +2,12 @@ +Thu Oct 27 22:15:54 UTC 2022 - Yogalakshmi Arunachalam <yarunacha...@suse.com> + +- Update to version 1.6.2 + * num2date should not fail on an empty integer array (issue #287). + * longdouble keyword in date2num so that a roundtrip from a time to a date + and back again does not lose microsecond precision when the units require + the times be encoded as floating point values (PR #284) + * added strptime method (issue #277). + * cibuildwheel wheel-building workflow added to github actions by @ocefpaf (triggers binary + wheel builds and uploads to pypi automatically when GH release created). PR #290. + +------------------------------------------------------------------- Old: ---- cftime-1.6.1.tar.gz New: ---- cftime-1.6.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-cftime.spec ++++++ --- /var/tmp/diff_new_pack.gqDpOe/_old 2022-10-29 20:18:42.558722966 +0200 +++ /var/tmp/diff_new_pack.gqDpOe/_new 2022-10-29 20:18:42.566723008 +0200 @@ -20,7 +20,7 @@ # no numpy for Python 3.6 %define skip_python36 1 Name: python-cftime -Version: 1.6.1 +Version: 1.6.2 Release: 0 Summary: Time-handling functionality from netcdf4-python License: MIT ++++++ cftime-1.6.1.tar.gz -> cftime-1.6.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cftime-1.6.1/Changelog new/cftime-1.6.2/Changelog --- old/cftime-1.6.1/Changelog 2022-06-30 16:34:49.000000000 +0200 +++ new/cftime-1.6.2/Changelog 2022-09-18 17:18:51.000000000 +0200 @@ -1,3 +1,13 @@ +version 1.6.2 (release tag v1.6.2rel) +===================================== + * num2date should not fail on an empty integer array (issue #287). + * longdouble keyword in date2num so that a roundtrip from a time to a date + and back again does not lose microsecond precision when the units require + the times be encoded as floating point values (PR #284) + * added strptime method (issue #277). + * cibuildwheel wheel-building workflow added to github actions by @ocefpaf (triggers binary + wheel builds and uploads to pypi automatically when GH release created). PR #290. + version 1.6.1 (release tag v1.6.1rel) ===================================== * fix failing tests on windows with numpy 1.23.0 (issue #278) @@ -13,18 +23,18 @@ version 1.5.2 (release tag v1.5.2rel) ===================================== - * silently change calendar='gregorian' to 'standard' internally, + * silently change calendar='gregorian' to 'standard' internally, since 'gregorian' deprecated in CF v1.9 (issue #256). * add "is_leap_year" function (issue #259). * wheels that work on Apple M1 (arm64) available on pypi. - + version 1.5.1.1 =============== * no code changes, just new binary wheels for python 3.10. version 1.5.1 (release tag v1.5.1.rel) ====================================== - * added support for "common_year" and "common_years" units for "noleap" + * 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 @@ -39,7 +49,6 @@ 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). @@ -47,13 +56,13 @@ (via `cftime.datetime.__format__`) PR #232. * add support for astronomical year numbering (including year zero) for real-world calendars using 'has_year_zero' cftime.datetime kwarg (PR #234). - Default is False for 'real-world' calendars ('julian', 'gregorian'/'standard', + Default is False for 'real-world' calendars ('julian', 'gregorian'/'standard', 'proleptic_gregorian'). Ignored for idealized calendars like '360_day (they always have year zero). - * add "change_calendar" cftime.datetime method to switch to another + * add "change_calendar" cftime.datetime method to switch to another 'real-world' calendar. Enable comparison of cftime.datetime instances with different 'real-world' calendars (using the new change_calendar method) - * remove legacy `utime` class, and legacy `JulianDayFromDate` and + * remove legacy `utime` class, and legacy `JulianDayFromDate` and `DateFromJulianDay` functions (replaced by `cftime.datetime.toordinal` and `cftime.datetime.fromordinal`). PR #235. * Change ValueError to TypeError in __sub__ (issue #236, PR #236). @@ -61,7 +70,7 @@ version 1.4.1 (release tag v1.4.1.rel) ====================================== * Restore use of calendar-specific sub-classes in `cftime.num2date`, - `cftime.datetime.__add__`, and `cftime.datetime.__sub__`. The use of them + `cftime.datetime.__add__`, and `cftime.datetime.__sub__`. The use of them will be removed in a later release. * add 'fromordinal' static method to create a cftime.datetime instance from a julian day ordinal and calendar (inverse of 'toordinal'). @@ -72,9 +81,9 @@ and times allow. Previously this would only be true if the units were 'microseconds' (PR #225). In other circumstances, as before, `cftime.date2num` will return an array of floats. - * Rewrite of julian day/calendar functions (_IntJulianDayToCalendar and + * Rewrite of julian day/calendar functions (_IntJulianDayToCalendar and _IntJulianDayFromCalendar) to remove GPL'ed code. cftime license - changed to MIT (to be consistent with netcdf4-python). + changed to MIT (to be consistent with netcdf4-python). * Added datetime.toordinal() (returns julian day, kwarg 'fractional' can be used to include fractional day). * cftime.datetime no longer uses calendar-specific sub-classes. @@ -100,7 +109,7 @@ The calendar specific sub-classes are now deprecated, but remain for now as stubs that just instantiate the base class and override __repr__. * update regex in _cpdef _parse_date so reference years with more than four - digits can be handled. + digits can be handled. * Change default calendar in cftime.date2num from 'standard' to None (calendar associated with first input datetime object is used). * add `cftime.datetime.tzinfo=None` for compatibility with python datetime @@ -131,7 +140,6 @@ * utime.date2num/utime.num2date now just call module level functions. JulianDayFromDate/DateFromJulianDay no longer used internally (PR #180). - version 1.1.3 (release tag v1.1.3rel) ===================================== * add isoformat method for compatibility with python datetime (issue #152). @@ -146,33 +154,27 @@ version 1.1.1.2 (release tag v1.1.1.2rel) ========================================= - * include pyproject.toml in MANIFEST.in so it gets + * include pyproject.toml in MANIFEST.in so it gets included in source tarball (issue #154). version 1.1.1.1 (release tag v1.1.1.1rel) ========================================= - * Fix error installing with pip on python 3.8 by following + * Fix error installing with pip on python 3.8 by following PEP 517 (issue #148, PR #149) version 1.1.1 (release tag v1.1.1rel) ===================================== - * fix microsecond formatting issue, ensure identical results computed for arrays and scales (issue #143, PR #146). version 1.1.0 (release tag v1.1.0rel) ===================================== - * improved exceptions for time differences (issue #128, PR #131). - * fix intersphinx entries (issue #133, PR #133) - * make only_use_cftime_datetimes=True by default, so cftime datetime instances are returned by default by num2date (instead of returning python datetime instances where possible). Issue #136, PR #135. - * Add daysinmonth attribute (issue #137, PR #138). - * If only_use_python_datetimes=True and only_use_cftime_datetimes=False, num2date only returns python datetime instances and raises an exception if this is not possible. num2pydate convenience function added which just calls @@ -180,12 +182,10 @@ only_use_cftime_datetimes=False. Remove positive times check, raise ValueError if python datetime tries to compute a date before MINYEAR (issue #134, PR #139) - * Fix for fractional seconds in reference date in units string (issue #140, PR # 141). version 1.0.4.2 release ======================= - - * fix for issue #126 (date2num error when converting a DatetimeProlepticGregorian + * fix for issue #126 (date2num error when converting a DatetimeProlepticGregorian object). PR #127. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cftime-1.6.1/PKG-INFO new/cftime-1.6.2/PKG-INFO --- old/cftime-1.6.1/PKG-INFO 2022-06-30 16:35:24.000000000 +0200 +++ new/cftime-1.6.2/PKG-INFO 2022-09-18 17:19:23.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: cftime -Version: 1.6.1 +Version: 1.6.2 Summary: Time-handling functionality from netcdf4-python Author: Jeff Whitaker Author-email: jeffrey.s.whita...@noaa.gov @@ -34,6 +34,9 @@ ## News For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog). + +9/18/2022: Version 1.6.2 released. strptime method added, fix for num2date failure on +empty integer array, date2num 'longdouble' keyword added. New wheel building workflow. 6/30/2022: Version 1.6.1 released. Fixes for numpy 1.23.0, updated CI/CD. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cftime-1.6.1/README.md new/cftime-1.6.2/README.md --- old/cftime-1.6.1/README.md 2022-06-30 16:34:49.000000000 +0200 +++ new/cftime-1.6.2/README.md 2022-09-18 17:18:51.000000000 +0200 @@ -11,6 +11,9 @@ ## News For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog). + +9/18/2022: Version 1.6.2 released. strptime method added, fix for num2date failure on +empty integer array, date2num 'longdouble' keyword added. New wheel building workflow. 6/30/2022: Version 1.6.1 released. Fixes for numpy 1.23.0, updated CI/CD. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cftime-1.6.1/src/cftime/_cftime.pyx new/cftime-1.6.2/src/cftime/_cftime.pyx --- old/cftime-1.6.1/src/cftime/_cftime.pyx 2022-06-30 16:34:49.000000000 +0200 +++ new/cftime-1.6.2/src/cftime/_cftime.pyx 2022-09-18 17:18:51.000000000 +0200 @@ -8,12 +8,11 @@ import cython import numpy as np import re -import sys import time from datetime import datetime as datetime_python from datetime import timedelta, MINYEAR, MAXYEAR -import time # strftime import warnings +from ._strptime import _strptime microsec_units = ['microseconds','microsecond', 'microsec', 'microsecs'] millisec_units = ['milliseconds', 'millisecond', 'millisec', 'millisecs', 'msec', 'msecs', 'ms'] @@ -38,7 +37,7 @@ 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.6.1' +__version__ = '1.6.2' # 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. @@ -101,7 +100,11 @@ 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) + "In general, units must be one of 'microseconds', 'milliseconds', " + "'seconds', 'minutes', 'hours', or 'days' (or select abbreviated " + "versions of these). For the '360_day' calendar, " + "'months' can also be used, or for the 'noleap' calendar 'common_years' " + "can also be used. Got '%s' instead, which are not recognized." % units) # parse the date string. year, month, day, hour, minute, second, microsecond, utc_offset =\ _parse_date( isostring.strip() ) @@ -122,12 +125,15 @@ return basedate def _can_use_python_datetime(date,calendar): - gregorian = datetime(1582,10,15,calendar=calendar,has_year_zero=date.has_year_zero) - return ((calendar == 'proleptic_gregorian' and date.year >= MINYEAR and date.year <= MAXYEAR) or \ - (calendar in ['gregorian','standard'] and date > gregorian and date.year <= MAXYEAR)) + #gregorian = datetime(1582,10,15,calendar=calendar,has_year_zero=date.has_year_zero) + #return ((calendar == 'proleptic_gregorian' and date.year >= MINYEAR and date.year <= MAXYEAR) or \ + # (calendar in ['gregorian','standard'] and date > gregorian and date.year <= MAXYEAR)) + return (calendar == 'proleptic_gregorian' and date.year >= MINYEAR and date.year <= MAXYEAR) or \ + ((calendar in ['gregorian','standard'] and date.year <= MAXYEAR) and (date.year > 1582 or \ + (date.year == 1582 and date.month >= 10 and date.day > 15))) @cython.embedsignature(True) -def date2num(dates,units,calendar=None,has_year_zero=None): +def date2num(dates, units, calendar=None, has_year_zero=None, longdouble=False): """ Return numeric time values given datetime objects. The units of the numeric time values are described by the **units** argument @@ -172,6 +178,12 @@ This kwarg is not needed to define calendar systems allowed by CF (the calendar-specific defaults do this). + **longdouble**: If set True, output is in the long double float type + (numpy.float128) instead of float (numpy.float64), allowing microsecond + accuracy when converting a time value to a date and back again. Otherwise + this is only possible if the discretization of the time variable is an + integer multiple of the units. + returns a numeric time value, or an array of numeric time values with approximately 1 microsecond accuracy. """ @@ -215,7 +227,7 @@ has_year_zero = _year_zero_defaults(calendar) # if calendar is None or '', use calendar of first input cftime.datetime instances. - # if inputs are 'real' python datetime instances, use propleptic gregorian. + # if inputs are 'real' python datetime instances, use proleptic gregorian. if not calendar: if all_python_datetimes: calendar = 'proleptic_gregorian' @@ -274,8 +286,25 @@ quotient = np.int64(td // unit_timedelta) times.append(quotient) else: - times.append(td / unit_timedelta) - + if longdouble: + # Division of timedelta's is in float64 precision, + # i.e. losing microsecond precision. + # Conversion to float128 helps but can still lead to imprecision + # of +-1 microsecond in division: + # quotient = (np.longdouble(td.total_seconds()) / + # np.longdouble(unit_timedelta.total_seconds())) + # -> Convert to (64-bit) integers of microseconds + mtd = (td.days * 86400000000 + + td.seconds * 1000000 + + td.microseconds) + munit = (unit_timedelta.days * 86400000000 + + unit_timedelta.seconds * 1000000 + + unit_timedelta.microseconds) + quotient = np.longdouble(mtd) / np.longdouble(munit) + else: + quotient = td / unit_timedelta + times.append(quotient) + if ismasked: # convert to masked array if input was masked array times = np.array(times, dtype=float) # None -> nan times = np.ma.masked_invalid(times) @@ -417,6 +446,8 @@ if num.dtype.kind == "f": return factor * num else: + if num.size == 0: # empty array (issue #287) + return num # Python integers have arbitrary precision, so convert min and max # returned by NumPy functions through item, prior to multiplying by # factor. @@ -666,7 +697,7 @@ has_year_zero = _year_zero_defaults(calendar) # if calendar is None or '', use calendar of first input cftime.datetime instances. - # if inputs are 'real' python datetime instances, use propleptic gregorian. + # if inputs are 'real' python datetime instances, use proleptic gregorian. if not calendar: d0 = dates_test.item(0) if isinstance(d0,datetime_python): @@ -1208,6 +1239,48 @@ format = self.format return _strftime(self, format) + @staticmethod + def strptime(datestring, format, calendar='standard', has_year_zero=None): + """ + Return a datetime corresponding to date_string, parsed according to format, + with a specified calendar and year zero convention. + The format directives 'y','Y','m','B','b','d','H','M','S' and 'f' + are supported for all calendars and dates. If the date is valid + in the python 'proleptic_gregorian' calendar, then python's + datetime.strptime is used. For a complete list of formatting directives + supported in python's datetime.strptime, see section + 'strftime() and strptime() Behavior' in the base Python documentation. + """ + # if possible use python's datetime.strptime to get a python datetime instance + # (works for dates in proleptic_gregorian calendar) + fd = [d[0] for d in format.split('%') if d] # extract format descriptors + # calendar specific format descriptors that won't work will all calendars + special_fd = ['a', 'A', 'w', 'j', 'U', 'W', 'G', 'u', 'V'] + try: + pydatetime = datetime_python.strptime(datestring, format) + # remove time zone offset + if getattr(pydatetime, 'tzinfo',None) is not None: + pydatetime = pydatetime.replace(tzinfo=None) - pydatetime.utcoffset() + compatible_date =\ + calendar == 'proleptic_gregorian' or \ + (calendar in ['gregorian','standard'] and (pydatetime.year > 1582 or \ + (pydatetime.year == 1582 and pydatetime.month > 10) or \ + (pydatetime.year == 1582 and pydatetime.month == 10 and pydatetime.day > 15))) + if not compatible_date and any(x in special_fd for x in fd): + msg='one of the supplied format directives may not be consistent with the chosen calendar' + raise KeyError(msg) + # convert the cftime datetime instance + return datetime(pydatetime.year, pydatetime.month, pydatetime.day, + pydatetime.hour, pydatetime.minute, pydatetime.second, + pydatetime.microsecond, calendar=calendar, has_year_zero=has_year_zero) + # otherwise use a stripped-down version of C-python's _strptime.py + # (doesn't understand all possible formats, just + # 'y','Y','m','B','b','d','H','M','S' and 'f') + except ValueError: + year,month,day,hour,minute,second,microsecond = _strptime(datestring,format) + return datetime(year,month,day,hour,minute,second,microsecond, + calendar=calendar,has_year_zero=has_year_zero) + def __format__(self, format): # the string format "{t_obj}".format(t_obj=t_obj) # without an explicit format gives an empty string (format='') @@ -1280,25 +1353,43 @@ def __str__(self): return self.isoformat(' ') - def isoformat(self,sep='T',timespec='auto'): - second = ":%02i" %self.second - if (timespec == 'auto' and self.microsecond) or timespec == 'microseconds': - second += ".%06i" % self.microsecond - if timespec == 'milliseconds': - millisecs = self.microsecond/1000 - second += ".%03i" % millisecs - if timespec in ['auto', 'microseconds', 'milliseconds']: - return "%04i-%02i-%02i%s%02i:%02i%s" %\ - (self.year, self.month, self.day, sep, self.hour, self.minute, second) - elif timespec == 'seconds': - return "%04i-%02i-%02i%s%02i:%02i:%02i" %\ - (self.year, self.month, self.day, sep, self.hour, self.minute, self.second) - elif timespec == 'minutes': - return "%04i-%02i-%02i%s%02i:%02i" %\ - (self.year, self.month, self.day, sep, self.hour, self.minute) + def isoformat(self, sep='T', timespec='auto'): + """ + ISO date representation + + """ + if self.year < 0: + form0 = '{:05d}-{:02d}-{:02d}' + else: + form0 = '{:04d}-{:02d}-{:02d}' + if timespec == 'days': + form = form0 + return form.format(self.year, self.month, self.day) elif timespec == 'hours': - return "%04i-%02i-%02i%s%02i" %\ - (self.year, self.month, self.day, sep, self.hour) + form = form0 + '{:s}{:02d}' + return form.format(self.year, self.month, self.day, sep, + self.hour) + elif timespec == 'minutes': + form = form0 + '{:s}{:02d}:{:02d}' + return form.format(self.year, self.month, self.day, sep, + self.hour, self.minute) + elif timespec == 'seconds': + form = form0 + '{:s}{:02d}:{:02d}:{:02d}' + return form.format(self.year, self.month, self.day, sep, + self.hour, self.minute, self.second) + elif timespec in ['auto', 'microseconds', 'milliseconds']: + second = '{:02d}'.format(self.second) + if timespec == 'milliseconds': + millisecs = int(round(self.microsecond / 1000, 0)) + second += '.{:03d}'.format(millisecs) + elif timespec == 'microseconds': + second += '.{:06d}'.format(self.microsecond) + else: + if self.microsecond > 0: + second += '.{:06d}'.format(self.microsecond) + form = form0 + '{:s}{:02d}:{:02d}:{:s}' + return form.format(self.year, self.month, self.day, sep, + self.hour, self.minute, second) else: raise ValueError('illegal timespec') @@ -1565,14 +1656,24 @@ # Every 28 years the calendar repeats, except through century leap # years where it's 6 years. But only if you're using the Gregorian # calendar. ;) - - +# Make also 4-digit negative years +# Allow .%f for microseconds cdef _strftime(datetime dt, fmt): if _illegal_s.search(fmt): raise TypeError("This strftime implementation does not handle %s") # don't use strftime method at all. # if dt.year > 1900: # return dt.strftime(fmt) + if '%f' in fmt: + if not fmt.endswith('.%f'): + raise TypeError('If %f is used for microseconds it must be the' + ' at the end as .%f') + else: + ihavems = True + fmt1 = fmt[:-3] + else: + ihavems = False + fmt1 = fmt year = dt.year # For every non-leap year century, advance by @@ -1584,10 +1685,10 @@ # Move to around the year 2000 year = year + ((2000 - year) // 28) * 28 timetuple = dt.timetuple() - s1 = time.strftime(fmt, (year,) + timetuple[1:]) + s1 = time.strftime(fmt1, (year,) + timetuple[1:]) sites1 = _findall(s1, str(year)) - s2 = time.strftime(fmt, (year + 28,) + timetuple[1:]) + s2 = time.strftime(fmt1, (year + 28,) + timetuple[1:]) sites2 = _findall(s2, str(year + 28)) sites = [] @@ -1596,9 +1697,14 @@ sites.append(site) s = s1 - syear = "%04d" % (dt.year,) + if dt.year < 0: + syear = "%05d" % (dt.year,) + else: + syear = "%04d" % (dt.year,) for site in sites: s = s[:site] + syear + s[site + 4:] + if ihavems: + s = s + '.{:06d}'.format(dt.microsecond) return s cdef bint is_leap_julian(int year, bint has_year_zero): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cftime-1.6.1/src/cftime/_strptime.py new/cftime-1.6.2/src/cftime/_strptime.py --- old/cftime-1.6.1/src/cftime/_strptime.py 1970-01-01 01:00:00.000000000 +0100 +++ new/cftime-1.6.2/src/cftime/_strptime.py 2022-09-18 17:18:51.000000000 +0200 @@ -0,0 +1,165 @@ +"""stripped-down version of _strptime.py from C python""" +from re import compile as re_compile +from re import IGNORECASE +from re import escape as re_escape +from _thread import allocate_lock as _thread_allocate_lock +from calendar import month_name, month_abbr + +__all__ = [] +month_name = list(month_name) +month_name = [m.lower() for m in month_name] +month_abbr = list(month_abbr) +month_abbr = [m.lower() for m in month_abbr] + +class TimeRE(dict): + """Handle conversion from format directives to regexes.""" + + def __init__(self): + """Create keys/values. + + Order of execution is important for dependency reasons. + + """ + base = super() + base.__init__({ + # The " [1-9]" part of the regex is to make %c from ANSI C work + 'd': r"(?P<d>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])", + 'f': r"(?P<f>[0-9]{1,6})", + 'H': r"(?P<H>2[0-3]|[0-1]\d|\d)", + 'm': r"(?P<m>1[0-2]|0[1-9]|[1-9])", + 'M': r"(?P<M>[0-5]\d|\d)", + 'S': r"(?P<S>6[0-1]|[0-5]\d|\d)", + 'y': r"(?P<y>\d\d)", +# 'Y': r"(?P<Y>\d\d\d\d)", + 'Y': r"(?P<Y>[+-]?[0-9]+)", # handle neg and > 4 digits + 'B': self.__seqToRE(month_name[1:], 'B'), + 'b': self.__seqToRE(month_abbr[1:], 'b'), + '%': '%'}) + + def __seqToRE(self, to_convert, directive): + """Convert a list to a regex string for matching a directive. + + Want possible matching values to be from longest to shortest. This + prevents the possibility of a match occurring for a value that also + a substring of a larger value that should have matched (e.g., 'abc' + matching when 'abcdef' should have been the match). + + """ + to_convert = sorted(to_convert, key=len, reverse=True) + for value in to_convert: + if value != '': + break + else: + return '' + regex = '|'.join(re_escape(stuff) for stuff in to_convert) + regex = '(?P<%s>%s' % (directive, regex) + return '%s)' % regex + + def pattern(self, format): + """Return regex pattern for the format string. + Need to make sure that any characters that might be interpreted as + regex syntax are escaped. + """ + processed_format = '' + # The sub() call escapes all characters that might be misconstrued + # as regex syntax. Cannot use re.escape since we have to deal with + # format directives (%m, etc.). + regex_chars = re_compile(r"([\\.^$*+?\(\){}\[\]|])") + format = regex_chars.sub(r"\\\1", format) + whitespace_replacement = re_compile(r'\s+') + format = whitespace_replacement.sub(r'\\s+', format) + while '%' in format: + directive_index = format.index('%')+1 + processed_format = "%s%s%s" % (processed_format, + format[:directive_index-1], + self[format[directive_index]]) + format = format[directive_index+1:] + return "%s%s" % (processed_format, format) + + def compile(self, format): + """Return a compiled re object for the format string.""" + return re_compile(self.pattern(format), IGNORECASE) + +_cache_lock = _thread_allocate_lock() +# DO NOT modify _TimeRE_cache or _regex_cache without acquiring the cache lock +# first! +_TimeRE_cache = TimeRE() +_CACHE_MAX_SIZE = 5 # Max number of regexes stored in _regex_cache +_regex_cache = {} + +def _strptime(data_string, format): + """Return a 7-tuple consisting of the data required to construct a + datetime based on the input string and the format string.""" + + for index, arg in enumerate([data_string, format]): + if not isinstance(arg, str): + msg = "strptime() argument {} must be str, not {}" + raise TypeError(msg.format(index, type(arg))) + + global _TimeRE_cache, _regex_cache + with _cache_lock: + if len(_regex_cache) > _CACHE_MAX_SIZE: + _regex_cache.clear() + format_regex = _regex_cache.get(format) + if not format_regex: + try: + format_regex = _TimeRE_cache.compile(format) + # KeyError raised when a bad format is found; can be specified as + # \\, in which case it was a stray % but with a space after it + except KeyError as err: + bad_directive = err.args[0] + if bad_directive == "\\": + bad_directive = "%" + del err + if bad_directive in ['I','a','A','w','j','u','U','V','W','G']: + msg="'%s' directive not supported for dates not valid in python %s calendar" + raise ValueError(msg % + (bad_directive,'proleptic_gregorian')) + else: + raise ValueError("'%s' is a bad directive in format '%s'" % + (bad_directive, format)) from None + # IndexError only occurs when the format string is "%" + except IndexError: + raise ValueError("stray %% in format '%s'" % format) from None + _regex_cache[format] = format_regex + found = format_regex.match(data_string) + if not found: + raise ValueError("time data %r does not match format %r" % + (data_string, format)) + if len(data_string) != found.end(): + raise ValueError("unconverted data remains: %s" % + data_string[found.end():]) + + month = day = 1 + hour = minute = second = fraction = 0 + found_dict = found.groupdict() + for group_key in found_dict.keys(): + if group_key == 'y': + year = int(found_dict['y']) + if year <= 68: + year += 2000 + else: + year += 1900 + elif group_key == 'Y': + year = int(found_dict['Y']) + elif group_key == 'm': + month = int(found_dict['m']) + elif group_key == 'B': + month = month_name.index(found_dict['B'].lower()) + elif group_key == 'b': + month = month_abbr.index(found_dict['b'].lower()) + elif group_key == 'd': + day = int(found_dict['d']) + elif group_key == 'H': + hour = int(found_dict['H']) + elif group_key == 'M': + minute = int(found_dict['M']) + elif group_key == 'S': + second = int(found_dict['S']) + elif group_key == 'f': + s = found_dict['f'] + # Pad to always return microseconds. + s += "0" * (6 - len(s)) + fraction = int(s) + + return year,month,day,hour,minute,second,fraction diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cftime-1.6.1/src/cftime.egg-info/PKG-INFO new/cftime-1.6.2/src/cftime.egg-info/PKG-INFO --- old/cftime-1.6.1/src/cftime.egg-info/PKG-INFO 2022-06-30 16:35:21.000000000 +0200 +++ new/cftime-1.6.2/src/cftime.egg-info/PKG-INFO 2022-09-18 17:19:20.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: cftime -Version: 1.6.1 +Version: 1.6.2 Summary: Time-handling functionality from netcdf4-python Author: Jeff Whitaker Author-email: jeffrey.s.whita...@noaa.gov @@ -34,6 +34,9 @@ ## News For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog). + +9/18/2022: Version 1.6.2 released. strptime method added, fix for num2date failure on +empty integer array, date2num 'longdouble' keyword added. New wheel building workflow. 6/30/2022: Version 1.6.1 released. Fixes for numpy 1.23.0, updated CI/CD. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cftime-1.6.1/src/cftime.egg-info/SOURCES.txt new/cftime-1.6.2/src/cftime.egg-info/SOURCES.txt --- old/cftime-1.6.1/src/cftime.egg-info/SOURCES.txt 2022-06-30 16:35:23.000000000 +0200 +++ new/cftime-1.6.2/src/cftime.egg-info/SOURCES.txt 2022-09-18 17:19:21.000000000 +0200 @@ -9,6 +9,7 @@ setup.py src/cftime/__init__.py src/cftime/_cftime.pyx +src/cftime/_strptime.py src/cftime.egg-info/PKG-INFO src/cftime.egg-info/SOURCES.txt src/cftime.egg-info/dependency_links.txt diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cftime-1.6.1/test/test_cftime.py new/cftime-1.6.2/test/test_cftime.py --- old/cftime-1.6.1/test/test_cftime.py 2022-06-30 16:34:49.000000000 +0200 +++ new/cftime-1.6.2/test/test_cftime.py 2022-09-18 17:18:51.000000000 +0200 @@ -732,7 +732,7 @@ # issue #140 (fractional seconds in reference date) d = datetime.strptime('2018-01-23 09:27:10.950000',"%Y-%m-%d %H:%M:%S.%f") units = 'seconds since 2018-01-23 09:31:42.94' - assert(cftime.date2num(d, units) == -271.99) + assert(float(cftime.date2num(d, units)) == -271.99) # issue 143 - same answer for arrays vs scalars. units = 'seconds since 1970-01-01 00:00:00' times_in = [1261440000.0, 1261440001.0, 1261440002.0, 1261440003.0, @@ -751,7 +751,7 @@ # issue #152 add isoformat() assert(d.isoformat()[0:24] == '2009-12-22T00:00:00.0156') assert(d.isoformat(sep=' ')[0:24] == '2009-12-22 00:00:00.0156') - assert(d.isoformat(sep=' ',timespec='milliseconds') == '2009-12-22 00:00:00.015') + assert(d.isoformat(sep=' ',timespec='milliseconds') == '2009-12-22 00:00:00.016') assert(d.isoformat(sep=' ',timespec='seconds') == '2009-12-22 00:00:00') assert(d.isoformat(sep=' ',timespec='minutes') == '2009-12-22 00:00') assert(d.isoformat(sep=' ',timespec='hours') == '2009-12-22 00') @@ -921,6 +921,9 @@ assert(not cftime.is_leap_year(1,calendar='standard',has_year_zero=True)) assert(not cftime.is_leap_year(1,calendar='365_day')) assert(cftime.is_leap_year(1,calendar='366_day')) + # num2date should not fail on an empty int array (issue #287) + d = cftime.num2date(np.array([], dtype="int64"), "days since 1970-01-01",\ + calendar="proleptic_gregorian", only_use_cftime_datetimes=True) class TestDate2index(unittest.TestCase): @@ -1686,6 +1689,59 @@ assert dt.strftime('%H%m%d') == '{0:%H%m%d}'.format(dt) assert 'the year is 2000' == 'the year is {dt:%Y}'.format(dt=dt) + +def test_string_format2(): + dt = cftime.datetime(-4713, 1, 1, 12, 0, 0, 10) + # check a given format string acts like strftime + assert dt.strftime('%H%m%d') == '{0:%H%m%d}'.format(dt) + assert dt.strftime() == '-4713-01-01 12:00:00' + assert dt.strftime('%Y-%m-%d %H:%M:%S') == '-4713-01-01 12:00:00' + assert dt.strftime('%Y-%m-%d %H:%M:%S.%f') == '-4713-01-01 12:00:00.000010' + assert dt.strftime('%d.%m.%Y %H:%M:%S.%f') == '01.01.-4713 12:00:00.000010' + dt = cftime.datetime(-713, 1, 1, 12, 0, 0, 10) + assert dt.strftime('%H%m%d') == '{0:%H%m%d}'.format(dt) + assert dt.strftime() == '-0713-01-01 12:00:00' + assert dt.strftime('%Y-%m-%d %H:%M:%S') == '-0713-01-01 12:00:00' + assert dt.strftime('%Y-%m-%d %H:%M:%S.%f') == '-0713-01-01 12:00:00.000010' + assert dt.strftime('%d.%m.%Y %H:%M:%S.%f') == '01.01.-0713 12:00:00.000010' + +def test_strptime(): + d = cftime.datetime.strptime('24/Aug/2004:17:57:26 +0200', '%d/%b/%Y:%H:%M:%S %z',calendar='julian',has_year_zero=True) + assert(repr(d) == "cftime.datetime(2004, 8, 24, 15, 57, 26, 0, calendar='julian', has_year_zero=True)") + d = cftime.datetime.strptime("0000-02-30",\ + "%Y-%m-%d",calendar='360_day',has_year_zero=True) + assert(repr(d) == "cftime.datetime(0, 2, 30, 0, 0, 0, 0, calendar='360_day', has_year_zero=True)") + d = cftime.datetime.strptime('-99999-02-29 10:18:32.926',\ + '%Y-%m-%d %H:%M:%S.%f',calendar='366_day') + assert(repr(d) == "cftime.datetime(-99999, 2, 29, 10, 18, 32, 926000, calendar='all_leap', has_year_zero=True)") + d = cftime.datetime.strptime('24/Aug/-4712:17:57:26', '%d/%b/%Y:%H:%M:%S',calendar='julian') + assert(repr(d) == "cftime.datetime(-4712, 8, 24, 17, 57, 26, 0, calendar='julian', has_year_zero=False)") + d = cftime.datetime.strptime('24/August/-4712:17:57:26', '%d/%B/%Y:%H:%M:%S',calendar='julian') + assert(repr(d) == "cftime.datetime(-4712, 8, 24, 17, 57, 26, 0, calendar='julian', has_year_zero=False)") + d = cftime.datetime.strptime("-4712", "%Y", calendar="julian") + assert(repr(d) == "cftime.datetime(-4712, 1, 1, 0, 0, 0, 0, calendar='julian', has_year_zero=False)") + # should fail with KeyError + try: + d=cftime.datetime.strptime("2000-45-3", "%G-%V-%u", calendar="noleap") + except KeyError: + pass + else: + raise AssertionError + + +def test_string_isoformat(): + dt = cftime.datetime(-4713, 1, 1, 12, 0, 0, 10) + assert dt.isoformat() == '-4713-01-01T12:00:00.000010' + assert dt.isoformat(' ', 'days') == '-4713-01-01' + assert dt.isoformat(' ', 'seconds') == '-4713-01-01 12:00:00' + assert dt.isoformat(' ', 'microseconds') == '-4713-01-01 12:00:00.000010' + dt = cftime.datetime(-713, 1, 1, 12, 0, 0, 10) + assert dt.isoformat() == '-0713-01-01T12:00:00.000010' + assert dt.isoformat(' ', 'days') == '-0713-01-01' + assert dt.isoformat(' ', 'seconds') == '-0713-01-01 12:00:00' + assert dt.isoformat(' ', 'microseconds') == '-0713-01-01 12:00:00.000010' + + def test_dayofyr_after_replace(date_type): date = date_type(1, 1, 1) assert date.dayofyr == 1 @@ -2048,11 +2104,16 @@ assert encoded.dtype == np.int64 np.testing.assert_equal(decoded, times) else: + # if sys.platform.startswith("win"): + # assert encoded.dtype == np.float64 + # else: + # assert encoded.dtype == np.float128 assert encoded.dtype == np.float64 tolerance = timedelta(microseconds=2000) meets_tolerance = np.abs(decoded - times) <= tolerance assert np.all(meets_tolerance) + def test_date2num_missing_data(): # Masked array a = [ @@ -2064,7 +2125,7 @@ mask = [True, False, True, False] array = np.ma.array(a, mask=mask) out = date2num(array, units="days since 2000-12-01", calendar="standard") - assert ((out == np.ma.array([-99, 1, -99, 3] , mask=mask)).all()) + assert ((out == np.ma.array([-99, 1, -99, 3], mask=mask)).all()) assert ((out.mask == mask).all()) # Scalar masked array @@ -2101,5 +2162,58 @@ np.testing.assert_equal(result, expected) +DATEPARSE_ERROR_TESTS = [ + ("foo", "In general, units must be"), + ("months", "'months since' units only allowed"), + ("common_years", "'common_years' units only allowed") +] + + +@pytest.mark.parametrize(("units", "match"), DATEPARSE_ERROR_TESTS) +def test_num2date_unrecognized_units(units, match): + with pytest.raises(ValueError, match=match): + num2date(0.0, units=f"{units} since 2000-01-01", calendar="standard") + + +@pytest.mark.parametrize(("units", "match"), DATEPARSE_ERROR_TESTS) +def test_date2num_unrecognized_units(units, match): + date = cftime.datetime(2000, 1, 1, calendar="standard") + with pytest.raises(ValueError, match=match): + date2num(date, units=f"{units} since 2000-01-01", calendar="standard") + + +def test_num2date_precision(): + if sys.platform.startswith("win"): + pytest.skip("skipping tests that require float128 on windows") + testdates = [(1271, 3, 18, 19, 41, 33), + (1271, 3, 18, 19, 41, 32, 999998)] + unitinc = ['microseconds', 'seconds', 'minutes', 'hours', 'days'] + for cc in ['standard', 'gregorian', 'julian', 'proleptic_gregorian', + 'noleap', 'all_leap', '365_day', '366_day', '360_day']: + for uinc in unitinc: + if cc in ['standard', 'gregorian', 'julian']: + units = uinc + ' since -4713-01-01 12:00:00' + elif cc in ['proleptic_gregorian']: + units = uinc + ' since -4714-01-01 12:00:00' + elif cc in ['noleap', 'all_leap', '365_day', '366_day', '360_day']: + units = uinc + ' since 0000-01-01 12:00:00' + # scalar + date = datetimex(*testdates[0], calendar=cc) + num = date2num(date, units, calendar=cc, longdouble=True) + date2 = num2date(num, units, calendar=cc) + assert date == date2 + # array + date = [ datetimex(*dd, calendar=cc) for dd in testdates ] + num = date2num(date, units, calendar=cc, longdouble=True) + date2 = num2date(num, units, calendar=cc) + for i in range(len(date)): + assert date[i] == date2[i] + # masked array + num = np.ma.array(num, mask=(True, False)) + date2 = num2date(num, units, calendar=cc) + assert np.ma.is_masked(date2[0]) + assert date[1] == date2[1] + + if __name__ == '__main__': unittest.main()