Hello community,

here is the log from the commit of package python-dbf for openSUSE:Factory 
checked in at 2018-12-13 19:47:42
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-dbf (Old)
 and      /work/SRC/openSUSE:Factory/.python-dbf.new.28833 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-dbf"

Thu Dec 13 19:47:42 2018 rev:2 rq:655641 version:0.97.11

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-dbf/python-dbf.changes    2018-06-02 
12:04:54.191933306 +0200
+++ /work/SRC/openSUSE:Factory/.python-dbf.new.28833/python-dbf.changes 
2018-12-13 19:47:43.732756532 +0100
@@ -1,0 +2,12 @@
+Thu Dec  6 12:46:33 UTC 2018 - Tomáš Chvátal <tchva...@suse.com>
+
+- Version update to 0.97.11:
+  * No obvious changelog
+- Fix fdupes call
+
+-------------------------------------------------------------------
+Tue Dec  4 12:47:09 UTC 2018 - Matej Cepl <mc...@suse.com>
+
+- Remove superfluous devel dependency for noarch package
+
+-------------------------------------------------------------------

Old:
----
  LICENSE
  dbf-0.97.5.tar.gz

New:
----
  dbf-0.97.11.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-dbf.spec ++++++
--- /var/tmp/diff_new_pack.G2GueJ/_old  2018-12-13 19:47:44.240755875 +0100
+++ /var/tmp/diff_new_pack.G2GueJ/_new  2018-12-13 19:47:44.244755869 +0100
@@ -12,26 +12,23 @@
 # license that conforms to the Open Source Definition (Version 1.9)
 # published by the Open Source Initiative.
 
-# Please submit bugfixes or comments via http://bugs.opensuse.org/
+# Please submit bugfixes or comments via https://bugs.opensuse.org/
 #
 
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-dbf
-Version:        0.97.5
+Version:        0.97.11
 Release:        0
 Summary:        Pure python package for reading/writing dBase, FoxPro, and 
Visual FoxPro .dbf
 License:        BSD-3-Clause
 Group:          Development/Languages/Python
-Url:            https://bitbucket.org/stoneleaf/dbf/src/default/
+URL:            https://bitbucket.org/stoneleaf/dbf/src/default/
 Source:         
https://files.pythonhosted.org/packages/source/d/dbf/dbf-%{version}.tar.gz
-Source10:       https://bitbucket.org/stoneleaf/dbf/raw/%{version}/dbf/LICENSE
-BuildRequires:  %{python_module devel}
 BuildRequires:  %{python_module setuptools}
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 BuildArch:      noarch
-
 %python_subpackages
 
 %description
@@ -44,18 +41,16 @@
 
 %prep
 %setup -q -n dbf-%{version}
-cp %{SOURCE10} .
 
 %build
 %python_build
 
 %install
 %python_install
-%fdupes %{buildroot}%{_prefix}
+%python_expand %fdupes %{buildroot}%{$python_sitelib}
 
 %files %{python_files}
-%defattr(-,root,root,-)
-%license LICENSE
+%license dbf/LICENSE
 %{python_sitelib}/dbf-%{version}-py*.egg-info
 %{python_sitelib}/dbf
 

++++++ dbf-0.97.5.tar.gz -> dbf-0.97.11.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dbf-0.97.5/PKG-INFO new/dbf-0.97.11/PKG-INFO
--- old/dbf-0.97.5/PKG-INFO     2018-05-24 07:13:40.000000000 +0200
+++ new/dbf-0.97.11/PKG-INFO    2018-06-06 03:54:52.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: dbf
-Version: 0.97.5
+Version: 0.97.11
 Summary: Pure python package for reading/writing dBase, FoxPro, and Visual 
