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',