diff options
-rw-r--r-- | .travis.yml | 38 | ||||
-rw-r--r-- | README.rst | 11 | ||||
-rw-r--r-- | bootstrap.py | 266 | ||||
-rw-r--r-- | buildout.cfg | 32 | ||||
-rw-r--r-- | setup.cfg | 2 | ||||
-rw-r--r-- | setup.py | 29 | ||||
-rw-r--r-- | src/isodate/duration.py | 143 | ||||
-rw-r--r-- | src/isodate/isoduration.py | 4 | ||||
-rw-r--r-- | src/isodate/isotime.py | 6 | ||||
-rw-r--r-- | src/isodate/tests/__init__.py | 1 | ||||
-rw-r--r-- | src/isodate/tests/test_date.py | 1 | ||||
-rw-r--r-- | src/isodate/tests/test_datetime.py | 1 | ||||
-rw-r--r-- | src/isodate/tests/test_duration.py | 5 | ||||
-rw-r--r-- | src/isodate/tests/test_pickle.py | 13 | ||||
-rw-r--r-- | src/isodate/tests/test_strf.py | 1 | ||||
-rw-r--r-- | src/isodate/tests/test_time.py | 1 | ||||
-rw-r--r-- | src/isodate/tzinfo.py | 17 | ||||
-rw-r--r-- | tox.ini | 6 |
18 files changed, 150 insertions, 427 deletions
diff --git a/.travis.yml b/.travis.yml index 5652e3f..182ac7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,26 @@ - language: python - -python: - - 3.4 - -env: - - TOX_ENV=py26 - - TOX_ENV=py27 - - TOX_ENV=py32 - - TOX_ENV=py33 - - TOX_ENV=py34 - - TOX_ENV=pypy - - TOX_ENV=pypy3 - - TOX_ENV=flake - - TOX_ENV=cover - +matrix: + include: + - python: 2.7 + env: TOXENV=py27 + - python: 3.3 + env: TOXENV=py33 + - python: 3.4 + env: TOXENV=py34 + - python: 3.5 + env: TOXENV=py35 + - python: 3.6 + env: TOXENV=py36 + - python: 3.6 + env: TOXENV=flake + - python: 3.6 + env: TOXENV=cover install: - pip install tox - script: - - tox -e $TOX_ENV - + - tox after_script: - - if [ $TOX_ENV == "cover" ]; then + - if [ $TOXENV == "cover" ]; then pip install --quiet coveralls; coveralls; fi @@ -8,14 +8,11 @@ ISO 8601 date/time parser .. image:: https://coveralls.io/repos/gweis/isodate/badge.svg?branch=master :target: https://coveralls.io/r/gweis/isodate?branch=master :alt: Coveralls -.. image:: https://pypip.in/version/isodate/badge.svg - :target: https://pypi.python.org/pypi/isodate/ +.. image:: https://img.shields.io/pypi/v/isodate.svg + :target: https://pypi.python.org/pypi/isodate/ :alt: Latest Version -.. image:: https://pypip.in/download/isodate/badge.svg - :target: https://pypi.python.org/pypi/isodate/ - :alt: Downloads -.. image:: https://pypip.in/license/isodate/badge.svg - :target: https://pypi.python.org/pypi/isodate/ +.. image:: https://img.shields.io/pypi/l/isodate.svg + :target: https://pypi.python.org/pypi/isodate/ :alt: License diff --git a/bootstrap.py b/bootstrap.py deleted file mode 100644 index 716795f..0000000 --- a/bootstrap.py +++ /dev/null @@ -1,266 +0,0 @@ -############################################################################## -# -# Copyright (c) 2006 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Bootstrap a buildout-based project - -Simply run this script in a directory containing a buildout.cfg. -The script accepts buildout command-line options, so you can -use the -c option to specify an alternate configuration file. -""" - -import os, shutil, sys, tempfile, urllib, urllib2, subprocess -from optparse import OptionParser - -if sys.platform == 'win32': - def quote(c): - if ' ' in c: - return '"%s"' % c # work around spawn lamosity on windows - else: - return c -else: - quote = str - -# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. -stdout, stderr = subprocess.Popen( - [sys.executable, '-Sc', - 'try:\n' - ' import ConfigParser\n' - 'except ImportError:\n' - ' print 1\n' - 'else:\n' - ' print 0\n'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() -has_broken_dash_S = bool(int(stdout.strip())) - -# In order to be more robust in the face of system Pythons, we want to -# run without site-packages loaded. This is somewhat tricky, in -# particular because Python 2.6's distutils imports site, so starting -# with the -S flag is not sufficient. However, we'll start with that: -if not has_broken_dash_S and 'site' in sys.modules: - # We will restart with python -S. - args = sys.argv[:] - args[0:0] = [sys.executable, '-S'] - args = map(quote, args) - os.execv(sys.executable, args) -# Now we are running with -S. We'll get the clean sys.path, import site -# because distutils will do it later, and then reset the path and clean -# out any namespace packages from site-packages that might have been -# loaded by .pth files. -clean_path = sys.path[:] -import site # imported because of its side effects -sys.path[:] = clean_path -for k, v in sys.modules.items(): - if k in ('setuptools', 'pkg_resources') or ( - hasattr(v, '__path__') and - len(v.__path__) == 1 and - not os.path.exists(os.path.join(v.__path__[0], '__init__.py'))): - # This is a namespace package. Remove it. - sys.modules.pop(k) - -is_jython = sys.platform.startswith('java') - -setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' -distribute_source = 'http://python-distribute.org/distribute_setup.py' - - -# parsing arguments -def normalize_to_url(option, opt_str, value, parser): - if value: - if '://' not in value: # It doesn't smell like a URL. - value = 'file://%s' % ( - urllib.pathname2url( - os.path.abspath(os.path.expanduser(value))),) - if opt_str == '--download-base' and not value.endswith('/'): - # Download base needs a trailing slash to make the world happy. - value += '/' - else: - value = None - name = opt_str[2:].replace('-', '_') - setattr(parser.values, name, value) - -usage = '''\ -[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] - -Bootstraps a buildout-based project. - -Simply run this script in a directory containing a buildout.cfg, using the -Python that you want bin/buildout to use. - -Note that by using --setup-source and --download-base to point to -local resources, you can keep this script from going over the network. -''' - -parser = OptionParser(usage=usage) -parser.add_option("-v", "--version", dest="version", - help="use a specific zc.buildout version") -parser.add_option("-d", "--distribute", - action="store_true", dest="use_distribute", default=False, - help="Use Distribute rather than Setuptools.") -parser.add_option("--setup-source", action="callback", dest="setup_source", - callback=normalize_to_url, nargs=1, type="string", - help=("Specify a URL or file location for the setup file. " - "If you use Setuptools, this will default to " + - setuptools_source + "; if you use Distribute, this " - "will default to " + distribute_source + ".")) -parser.add_option("--download-base", action="callback", dest="download_base", - callback=normalize_to_url, nargs=1, type="string", - help=("Specify a URL or directory for downloading " - "zc.buildout and either Setuptools or Distribute. " - "Defaults to PyPI.")) -parser.add_option("--eggs", - help=("Specify a directory for storing eggs. Defaults to " - "a temporary directory that is deleted when the " - "bootstrap script completes.")) -parser.add_option("-t", "--accept-buildout-test-releases", - dest='accept_buildout_test_releases', - action="store_true", default=False, - help=("Normally, if you do not specify a --version, the " - "bootstrap script and buildout gets the newest " - "*final* versions of zc.buildout and its recipes and " - "extensions for you. If you use this flag, " - "bootstrap and buildout will get the newest releases " - "even if they are alphas or betas.")) -parser.add_option("-c", None, action="store", dest="config_file", - help=("Specify the path to the buildout configuration " - "file to be used.")) - -options, orig_args = parser.parse_args() - -args = [] - -# if -c was provided, we push it back into args for buildout's main function -if options.config_file is not None: - args += ['-c', options.config_file] - -if options.eggs: - eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) -else: - eggs_dir = tempfile.mkdtemp() - -if options.setup_source is None: - if options.use_distribute: - options.setup_source = distribute_source - else: - options.setup_source = setuptools_source - -if options.accept_buildout_test_releases: - args.append('buildout:accept-buildout-test-releases=true') - -try: - import pkg_resources - import setuptools # A flag. Sometimes pkg_resources is installed alone. - if not hasattr(pkg_resources, '_distribute'): - raise ImportError -except ImportError: - ez_code = urllib2.urlopen( - options.setup_source).read().replace('\r\n', '\n') - ez = {} - exec ez_code in ez - setup_args = dict(to_dir=eggs_dir, download_delay=0) - if options.download_base: - setup_args['download_base'] = options.download_base - if options.use_distribute: - setup_args['no_fake'] = True - ez['use_setuptools'](**setup_args) - if 'pkg_resources' in sys.modules: - reload(sys.modules['pkg_resources']) - import pkg_resources - # This does not (always?) update the default working set. We will - # do it. - for path in sys.path: - if path not in pkg_resources.working_set.entries: - pkg_resources.working_set.add_entry(path) - -cmd = [quote(sys.executable), - '-c', - quote('from setuptools.command.easy_install import main; main()'), - '-mqNxd', - quote(eggs_dir)] - -if not has_broken_dash_S: - cmd.insert(1, '-S') - -find_links = options.download_base -if not find_links: - find_links = os.environ.get('bootstrap-testing-find-links') -if find_links: - cmd.extend(['-f', quote(find_links)]) - -if options.use_distribute: - setup_requirement = 'distribute' -else: - setup_requirement = 'setuptools' -ws = pkg_resources.working_set -setup_requirement_path = ws.find( - pkg_resources.Requirement.parse(setup_requirement)).location -env = dict( - os.environ, - PYTHONPATH=setup_requirement_path) - -requirement = 'zc.buildout' -version = options.version -if version is None and not options.accept_buildout_test_releases: - # Figure out the most recent final version of zc.buildout. - import setuptools.package_index - _final_parts = '*final-', '*final' - - def _final_version(parsed_version): - for part in parsed_version: - if (part[:1] == '*') and (part not in _final_parts): - return False - return True - index = setuptools.package_index.PackageIndex( - search_path=[setup_requirement_path]) - if find_links: - index.add_find_links((find_links,)) - req = pkg_resources.Requirement.parse(requirement) - if index.obtain(req) is not None: - best = [] - bestv = None - for dist in index[req.project_name]: - distv = dist.parsed_version - if _final_version(distv): - if bestv is None or distv > bestv: - best = [dist] - bestv = distv - elif distv == bestv: - best.append(dist) - if best: - best.sort() - version = best[-1].version -if version: - requirement = '=='.join((requirement, version)) -cmd.append(requirement) - -if is_jython: - import subprocess - exitcode = subprocess.Popen(cmd, env=env).wait() -else: # Windows prefers this, apparently; otherwise we would prefer subprocess - exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) -if exitcode != 0: - sys.stdout.flush() - sys.stderr.flush() - print ("An error occurred when trying to install zc.buildout. " - "Look above this message for any errors that " - "were output by easy_install.") - sys.exit(exitcode) - -ws.add_entry(eggs_dir) -ws.require(requirement) -import zc.buildout.buildout -if orig_args: - # run buildout with commands passed to bootstrap.py, then actually bootstrap - zc.buildout.buildout.main(args + orig_args) -zc.buildout.buildout.main(args + ['bootstrap']) -if not options.eggs: # clean up temporary egg directory - shutil.rmtree(eggs_dir) diff --git a/buildout.cfg b/buildout.cfg deleted file mode 100644 index 5f8c446..0000000 --- a/buildout.cfg +++ /dev/null @@ -1,32 +0,0 @@ -[buildout] -develop = . -parts = isodate importchecker test pydev coverage coverage-report - -[isodate] -recipe = zc.recipe.egg -eggs = isodate -interpreter = python - -[pydev] -recipe = pb.recipes.pydev -eggs = ${isodate:eggs} - -[test] -recipe = zc.recipe.testrunner -eggs = isodate - -[coverage] -recipe = zc.recipe.testrunner -eggs = isodate -defaults = ['--coverage', '.'] - -[coverage-report] -recipe = zc.recipe.egg -eggs = z3c.coverage -scripts = coverage=coverage-report -arguments = ('parts/coverage', 'parts/coverage-report') - -[importchecker] -recipe = zc.recipe.egg -eggs = importchecker -arguments = "${buildout:directory}/src" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 @@ -26,32 +26,22 @@ # CONTRACT, STRICT LIABILITY, OR TORT ############################################################################## import os -import sys - -setupargs = {} - -try: - from setuptools import setup - setupargs['test_suite'] = 'isodate.tests.test_suite' - if sys.version[0] == '3': - setupargs['use_2to3'] = True -except ImportError: - from distutils.core import setup - if sys.version[0] == '3': - from distutils.command.build_py import build_py_2to3 - setupargs['cmdclass'] = {'build_py': build_py_2to3} +from setuptools import setup def read(*rnames): return open(os.path.join(os.path.dirname(__file__), *rnames)).read() + setup(name='isodate', - version='0.5.5.dev', + version='0.6.0.dev', packages=['isodate', 'isodate.tests'], package_dir={'': 'src'}, # dependencies: - # install_requires = [], + install_requires=[ + 'six' + ], # PyPI metadata author='Gerhard Weis', @@ -59,7 +49,7 @@ setup(name='isodate', description='An ISO 8601 date/time/duration parser and formatter', license='BSD', # keywords = '', - url='http://cheeseshop.python.org/pypi/isodate', + url='https://github.com/gweis/isodate/', long_description=(read('README.rst') + read('CHANGES.txt') + @@ -73,12 +63,13 @@ setup(name='isodate', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet', ('Topic :: Software Development :' ': Libraries :: Python Modules'), ], - **setupargs) + test_suite='isodate.tests.test_suite') diff --git a/src/isodate/duration.py b/src/isodate/duration.py index 5408315..c47b964 100644 --- a/src/isodate/duration.py +++ b/src/isodate/duration.py @@ -30,7 +30,7 @@ This module defines a Duration class. The class Duration allows to define durations in years and months and can be used as limited replacement for timedelta objects. ''' -from datetime import date, datetime, timedelta +from datetime import timedelta from decimal import Decimal, ROUND_FLOOR @@ -121,7 +121,10 @@ class Duration(object): if self.years: params.append('%d years' % self.years) if self.months: - params.append('%d months' % self.months) + fmt = "%d months" + if self.months <= 1: + fmt = "%d month" + params.append(fmt % self.months) params.append(str(self.tdelta)) return ', '.join(params) @@ -156,16 +159,15 @@ class Duration(object): Durations can be added with Duration, timedelta, date and datetime objects. ''' - if isinstance(other, timedelta): - newduration = Duration(years=self.years, months=self.months) - newduration.tdelta = self.tdelta + other - return newduration if isinstance(other, Duration): newduration = Duration(years=self.years + other.years, months=self.months + other.months) newduration.tdelta = self.tdelta + other.tdelta return newduration - if isinstance(other, (date, datetime)): + try: + # try anything that looks like a date or datetime + # other has attributes year, month, day + # and relies on 'timedelta + other' being implemented if (not(float(self.years).is_integer() and float(self.months).is_integer())): raise ValueError('fractional years or months not supported' @@ -179,35 +181,24 @@ class Duration(object): else: newday = other.day newdt = other.replace(year=newyear, month=newmonth, day=newday) + # does a timedelta + date/datetime return self.tdelta + newdt - raise TypeError('unsupported operand type(s) for +: %s and %s' % - (self.__class__, other.__class__)) - - def __radd__(self, other): - ''' - Add durations to timedelta, date and datetime objects. - ''' - if isinstance(other, timedelta): + except AttributeError: + # other probably was not a date/datetime compatible object + pass + try: + # try if other is a timedelta + # relies on timedelta + timedelta supported newduration = Duration(years=self.years, months=self.months) newduration.tdelta = self.tdelta + other return newduration - if isinstance(other, (date, datetime)): - if (not(float(self.years).is_integer() and - float(self.months).is_integer())): - raise ValueError('fractional years or months not supported' - ' for date calculations') - newmonth = other.month + self.months - carry, newmonth = fquotmod(newmonth, 1, 13) - newyear = other.year + self.years + carry - maxdays = max_days_in_month(newyear, newmonth) - if other.day > maxdays: - newday = maxdays - else: - newday = other.day - newdt = other.replace(year=newyear, month=newmonth, day=newday) - return newdt + self.tdelta - raise TypeError('unsupported operand type(s) for +: %s and %s' % - (other.__class__, self.__class__)) + except AttributeError: + # ignore ... other probably was not a timedelt compatible object + pass + # we have tried everything .... return a NotImplemented + return NotImplemented + + __radd__ = __add__ def __mul__(self, other): if isinstance(other, int): @@ -216,19 +207,9 @@ class Duration(object): months=self.months * other) newduration.tdelta = self.tdelta * other return newduration - raise TypeError('unsupported operand type(s) for *: %s and %s' % - (self.__class__, other.__class__)) + return NotImplemented - def __rmul__(self, other): - - if isinstance(other, int): - newduration = Duration( - years=self.years * other, - months=self.months * other) - newduration.tdelta = self.tdelta * other - return newduration - raise TypeError('unsupported operand type(s) for *: %s and %s' % - (other.__class__, self.__class__)) + __rmul__ = __mul__ def __sub__(self, other): ''' @@ -240,20 +221,37 @@ class Duration(object): months=self.months - other.months) newduration.tdelta = self.tdelta - other.tdelta return newduration - if isinstance(other, timedelta): + try: + # do maths with our timedelta object .... newduration = Duration(years=self.years, months=self.months) newduration.tdelta = self.tdelta - other return newduration - raise TypeError('unsupported operand type(s) for -: %s and %s' % - (self.__class__, other.__class__)) + except TypeError: + # looks like timedelta - other is not implemented + pass + return NotImplemented def __rsub__(self, other): ''' It is possible to subtract Duration objecs from date, datetime and timedelta objects. + + TODO: there is some weird behaviour in date - timedelta ... + if timedelta has seconds or microseconds set, then + date - timedelta != date + (-timedelta) + for now we follow this behaviour to avoid surprises when mixing + timedeltas with Durations, but in case this ever changes in + the stdlib we can just do: + return -self + other + instead of all the current code ''' - # print '__rsub__:', self, other - if isinstance(other, (date, datetime)): + if isinstance(other, timedelta): + tmpdur = Duration() + tmpdur.tdelta = other + return tmpdur - self + try: + # check if other behaves like a date/datetime object + # does it have year, month, day and replace? if (not(float(self.years).is_integer() and float(self.months).is_integer())): raise ValueError('fractional years or months not supported' @@ -268,27 +266,26 @@ class Duration(object): newday = other.day newdt = other.replace(year=newyear, month=newmonth, day=newday) return newdt - self.tdelta - if isinstance(other, timedelta): - tmpdur = Duration() - tmpdur.tdelta = other - return tmpdur - self - raise TypeError('unsupported operand type(s) for -: %s and %s' % - (other.__class__, self.__class__)) + except AttributeError: + # other probably was not compatible with data/datetime + pass + return NotImplemented def __eq__(self, other): ''' If the years, month part and the timedelta part are both equal, then the two Durations are considered equal. ''' - if ((isinstance(other, timedelta) and - self.years == 0 and self.months == 0)): + if isinstance(other, Duration): + if (((self.years * 12 + self.months) == + (other.years * 12 + other.months) and + self.tdelta == other.tdelta)): + return True + return False + # check if other con be compared against timedelta object + # will raise an AssertionError when optimisation is off + if self.years == 0 and self.months == 0: return self.tdelta == other - if not isinstance(other, Duration): - return NotImplemented - if (((self.years * 12 + self.months) == - (other.years * 12 + other.months) and - self.tdelta == other.tdelta)): - return True return False def __ne__(self, other): @@ -296,17 +293,17 @@ class Duration(object): If the years, month part or the timedelta part is not equal, then the two Durations are considered not equal. ''' - if ((isinstance(other, timedelta) and - self.years == 0 and - self.months == 0)): + if isinstance(other, Duration): + if (((self.years * 12 + self.months) != + (other.years * 12 + other.months) or + self.tdelta != other.tdelta)): + return True + return False + # check if other can be compared against timedelta object + # will raise an AssertionError when optimisation is off + if self.years == 0 and self.months == 0: return self.tdelta != other - if not isinstance(other, Duration): - return NotImplemented - if (((self.years * 12 + self.months) != - (other.years * 12 + other.months) or - self.tdelta != other.tdelta)): - return True - return False + return True def totimedelta(self, start=None, end=None): ''' diff --git a/src/isodate/isoduration.py b/src/isodate/isoduration.py index 6da69f5..88829f7 100644 --- a/src/isodate/isoduration.py +++ b/src/isodate/isoduration.py @@ -34,6 +34,8 @@ from datetime import timedelta from decimal import Decimal import re +from six import string_types + from isodate.duration import Duration from isodate.isoerror import ISO8601Error from isodate.isodatetime import parse_datetime @@ -80,7 +82,7 @@ def parse_duration(datestring): The alternative format does not support durations with years, months or days set to 0. """ - if not isinstance(datestring, basestring): + if not isinstance(datestring, string_types): raise TypeError("Expecting a string %r" % datestring) match = ISO8601_PERIOD_REGEX.match(datestring) if not match: diff --git a/src/isodate/isotime.py b/src/isodate/isotime.py index 9650cda..113d34b 100644 --- a/src/isodate/isotime.py +++ b/src/isodate/isotime.py @@ -125,7 +125,7 @@ def parse_time(timestring): if 'second' in groups: # round to microseconds if fractional seconds are more precise second = Decimal(groups['second']).quantize(Decimal('.000001')) - microsecond = (second - int(second)) * long(1e6) + microsecond = (second - int(second)) * int(1e6) # int(...) ... no rounding # to_integral() ... rounding return time(int(groups['hour']), int(groups['minute']), @@ -134,7 +134,7 @@ def parse_time(timestring): if 'minute' in groups: minute = Decimal(groups['minute']) second = (minute - int(minute)) * 60 - microsecond = (second - int(second)) * long(1e6) + microsecond = (second - int(second)) * int(1e6) return time(int(groups['hour']), int(minute), int(second), int(microsecond.to_integral()), tzinfo) else: @@ -142,7 +142,7 @@ def parse_time(timestring): hour = Decimal(groups['hour']) minute = (hour - int(hour)) * 60 second = (minute - int(minute)) * 60 - microsecond = (second - int(second)) * long(1e6) + microsecond = (second - int(second)) * int(1e6) return time(int(hour), int(minute), int(second), int(microsecond.to_integral()), tzinfo) raise ISO8601Error('Unrecognised ISO 8601 time format: %r' % timestring) diff --git a/src/isodate/tests/__init__.py b/src/isodate/tests/__init__.py index 09dba2e..b1d46bd 100644 --- a/src/isodate/tests/__init__.py +++ b/src/isodate/tests/__init__.py @@ -46,5 +46,6 @@ def test_suite(): test_pickle.test_suite(), ]) + if __name__ == '__main__': unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_date.py b/src/isodate/tests/test_date.py index 2519e68..6ee89cb 100644 --- a/src/isodate/tests/test_date.py +++ b/src/isodate/tests/test_date.py @@ -127,5 +127,6 @@ def test_suite(): def load_tests(loader, tests, pattern): return test_suite() + if __name__ == '__main__': unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_datetime.py b/src/isodate/tests/test_datetime.py index ddad5da..3cdfe42 100644 --- a/src/isodate/tests/test_datetime.py +++ b/src/isodate/tests/test_datetime.py @@ -142,5 +142,6 @@ def test_suite(): def load_tests(loader, tests, pattern): return test_suite() + if __name__ == '__main__': unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_duration.py b/src/isodate/tests/test_duration.py index 0b80a54..b0e0332 100644 --- a/src/isodate/tests/test_duration.py +++ b/src/isodate/tests/test_duration.py @@ -313,6 +313,10 @@ class DurationTest(unittest.TestCase): self.assertEqual('10 years, 10 months, 10 days, 0:00:10', str(dur)) self.assertEqual('isodate.duration.Duration(10, 10, 0,' ' years=10, months=10)', repr(dur)) + dur = Duration(months=0) + self.assertEqual('0:00:00', str(dur)) + dur = Duration(months=1) + self.assertEqual('1 month, 0:00:00', str(dur)) def test_hash(self): ''' @@ -597,5 +601,6 @@ def test_suite(): def load_tests(loader, tests, pattern): return test_suite() + if __name__ == '__main__': unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_pickle.py b/src/isodate/tests/test_pickle.py index b52f8cb..7fc6213 100644 --- a/src/isodate/tests/test_pickle.py +++ b/src/isodate/tests/test_pickle.py @@ -1,5 +1,7 @@ import unittest -import cPickle as pickle + +from six.moves import cPickle as pickle + import isodate @@ -31,11 +33,17 @@ class TestPickle(unittest.TestCase): pikl = pickle.dumps(dur, proto) if dur != pickle.loads(pikl): raise Exception("not equal") - except Exception, e: + except Exception as e: failed.append("pickle proto %d failed (%s)" % (proto, repr(e))) self.assertEqual(len(failed), 0, "pickle protos failed: %s" % str(failed)) + def test_pickle_utc(self): + ''' + isodate.UTC objects remain the same after pickling. + ''' + self.assertTrue(isodate.UTC is pickle.loads(pickle.dumps(isodate.UTC))) + def test_suite(): ''' @@ -50,5 +58,6 @@ def test_suite(): def load_tests(loader, tests, pattern): return test_suite() + if __name__ == '__main__': unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_strf.py b/src/isodate/tests/test_strf.py index 37a135b..1aa76c7 100644 --- a/src/isodate/tests/test_strf.py +++ b/src/isodate/tests/test_strf.py @@ -131,5 +131,6 @@ def test_suite(): def load_tests(loader, tests, pattern): return test_suite() + if __name__ == '__main__': unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tests/test_time.py b/src/isodate/tests/test_time.py index cc5ec08..7dfd0c0 100644 --- a/src/isodate/tests/test_time.py +++ b/src/isodate/tests/test_time.py @@ -139,5 +139,6 @@ def test_suite(): def load_tests(loader, tests, pattern): return test_suite() + if __name__ == '__main__': unittest.main(defaultTest='test_suite') diff --git a/src/isodate/tzinfo.py b/src/isodate/tzinfo.py index b41f058..5d3a42d 100644 --- a/src/isodate/tzinfo.py +++ b/src/isodate/tzinfo.py @@ -36,10 +36,24 @@ class Utc(tzinfo): ''' return ZERO + def __reduce__(self): + ''' + When unpickling a Utc object, return the default instance below, UTC. + ''' + return _Utc, () + + UTC = Utc() # the default instance for UTC. +def _Utc(): + ''' + Helper function for unpickling a Utc object. + ''' + return UTC + + class FixedOffset(tzinfo): ''' A class building tzinfo objects for fixed-offset time zones. @@ -138,5 +152,6 @@ class LocalTimezone(tzinfo): tt = time.localtime(stamp) return tt.tm_isdst > 0 -LOCAL = LocalTimezone() + # the default instance for local time zone. +LOCAL = LocalTimezone() @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py32,py33,py34,pypy,pypy3,flake,cover +envlist = py26,py27,py33,py34,py35,py36,pypy,pypy3,flake,cover [testenv] deps = @@ -7,13 +7,13 @@ commands = {envpython} setup.py test [testenv:flake] -basepython = python2.7 +basepython = python3.5 commands= pip install --quiet flake8 {envpython} setup.py clean --all flake8 [testenv:cover] -basepython = python2.7 +basepython = python3.5 commands = pip install --quiet coverage {envpython} setup.py clean --all |