FoxPro .dbf files (including memos)
 Home-page: https://pypi.python.org/pypi/dbf
 Author: Ethan Furman
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dbf-0.97.5/dbf/LICENSE new/dbf-0.97.11/dbf/LICENSE
--- old/dbf-0.97.5/dbf/LICENSE  1970-01-01 01:00:00.000000000 +0100
+++ new/dbf-0.97.11/dbf/LICENSE 2018-06-06 03:38:23.000000000 +0200
@@ -0,0 +1,32 @@
+Copyright (c) 2008-2018 Ethan Furman
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+    Redistributions of source code must retain the above
+    copyright notice, this list of conditions and the
+    following disclaimer.
+
+    Redistributions in binary form must reproduce the above
+    copyright notice, this list of conditions and the following
+    disclaimer in the documentation and/or other materials
+    provided with the distribution.
+
+    Neither the name Ethan Furman nor the names of any
+    contributors may be used to endorse or promote products
+    derived from this software without specific prior written
+    permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dbf-0.97.5/dbf/README.md 
new/dbf-0.97.11/dbf/README.md
--- old/dbf-0.97.5/dbf/README.md        1970-01-01 01:00:00.000000000 +0100
+++ new/dbf-0.97.11/dbf/README.md       2018-06-06 03:38:23.000000000 +0200
@@ -0,0 +1,128 @@
+dbf
+===
+
+dbf (also known as python dbase) is a module for reading/writing
+dBase III, FP, VFP, and Clipper .dbf database files.  It's
+an ancient format that still finds lots of use (the most common
+I'm aware of is retrieving legacy data so it can be stored in a
+newer database system; other uses include GIS, stand-alone programs
+such as Family History, Personal Finance, etc.).
+
+Highlights
+----------
+
+Table -- represents a single .dbf/.dbt (or .fpt) file combination
+and provides access to records; suports the sequence access and 'with'
+protocols.  Temporary tables can also live entirely in memory.
+
+Record -- repesents a single record/row in the table, with field access
+returning native or custom data types; supports the sequence, mapping,
+attribute access (with the field names as the attributes), and 'with'
+protocols.  Updates to a record object are reflected on disk either
+immediately (using gather() or write()), or at the end of a 'with'
+statement.
+
+Index -- nonpersistent index for a table.
+
+Fields::
+
+    dBase III (Null not supported)
+
+        Character --> unicode
+        Date      --> datetime.date or None
+        Logical   --> bool or None
+        Memo      --> unicode or None
+        Numeric   --> int/float depending on field definition or None
+
+        Float     --> same as numeric
+
+    Clipper (Null not supported)
+
+        Character --> unicode  (character fields can be up to 65,519)
+
+    Foxpro (Null supported)
+
+        General   --> str (treated as binary)
+        Picture   --> str (treated as binary)
+
+    Visual Foxpro (Null supported)
+
+        Currency  --> decimal.Decimal
+        douBle    --> float
+        Integer   --> int
+        dateTime  --> datetime.datetime
+
+    If a field is uninitialized (Date, Logical, Numeric, Memo, General,
+    Picture) then None is returned for the value.
+
+Custom data types::
+
+    Null     -->  used to support Null values
+
+    Char     -->  unicode type that auto-trims trailing whitespace, and
+                  ignores trailing whitespace for comparisons
+
+    Date     -->  date object that allows for no date
+
+    DateTime -->  datetime object that allows for no datetime
+
+    Time     -->  time object that allows for no time
+
+    Logical  -->  adds Unknown state to bool's: instead of True/False/None,
+                  values are Truth, Falsth, and Unknown, with appropriate
+                  tri-state logic; just as bool(None) is False, bool(Unknown)
+                  is also False;  the numerical values of Falsth, Truth, and
+                  Unknown is 0, 1, 2
+
+    Quantum  -->  similar to Logical, but implements boolean algebra (I think).
+                  Has states of Off, On, and Other.  Other has no boolean nor
+                  numerical value, and attempts to use it as such will raise
+                  an exception
+
+
+Whirlwind Tour
+--------------
+
+    import datetime
+    import dbf
+
+    table = dbf.Table(
+            filename='test',
+            field_specs='name C(25); age N(3,0); birth D; qualified L',
+            on_disk=False,
+            )
+    table.open()
+
+    for datum in (
+            ('Spanky', 7, dbf.Date.fromymd('20010315'), False),
+            ('Spunky', 23, dbf.Date(1989, 07, 23), True),
+            ('Sparky', 99, dbf.Date(), dbf.Unknown),
+            ):
+        table.append(datum)
+
+    for record in table:
+        print record
+        print '--------'
+        print record[0:3]
+        print record['name':'qualified']
+        print [record.name, record.age, record.birth]
+        print '--------'
+
+    custom = table.new(
+            filename='test_on_disk',
+            default_data_types=dict(C=dbf.Char, D=dbf.Date, L=dbf.Logical),
+            )
+
+    with custom:    # automatically opened and closed
+        for record in table:
+            custom.append(record)
+        for record in custom:
+            dbf.write(record, name=record.name.upper())
+            print record
+            print '--------'
+            print record[0:3]
+            print record['name':'qualified']
+            print [record.name, record.age, record.birth]
+            print '--------'
+
+    table.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dbf-0.97.5/dbf/__init__.py 
new/dbf-0.97.11/dbf/__init__.py
--- old/dbf-0.97.5/dbf/__init__.py      2018-05-24 06:49:01.000000000 +0200
+++ new/dbf-0.97.11/dbf/__init__.py     2018-06-06 03:38:23.000000000 +0200
@@ -53,6 +53,10 @@
 from os import SEEK_END
 from textwrap import dedent
 
+try:
+    import pytz
+except ImportError:
+    pytz = None
 
 py_ver = sys.version_info[:2]
 if py_ver < (3, 0):
@@ -68,7 +72,7 @@
     long = int
     xrange = range
 
-version = 0, 97, 5
+version = 0, 97, 11
 
 NoneType = type(None)
 
@@ -1187,7 +1191,7 @@
 
     @classmethod
     def fromymd(cls, yyyymmdd):
-        if yyyymmdd in ('', '        ', 'no date'):
+        if yyyymmdd in ('', '        ', 'no date', '00000000'):
             return cls()
         return cls(datetime.date(int(yyyymmdd[:4]), int(yyyymmdd[4:6]), 
int(yyyymmdd[6:])))
 
@@ -1239,6 +1243,9 @@
             return cls(*(time.strptime(date_string, format)[0:3]))
         return cls(*(time.strptime(date_string, "%Y-%m-%d")[0:3]))
 
+    def timetuple(self):
+        return self._date.timetuple()
+
     @classmethod
     def today(cls):
         return cls(datetime.date.today())
@@ -1263,7 +1270,7 @@
 
     __slots__ = ['_datetime']
 
-    def __new__(cls, year=None, month=0, day=0, hour=0, minute=0, second=0, 
microsecond=0):
+    def __new__(cls, year=None, month=0, day=0, hour=0, minute=0, second=0, 
microsecond=0, tzinfo=Null):
         """year may be a datetime.datetime"""
         if year is None or year is Null:
             return cls._null_datetime
@@ -1271,15 +1278,35 @@
         if isinstance(year, basestring):
             return DateTime.strptime(year)
         elif isinstance(year, DateTime):
+            if tzinfo is not Null and year._datetime.tzinfo:
+                raise ValueError('not naive datetime (tzinfo is already set)')
+            elif tzinfo is Null:
+                tzinfo = None
             ndt._datetime = year._datetime
         elif isinstance(year, datetime.datetime):
+            if tzinfo is not Null and year.tzinfo:
+                raise ValueError('not naive datetime (tzinfo is already set)')
+            elif tzinfo is Null:
+                tzinfo = year.tzinfo
             microsecond = year.microsecond // 1000 * 1000
             hour, minute, second = year.hour, year.minute, year.second
             year, month, day = year.year, year.month, year.day
-            ndt._datetime = datetime.datetime(year, month, day, hour, minute, 
second, microsecond)
+            if pytz is None or tzinfo is None:
+                ndt._datetime = datetime.datetime(year, month, day, hour, 
minute, second, microsecond, tzinfo)
+            else:
+                # if pytz and tzinfo, tzinfo must be added after creation
+                _datetime = datetime.datetime(year, month, day, hour, minute, 
second, microsecond)
+                ndt._datetime = tzinfo.normalize(tzinfo.localize(_datetime))
         elif year is not None:
+            if tzinfo is Null:
+                tzinfo = None
             microsecond = microsecond // 1000 * 1000
-            ndt._datetime = datetime.datetime(year, month, day, hour, minute, 
second, microsecond)
+            if pytz is None or tzinfo is None:
+                ndt._datetime = datetime.datetime(year, month, day, hour, 
minute, second, microsecond, tzinfo)
+            else:
+                # if pytz and tzinfo, tzinfo must be added after creation
+                _datetime = datetime.datetime(year, month, day, hour, minute, 
second, microsecond)
+                ndt._datetime = tzinfo.normalize(tzinfo.localize(_datetime))
         return ndt
 
     def __add__(self, other):
@@ -1292,7 +1319,9 @@
         if isinstance(other, self.__class__):
             return self._datetime == other._datetime
         if isinstance(other, datetime.date):
-            return self._datetime == other
+            me = self._datetime.timetuple()
+            them = other.timetuple()
+            return me[:6] == them[:6] and self.microsecond == 
(other.microsecond//1000*1000)
         if isinstance(other, type(None)):
             return self._datetime is None
         return NotImplemented
@@ -1419,8 +1448,15 @@
 
     def __repr__(self):
         if self:
-            return "DateTime(%5d, %2d, %2d, %2d, %2d, %2d, %2d)" % (
-                self._datetime.timetuple()[:6] + (self._datetime.microsecond, )
+            if self.tzinfo is None:
+                tz = ''
+            else:
+                diff = self._datetime.utcoffset()
+                hours, minutes = divmod(diff.days * 86400 + diff.seconds, 3600)
+                minus, hours = hours < 0, abs(hours)
+                tz = ', tzinfo=<%s %s%02d%02d>' % (self._datetime.tzname(), 
('','-')[minus], hours, minutes)
+            return "DateTime(%d, %d, %d, %d, %d, %d, %d%s)" % (
+                self._datetime.timetuple()[:6] + (self._datetime.microsecond, 
tz)
                 )
         else:
             return "DateTime()"
@@ -1441,9 +1477,16 @@
             return NotImplemented
 
     @classmethod
-    def combine(cls, date, time):
+    def combine(cls, date, time, tzinfo=Null):
+        # if tzinfo is given, timezone is added/stripped
+        if tzinfo is Null:
+            tzinfo = time.tzinfo
         if Date(date) and Time(time):
-            return cls(date.year, date.month, date.day, time.hour, 
time.minute, time.second, time.microsecond)
+            return cls(
+                    date.year, date.month, date.day,
+                    time.hour, time.minute, time.second, time.microsecond,
+                    tzinfo=tzinfo,
+                    )
         return cls()
 
     def date(self):
@@ -1468,15 +1511,17 @@
         return DateTime(datetime.datetime.fromtimestamp(timestamp))
 
     @classmethod
-    def now(cls):
+    def now(cls, tzinfo=None):
         "only accurate to milliseconds"
-        return cls(datetime.datetime.now())
+        return cls(datetime.datetime.now(), tzinfo=tzinfo)
 
-    def replace(self, year=None, month=None, day=None, hour=None, minute=None, 
second=None, microsecond=None,
+    def replace(self, year=None, month=None, day=None, hour=None, minute=None, 
second=None, microsecond=None, tzinfo=Null,
               delta_year=0, delta_month=0, delta_day=0, delta_hour=0, 
delta_minute=0, delta_second=0):
         if not self:
             return self.__class__._null_datetime
         old_year, old_month, old_day, old_hour, old_minute, old_second, 
old_micro = self.timetuple()[:7]
+        if tzinfo is Null:
+            tzinfo = self._datetime.tzinfo
         if isinstance(month, RelativeMonth):
             this_month = IsoMonth(old_month)
             delta_month += month.months_from(this_month)
@@ -1534,7 +1579,7 @@
             while second > 59:
                 minute += 1
                 second = second - 60
-        return DateTime(year, month, day, hour, minute, second, microsecond)
+        return DateTime(year, month, day, hour, minute, second, microsecond, 
tzinfo)
 
     def strftime(self, format):
         fmt_cls = type(format)
@@ -1561,6 +1606,14 @@
             return Time(self.hour, self.minute, self.second, self.microsecond)
         return Time()
 
+    def timetuple(self):
+        return self._datetime.timetuple()
+
+    def timetz(self):
+        if self:
+            return Time(self._datetime.timetz())
+        return Time()
+
     @classmethod
     def utcnow(cls):
         return cls(datetime.datetime.utcnow())
@@ -1583,7 +1636,7 @@
 
     __slots__ = ['_time']
 
-    def __new__(cls, hour=None, minute=0, second=0, microsecond=0):
+    def __new__(cls, hour=None, minute=0, second=0, microsecond=0, 
tzinfo=Null):
         """
         hour may be a datetime.time or a str(Time)
         """
@@ -1593,14 +1646,24 @@
         if isinstance(hour, basestring):
             hour = Time.strptime(hour)
         if isinstance(hour, Time):
-            nt._time = hour._time
+            if tzinfo is not Null and hour._time.tzinfo:
+                raise ValueError('not naive time (tzinfo is already set)')
+            elif tzinfo is Null:
+                tzinfo = None
+            nt._time = hour._time.replace(tzinfo=tzinfo)
         elif isinstance(hour, (datetime.time)):
+            if tzinfo is not Null and hour.tzinfo:
+                raise ValueError('not naive time (tzinfo is already set)')
+            if tzinfo is Null:
+                tzinfo = hour.tzinfo
             microsecond = hour.microsecond // 1000 * 1000
             hour, minute, second = hour.hour, hour.minute, hour.second
-            nt._time = datetime.time(hour, minute, second, microsecond)
+            nt._time = datetime.time(hour, minute, second, microsecond, tzinfo)
         elif hour is not None:
+            if tzinfo is Null:
+                tzinfo = None
             microsecond = microsecond // 1000 * 1000
-            nt._time = datetime.time(hour, minute, second, microsecond)
+            nt._time = datetime.time(hour, minute, second, microsecond, tzinfo)
         return nt
 
     def __add__(self, other):
@@ -1616,7 +1679,12 @@
         if isinstance(other, self.__class__):
             return self._time == other._time
         if isinstance(other, datetime.time):
-            return self._time == other
+            return (
+                    self.hour == other.hour and
+                    self.minute == other.minute and
+                    self.second == other.second and
+                    self.microsecond == (other.microsecond//1000*1000)
+                    )
         if isinstance(other, type(None)):
             return self._time is None
         return NotImplemented
@@ -1743,7 +1811,14 @@
 
     def __repr__(self):
         if self:
-            return "Time(%d, %d, %d, %d)" % (self.hour, self.minute, 
self.second, self.microsecond)
+            if self.tzinfo is None:
+                tz = ''
+            else:
+                diff = self._time.tzinfo.utcoffset(self._time)
+                hours, minutes = divmod(diff.days * 86400 + diff.seconds, 3600)
+                minus, hours = hours < 0, abs(hours)
+                tz = ', tzinfo=<%s %s%02d%02d>' % 
(self._time.tzinfo.tzname(self._time), ('','-')[minus], hours, minutes)
+            return "Time(%d, %d, %d, %d%s)" % (self.hour, self.minute, 
self.second, self.microsecond, tz)
         else:
             return "Time()"
 
@@ -1790,13 +1865,15 @@
         return Time(hours, minutes, seconds, microseconds)
 
     @staticmethod
-    def now():
+    def now(tzinfo=None):
         "only accurate to milliseconds"
-        return DateTime.now().time()
+        return DateTime.now(tzinfo).timetz()
 
-    def replace(self, hour=None, minute=None, second=None, microsecond=None, 
delta_hour=0, delta_minute=0, delta_second=0):
+    def replace(self, hour=None, minute=None, second=None, microsecond=None, 
tzinfo=Null, delta_hour=0, delta_minute=0, delta_second=0):
         if not self:
             return self.__class__._null_time
+        if tzinfo is Null:
+            tzinfo = self._time.tzinfo
         old_hour, old_minute, old_second, old_micro = self.hour, self.minute, 
self.second, self.microsecond
         hour = (hour or old_hour) + delta_hour
         minute = (minute or old_minute) + delta_minute
@@ -1819,7 +1896,7 @@
                 hour = 24 + hour
             while hour > 23:
                 hour = hour - 24
-        return Time(hour, minute, second, microsecond)
+        return Time(hour, minute, second, microsecond, tzinfo)
 
     def strftime(self, format):
         fmt_cls = type(format)
@@ -1830,13 +1907,13 @@
     @classmethod
     def strptime(cls, time_string, format=None):
         if format is not None:
-            return cls(datetime.time.strptime(time_string, format))
+            return cls(*time.strptime(time_string, format)[3:6])
         for format in (
                 "%H:%M:%S.%f",
                 "%H:%M:%S",
                 ):
             try:
-                return cls(datetime.datetime.strptime(time_string, format))
+                return cls(*time.strptime(time_string, format)[3:6])
             except ValueError:
                 pass
         raise ValueError("Unable to convert %r" % time_string)
@@ -2548,9 +2625,16 @@
     from xmlrpclib import Marshaller
 else:
     from xmlrpc.client import Marshaller
+# Char is unicode
 Marshaller.dispatch[Char] = Marshaller.dump_unicode
+# Logical unknown becomes False
 Marshaller.dispatch[Logical] = Marshaller.dump_bool
-Marshaller.dispatch[DateTime] = Marshaller.dump_datetime
+# DateTime is transmitted as UTC if aware, local if naive
+Marshaller.dispatch[DateTime] = lambda s, dt, w: w(
+        '<value><dateTime.iso8601>'
+        '%04d%02d%02dT%02d:%02d:%02d'
+        '</dateTime.iso8601></value>\n'
+            % dt.utctimetuple()[:6])
 del Marshaller
 
 # Internal classes
@@ -3012,11 +3096,12 @@
         calls appropriate routine to convert value stored in field from array
         """
         # check nullable here, binary is handled in the appropriate retrieve_* 
functions
-        index = self._meta.fields.index(name)
+        # index = self._meta.fields.index(name)
         fielddef = self._meta[name]
         flags = fielddef[FLAGS]
         nullable = flags & NULLABLE and '_nullflags' in self._meta
         if nullable:
+            index = fielddef[NUL]
             byte, bit = divmod(index, 8)
             null_def = self._meta['_nullflags']
             null_data = self._data[null_def[START]:null_def[END]]
@@ -3067,8 +3152,8 @@
         calls appropriate routine to convert value to bytes, and save it in 
record
         """
         # check nullabel here, binary is handled in the appropriate update_* 
functions
-        index = self._meta.fields.index(name)
         fielddef = self._meta[name]
+        index = fielddef[NUL]
         field_type = fielddef[TYPE]
         flags = fielddef[FLAGS]
         nullable = flags & NULLABLE and '_nullflags' in self._meta
@@ -3143,11 +3228,11 @@
         array
         """
         # check nullable here, binary is handled in the appropriate retrieve_* 
functions
-        index = self._meta.fields.index(name)
         fielddef = self._meta[name]
         flags = fielddef[FLAGS]
         nullable = flags & NULLABLE and '_nullflags' in self._meta
         if nullable:
+            index = fielddef[NUL]
             byte, bit = divmod(index, 8)
             null_def = self._meta['_nullflags']
             null_data = self._data[null_def[START]:null_def[END]]
@@ -3184,8 +3269,8 @@
         calls appropriate routine to convert value to ascii bytes, and save it 
in record
         """
         # check nullabel here, binary is handled in the appropriate update_* 
functions
-        index = self._meta.fields.index(name)
         fielddef = self._meta[name]
+        index = fielddef[NUL]
         field_type = fielddef[TYPE]
         flags = fielddef[FLAGS]
         nullable = flags & NULLABLE and '_nullflags' in self._meta
@@ -3865,7 +3950,7 @@
     Returns the ascii coded date as fielddef[CLASS] or fielddef[EMPTY]
     """
     text = to_bytes(bytes)
-    if text == b'        ':
+    if text in (b'        ', b'00000000'):
         cls = fielddef[EMPTY]
         if cls is NoneType:
             return None
@@ -4872,7 +4957,7 @@
             if layout[TYPE] in meta.memo_types:
                 memo = True
             if layout[FLAGS] & NULLABLE:
-                nulls = True
+                nulls += 1
         if memo:
             if self._yesMemoMask <= 0x80:
                 header.version = header.version | self._yesMemoMask
@@ -4888,7 +4973,7 @@
             meta.memo = None
         if nulls:
             start = layout[START] + layout[LENGTH]
-            length, one_more = divmod(len(meta.fields), 8)
+            length, one_more = divmod(nulls, 8)
             if one_more:
                 length += 1
             fielddef = array('B', [0] * 32)
@@ -5125,7 +5210,7 @@
                     'L' : Logical,
                     'D' : Date,
                     }
-            if self._versionabbr != 'db3':
+            if self._versionabbr in ('vfp', 'db4'):
                 default_data_types['T'] = DateTime
         self._meta._default_data_types = default_data_types
         if field_data_types is None:
@@ -5418,7 +5503,8 @@
         if meta.status != READ_WRITE:
             raise DbfError('%s not in read/write mode, unable to add fields 
(%s)' % (meta.filename, meta.status))
         fields = self.structure() + self._list_fields(field_specs, sep=u';')
-        if (len(fields) + ('_nullflags' in meta)) > meta.max_fields:
+        null_fields = any(['null' in f.lower() for f in fields])
+        if (len(fields) + null_fields) > meta.max_fields:
             raise DbfError(
                     "Adding %d more field%s would exceed the limit of %d"
                     % (len(fields), ('','s')[len(fields)==1], meta.max_fields)
@@ -5439,6 +5525,7 @@
         meta.fields[:] = []
 
         meta.blankrecord = None
+        null_index = -1
         for field in fields:
             if not field:
                 continue
@@ -5474,6 +5561,9 @@
             except FieldSpecError:
                 exc = sys.exc_info()[1]
                 raise FieldSpecError(exc.message + ' (%s:%s)' % 
(meta.filename, name)).from_None()
+            nullable = flags & NULLABLE
+            if nullable:
+                null_index += 1
             start = offset
             end = offset + length
             offset = end
@@ -5489,6 +5579,7 @@
                     flags,
                     cls,
                     empty,
+                    nullable and null_index,
                     )
         self._build_header_fields()
         self._update_disk()
@@ -6518,6 +6609,7 @@
                     flags,
                     cls,
                     empty,
+                    0,
                     )
         if offset != total_length:
             raise BadDataError("Header shows record length of %d, but 
calculated record length is %d" % (total_length, offset))
@@ -6687,7 +6779,7 @@
             raise BadDataError("Header shows record length of %d, but 
calculated record length is %d" % (total_length, offset))
         if nulls_found:
             nullable_fields = [f for f in meta if meta[f][NUL]]
-            nullable_fields.sort(key=lambda f: f[START])
+            nullable_fields.sort(key=lambda f: meta[f][START])
             for i, f in enumerate(nullable_fields):
                 meta[f] = meta[f][:-1] + (i, )
             null_bytes, plus_one = divmod(len(nullable_fields), 8)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dbf-0.97.5/dbf/test.py new/dbf-0.97.11/dbf/test.py
--- old/dbf-0.97.5/dbf/test.py  2018-05-24 06:49:01.000000000 +0200
+++ new/dbf-0.97.11/dbf/test.py 2018-06-06 03:38:23.000000000 +0200
@@ -6,6 +6,7 @@
 import tempfile
 import shutil
 import stat
+from unittest import skipIf, TestCase as unittest_TestCase
 
 py_ver = sys.version_info[:2]
 module = globals()
@@ -13,6 +14,11 @@
 from dbf import *
 import dbf
 
+try:
+    import pytz
+except ImportError:
+    pytz = None
+
 if py_ver < (3, 0):
     MISC = ''.join([chr(i) for i in range(256)])
     PHOTO = ''.join(reversed([chr(i) for i in range(256)]))
@@ -28,6 +34,28 @@
     dbf.version[:3] + (sys.platform, sys.version) ))
 
 
+class TestCase(unittest_TestCase):
+
+    def __init__(self, *args, **kwds):
+        regex = getattr(self, 'assertRaisesRegex', None)
+        if regex is None:
+            self.assertRaisesRegex = getattr(self, 'assertRaisesRegexp')
+        super(TestCase, self).__init__(*args, **kwds)
+
+    @classmethod
+    def setUpClass(cls, *args, **kwds):
+        super(TestCase, cls).setUpClass(*args, **kwds)
+        ## filter warnings  (example from scription)
+        # warnings.filterwarnings(
+        #         'ignore',
+        #         'inspect\.getargspec\(\) is deprecated',
+        #         DeprecationWarning,
+        #         'scription',
+        #         0,
+        #         )
+        # double check existence of temp dir
+
+
 # Walker in Leaves -- by Scot Noel -- 
http://www.scienceandfantasyfiction.com/sciencefiction/Walker-in-Leaves/walker-in-leaves.htm
 
 words = """
@@ -139,7 +167,7 @@
     return DoNotIndex
 
 
-class TestChar(unittest.TestCase):
+class TestChar(TestCase):
 
     def test_exceptions(self):
         "exceptions"
@@ -243,19 +271,21 @@
         self.assertTrue(a6 > a5)
 
 
-class TestDateTime(unittest.TestCase):
+class TestDateTime(TestCase):
     "Testing Date"
 
     def test_date_creation(self):
         "Date creation"
-        Date()
-        Date.fromymd('        ')
-        Date.fromordinal(0)
-        Date.today()
-        Date.max
-        Date.min
+        self.assertEqual(Date(), NullDate)
+        self.assertEqual(Date.fromymd('        '), NullDate)
+        self.assertEqual(Date.fromymd('00000000'), NullDate)
+        self.assertEqual(Date.fromordinal(0), NullDate)
+        self.assertEqual(Date.today(), datetime.date.today())
+        self.assertEqual(Date.max, datetime.date.max)
+        self.assertEqual(Date.min, datetime.date.min)
+        self.assertEqual(Date(2018, 5, 21), datetime.date(2018, 5, 21))
+        self.assertEqual(Date.strptime('2018-01-01'), datetime.date(2018, 1, 
1))
         self.assertRaises(ValueError, Date.fromymd, '00000')
-        self.assertRaises(ValueError, Date.fromymd, '00000000')
         self.assertRaises(ValueError, Date, 0, 0, 0)
 
     def test_date_compare(self):
@@ -269,11 +299,13 @@
 
     def test_datetime_creation(self):
         "DateTime creation"
-        DateTime()
-        DateTime.fromordinal(0)
-        DateTime.today()
-        DateTime.max
-        DateTime.min
+        self.assertEqual(DateTime(), NullDateTime)
+        self.assertEqual(DateTime.fromordinal(0), NullDateTime)
+        self.assertTrue(DateTime.today())
+        self.assertEqual(DateTime.max, datetime.datetime.max)
+        self.assertEqual(DateTime.min, datetime.datetime.min)
+        self.assertEqual(DateTime(2018, 5, 21, 19, 17, 16), 
datetime.datetime(2018, 5, 21, 19, 17 ,16))
+        self.assertEqual(DateTime.strptime('2018-01-01 19:17:16'), 
datetime.datetime(2018, 1, 1, 19, 17, 16))
 
     def test_datetime_compare(self):
         "DateTime comparisons"
@@ -303,9 +335,11 @@
 
     def test_time_creation(self):
         "Time creation"
-        Time()
-        Time.max
-        Time.min
+        self.assertEqual(Time(), NullTime)
+        self.assertEqual(Time.max, datetime.time.max)
+        self.assertEqual(Time.min, datetime.time.min)
+        self.assertEqual(Time(19, 17, 16), datetime.time(19, 17 ,16))
+        self.assertEqual(Time.strptime('19:17:16'), datetime.time(19, 17, 16))
 
     def test_time_compare(self):
         "Time comparisons"
@@ -316,6 +350,53 @@
         time3 = Date.fromordinal(3000)
         self.compareTimes(notime1, notime2, time1, time2, time3)
 
+    @unittest.skipIf(pytz is None, 'pytz not installed')
+    def test_datetime_tz(self):
+        "DateTime with Time Zones"
+        pst = pytz.timezone('America/Los_Angeles')
+        mst = pytz.timezone('America/Boise')
+        cst = pytz.timezone('America/Chicago')
+        est = pytz.timezone('America/New_York')
+        utc = pytz.timezone('UTC')
+        #
+        pdt = DateTime(2018, 5, 20, 5, 41, 33, tzinfo=pst)
+        mdt = DateTime(2018, 5, 20, 6, 41, 33, tzinfo=mst)
+        cdt = DateTime(2018, 5, 20, 7, 41, 33, tzinfo=cst)
+        edt = DateTime(2018, 5, 20, 8, 41, 33, tzinfo=est)
+        udt = DateTime(2018, 5, 20, 12, 41, 33, tzinfo=utc)
+        self.assertTrue(pdt == mdt == cdt == edt == udt)
+        #
+        dup1 = DateTime.combine(pdt.date(), mdt.timetz())
+        dup2 = DateTime.combine(cdt.date(), Time(5, 41, 33, tzinfo=pst))
+        self.assertTrue(dup1 == dup2 == udt)
+        #
+        udt2 = DateTime(2018, 5, 20, 13, 41, 33, tzinfo=utc)
+        mdt2 = mdt.replace(tzinfo=pst)
+        self.assertTrue(mdt2 == udt2)
+        #
+        with self.assertRaisesRegex(ValueError, 'not naive datetime'):
+            DateTime(pdt, tzinfo=mst)
+        with self.assertRaisesRegex(ValueError, 'not naive datetime'):
+            DateTime(datetime.datetime(2018, 5, 27, 15, 57, 11, tzinfo=pst), 
tzinfo=pst)
+        with self.assertRaisesRegex(ValueError, 'not naive time'):
+            Time(pdt.timetz(), tzinfo=mst)
+        with self.assertRaisesRegex(ValueError, 'not naive time'):
+            Time(datetime.time(15, 58, 59, tzinfo=mst), tzinfo=mst)
+        #
+        if py_ver < (3, 0):
+            from xmlrpclib import Marshaller, loads
+        else:
+            from xmlrpc.client import Marshaller, loads
+        self.assertEqual(
+                udt.utctimetuple(),
+                loads(Marshaller().dumps([pdt]), 
use_datetime=True)[0][0].utctimetuple(),
+                )
+        #
+        self.assertEqual(
+                pdt,
+                DateTime.combine(Date(2018, 5, 20), Time(5, 41, 33), 
tzinfo=pst),
+                )
+
     def test_arithmetic(self):
         "Date, DateTime, & Time Arithmetic"
         one_day = datetime.timedelta(1)
@@ -409,7 +490,7 @@
         self.assertEqual(tres != tres, False)
 
 
-class TestNull(unittest.TestCase):
+class TestNull(TestCase):
 
     def test_all(self):
         null = Null = dbf.Null()
@@ -517,7 +598,7 @@
 
         self.assertRaises(TypeError, hash, null)
 
-class TestLogical(unittest.TestCase):
+class TestLogical(TestCase):
     "Testing Logical"
 
     def test_unknown(self):
@@ -2193,7 +2274,7 @@
         self.assertEqual(divmod(None, unknown), (unknown, unknown))
 
 
-class TestQuantum(unittest.TestCase):
+class TestQuantum(TestCase):
     "Testing Quantum"
 
     def test_exceptions(self):
@@ -2543,7 +2624,7 @@
         self.assertEqual(-none is none, True)
 
 
-class TestExceptions(unittest.TestCase):
+class TestExceptions(TestCase):
 
     def test_bad_field_specs_on_creation(self):
         self.assertRaises(FieldSpecError, Table, 'blah', 'age N(3,2)', 
on_disk=False)
@@ -2674,7 +2755,7 @@
         table.close()
 
 
-class TestIndexLocation(unittest.TestCase):
+class TestIndexLocation(TestCase):
 
     def test_false(self):
         self.assertFalse(IndexLocation(0, False))
@@ -2685,7 +2766,7 @@
         self.assertTrue(IndexLocation(42, True))
 
 
-class TestDbfCreation(unittest.TestCase):
+class TestDbfCreation(TestCase):
     "Testing table creation..."
 
     def test_db3_memory_tables(self):
@@ -2858,7 +2939,7 @@
             table.close()
 
 
-class TestDbfRecords(unittest.TestCase):
+class TestDbfRecords(TestCase):
     "Testing records"
 
     def setUp(self):
@@ -3281,6 +3362,7 @@
                 appt = appt,
                 wisdom = 'timing is everything',
                 )
+        record = table[-1]
         self.assertEqual(record.name, 'Ethan')
         self.assertEqual(type(record.name), Char)
         self.assertTrue(record.born)
@@ -3306,11 +3388,56 @@
                 appt = None,
                 wisdom = None,
                 )
+        record = table[-1]
         self.assertTrue(record.name is None)
         self.assertTrue(record.born is None)
         self.assertTrue(record.married is None)
         self.assertTrue(record.appt is None)
         self.assertTrue(record.wisdom is None)
+        table = Table(
+            filename=':memory:',
+            field_specs='name C(20); born L; married D null; appt T; wisdom M; 
pets L; cars N(3,0) null; story M; died D null;',
+            default_data_types=dict(
+                    C=(Char, NoneType, NullType),
+                    L=(Logical, NoneType, NullType),
+                    D=(Date, NoneType, NullType),
+                    T=(DateTime, NoneType, NullType),
+                    M=(Char, NoneType, NullType),
+                    N=(int, NoneType, NullType),
+                    ),
+            dbf_type='vfp',
+            on_disk=False,
+            )
+        table.open(mode=READ_WRITE)
+        table.append()
+        record = table[-1]
+        dbf.write(
+                record,
+                name = 'Ethan               ',
+                born = True,
+                married = datetime.date(2001, 6, 27),
+                appt = appt,
+                wisdom = 'timing is everything',
+                pets = True,
+                cars = 10,
+                story = 'a poor farm boy who made  good',
+                died = datetime.date(2018, 5, 30),
+                )
+        record = table[-1]
+        self.assertEqual(record.name, 'Ethan')
+        self.assertTrue(record.born)
+        self.assertTrue(record.born is Truth)
+        self.assertEqual(record.married, datetime.date(2001, 6, 27))
+        self.assertEqual(record.appt, datetime.datetime(2012, 12, 15, 9, 37, 
11))
+        self.assertEqual(record.wisdom, 'timing is everything')
+        self.assertTrue(record.pets)
+        self.assertEqual(record.cars, 10)
+        self.assertEqual(record.story, 'a poor farm boy who made  good',)
+        self.assertEqual(record.died, datetime.date(2018, 5, 30))
+        dbf.write(record, married=Null, died=Null)
+        record = table[-1]
+        self.assertTrue(record.married is Null)
+        self.assertTrue(record.died is Null)
 
     def test_nonascii_text_cptrans(self):
         "check non-ascii text to unicode"
@@ -3514,7 +3641,7 @@
         self.assertNotEqual(old_data, dbf.scatter(record))
 
 
-class TestDbfRecordTemplates(unittest.TestCase):
+class TestDbfRecordTemplates(TestCase):
     "Testing records"
 
     def setUp(self):
@@ -3564,7 +3691,7 @@
         table.append(record)
 
 
-class TestDbfFunctions(unittest.TestCase):
+class TestDbfFunctions(TestCase):
 
     def setUp(self):
         "create a dbf and vfp table"
@@ -4508,7 +4635,7 @@
         self.assertTrue(sorted.index_search('jul', partial=True))
 
 
-class TestDbfNavigation(unittest.TestCase):
+class TestDbfNavigation(TestCase):
 
     def setUp(self):
         "create a dbf and vfp table"
@@ -4806,7 +4933,7 @@
         table.close()
 
 
-class TestDbfLists(unittest.TestCase):
+class TestDbfLists(TestCase):
     "DbfList tests"
 
     def setUp(self):
@@ -4966,7 +5093,7 @@
         table.close()
 
 
-class TestReadWriteDefaultOpen(unittest.TestCase):
+class TestReadWriteDefaultOpen(TestCase):
     "test __enter__/__exit__"
 
     def setUp(self):
@@ -5007,7 +5134,7 @@
         self.assertRaises((IOError, OSError), table.open, READ_WRITE)
 
 
-class TestWhatever(unittest.TestCase):
+class TestWhatever(TestCase):
     "move tests here to run one at a time while debugging"
 
     def setUp(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dbf-0.97.5/dbf.egg-info/PKG-INFO 
new/dbf-0.97.11/dbf.egg-info/PKG-INFO
--- old/dbf-0.97.5/dbf.egg-info/PKG-INFO        2018-05-24 07:13:40.000000000 
+0200
+++ new/dbf-0.97.11/dbf.egg-info/PKG-INFO       2018-06-06 03:54:52.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: dbf
-Version: 0.97.5
+Version: 0.97.11
 Summary: Pure python package for reading/writing dBase, FoxPro, and Visual 
FoxPro .dbf files (including memos)
 Home-page: https://pypi.python.org/pypi/dbf
 Author: Ethan Furman
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dbf-0.97.5/dbf.egg-info/SOURCES.txt 
new/dbf-0.97.11/dbf.egg-info/SOURCES.txt
--- old/dbf-0.97.5/dbf.egg-info/SOURCES.txt     2018-05-24 07:13:40.000000000 
+0200
+++ new/dbf-0.97.11/dbf.egg-info/SOURCES.txt    2018-06-06 03:54:52.000000000 
+0200
@@ -1,4 +1,6 @@
 setup.py
+dbf/LICENSE
+dbf/README.md
 dbf/__init__.py
 dbf/_index.py
 dbf/test.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dbf-0.97.5/setup.py new/dbf-0.97.11/setup.py
--- old/dbf-0.97.5/setup.py     2018-05-24 06:49:01.000000000 +0200
+++ new/dbf-0.97.11/setup.py    2018-06-06 03:38:23.000000000 +0200
@@ -21,12 +21,18 @@
 
 data = dict(
         name='dbf',
-        version='0.97.5',
+        version='0.97.11',
         license='BSD License',
         description='Pure python package for reading/writing dBase, FoxPro, 
and Visual FoxPro .dbf files (including memos)',
         long_description=long_desc,
         url='https://pypi.python.org/pypi/dbf',
         packages=['dbf', ],
+        package_data={
+           'dbf' : [
+               'LICENSE',
+               'README.md',
+               ]
+           },
         provides=['dbf'],
         install_requires=['aenum'],
         author='Ethan Furman',


Reply via email to