From 2ace19f248305e0063f4350b77aad91f00a801f6 Mon Sep 17 00:00:00 2001 From: Gerhard Weis Date: Tue, 8 Oct 2024 12:12:24 +1000 Subject: [PATCH] modernise packaging update actions move to pytest move tests out of src --- .coveragerc | 3 - .github/workflows/lint.yml | 10 +- .github/workflows/test.yml | 13 +- .gitignore | 3 + .pre-commit-config.yaml | 10 +- CHANGES.txt | 15 +- MANIFEST.in | 2 - README.rst | 7 +- pyproject.toml | 60 ++ setup.py | 43 -- src/isodate/__init__.py | 66 +- src/isodate/duration.py | 3 +- src/isodate/isodates.py | 3 +- src/isodate/isodatetime.py | 4 +- src/isodate/isoduration.py | 7 +- src/isodate/isostrf.py | 1 + src/isodate/isotime.py | 5 +- src/isodate/isotzinfo.py | 3 +- src/isodate/tests/__init__.py | 36 -- src/isodate/tests/test_date.py | 110 ---- src/isodate/tests/test_duration.py | 563 ------------------ src/isodate/tests/test_pickle.py | 62 -- src/isodate/tests/test_strf.py | 127 ---- src/isodate/tzinfo.py | 3 +- tests/test_date.py | 83 +++ {src/isodate/tests => tests}/test_datetime.py | 97 ++- tests/test_duration.py | 436 ++++++++++++++ tests/test_pickle.py | 32 + tests/test_strf.py | 83 +++ {src/isodate/tests => tests}/test_time.py | 89 ++- tox.ini | 13 +- 31 files changed, 873 insertions(+), 1119 deletions(-) delete mode 100644 .coveragerc delete mode 100644 MANIFEST.in create mode 100644 pyproject.toml delete mode 100644 setup.py delete mode 100644 src/isodate/tests/__init__.py delete mode 100644 src/isodate/tests/test_date.py delete mode 100644 src/isodate/tests/test_duration.py delete mode 100644 src/isodate/tests/test_pickle.py delete mode 100644 src/isodate/tests/test_strf.py create mode 100644 tests/test_date.py rename {src/isodate/tests => tests}/test_datetime.py (61%) create mode 100644 tests/test_duration.py create mode 100644 tests/test_pickle.py create mode 100644 tests/test_strf.py rename {src/isodate/tests => tests}/test_time.py (59%) diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 6d23d9f..0000000 --- a/.coveragerc +++ /dev/null @@ -1,3 +0,0 @@ -[run] -include=src/isodate/* -omit=src/isodate/tests/* diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 769ea4e..4cc6ce5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,9 +4,11 @@ on: [push, pull_request, workflow_dispatch] jobs: lint: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - - uses: pre-commit/action@v2.0.3 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index edf58a9..e93c438 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10"] + python-version: ["pypy-3.9", "3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest, macos-latest, windows-latest] include: # Include new variables for Codecov @@ -20,27 +20,24 @@ jobs: - { codecov-flag: GHA_Windows, os: windows-latest } steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip - cache-dependency-path: "setup.py" - name: Install dependencies run: | - python -m pip install -U pip - python -m pip install -U wheel - python -m pip install -U tox + python -m pip install -U pip wheel tox - name: Tox tests run: | tox -e py - name: Upload coverage - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v4 with: flags: ${{ matrix.codecov-flag }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index fdacade..ac15e51 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ parts .coverage coverage.xml htmlcov +src/isodate/version.py +.eggs +.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f04e3f..02a2647 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,24 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v3.17.0 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py39-plus] - repo: https://github.com/pycqa/flake8 - rev: 4.0.1 + rev: 7.1.1 hooks: - id: flake8 args: - "--max-line-length=88" - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: python-check-blanket-noqa - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v5.0.0 hooks: - id: check-merge-conflict - id: check-yaml diff --git a/CHANGES.txt b/CHANGES.txt index c526264..7500705 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,12 +2,23 @@ CHANGES ======= -0.7.0 (unreleased) +0.7.1 (unreleased) +------------------ + +- no changes yet + + +0.7.0 (2024-10-08) ------------------ - drop end of life python versions - Don't match garbage characters at the end of parsed strings #16 (Gabriel de Perthuis) -- Breaking: fractional seconds are cut off to microseconds (round down) + + +Potentially breaking changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- Fractional seconds are cut off to microseconds (always round down) - Allow control over return type of parse_duration #64 (Felix Claessen) diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 127f5af..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include CHANGES.txt -include TODO.txt diff --git a/README.rst b/README.rst index d030a4a..f8608de 100644 --- a/README.rst +++ b/README.rst @@ -33,7 +33,7 @@ As this module maps ISO 8601 dates/times to standard Python data types, like all possible ISO 8601 dates/times. For instance, dates before 0001-01-01 are not allowed by the Python *date* and *datetime* classes. Additionally fractional seconds are limited to microseconds. That means if the parser finds -for instance nanoseconds it will round it to microseconds. +for instance nanoseconds it will round it down to microseconds. Documentation ------------- @@ -88,8 +88,7 @@ Installation This module can easily be installed with Python standard installation methods. -Either use *python setup.py install* or in case you have *setuptools* or -*distribute* available, you can also use *easy_install*. +Use *pip install isodate*. Limitations ----------- @@ -113,4 +112,4 @@ the methods and their limitations. The source release provides a *setup.py* script, which can be used to run the unit tests included. -Source code is available at ``_. +Source code is available at ``_. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e18d52e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[project] +name = "isodate" +description = "An ISO 8601 date/time/duration parser and formatter" +# TODO: long_description = "file: README.rst, CHANGES.txt, TODO.txt" +# TODO: url = "https://github.com/gweis/isodate/" +authors = [{name="Gerhard Weis"}] +# keywords = +license = {file = "LICENSE"} +# license = {text="BSD-3-Clause"} +#TODO: license_files = "LICENSE" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Python Modules", +] +# requires-python = ">3.7" +dynamic = ["version"] + + +[build-system] +requires = ["setuptools", "setuptools_scm[toml]"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +write_to = "src/isodate/version.py" +fallback_version = "0.0.0.dev0" + + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = [ + # treat all warnings as errors + "error", + # ignore: + # e.g.: + # ignore:jsonschema.RefResolver is deprecated as of v4.18.0 +] +junit_family = "xunit2" + +[tool.coverage.run] +source_pkgs = ["isodate"] +omit = ["tests/"] + +[tool.black] +line-length = 88 + +[tool.isort] +profile = "black" diff --git a/setup.py b/setup.py deleted file mode 100644 index f95e8c2..0000000 --- a/setup.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python -import os -from setuptools import setup - - -def read(*rnames): - with open(os.path.join(os.path.dirname(__file__), *rnames)) as read_file: - return read_file.read() - - -setup( - name="isodate", - version="0.7.0.dev0", - packages=["isodate", "isodate.tests"], - package_dir={"": "src"}, - # PyPI metadata - author="Gerhard Weis", - author_email="gerhard.weis@proclos.com", - description="An ISO 8601 date/time/duration parser and formatter", - license="BSD-3-Clause", - license_files=("LICENSE",), - # keywords = '', - url="https://github.com/gweis/isodate/", - long_description=(read("README.rst") + read("CHANGES.txt") + read("TODO.txt")), - python_requires=">=3.7", - classifiers=[ - "Development Status :: 4 - Beta", - # 'Environment :: Web Environment', - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Internet", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - test_suite="isodate.tests.test_suite", -) diff --git a/src/isodate/__init__.py b/src/isodate/__init__.py index 3b1e147..d9cca6a 100644 --- a/src/isodate/__init__.py +++ b/src/isodate/__init__.py @@ -4,30 +4,51 @@ This module contains also various pre-defined ISO 8601 format strings. """ -from isodate.isodates import parse_date, date_isoformat -from isodate.isotime import parse_time, time_isoformat -from isodate.isodatetime import parse_datetime, datetime_isoformat -from isodate.isoduration import parse_duration, duration_isoformat + +from isodate.duration import Duration +from isodate.isodates import date_isoformat, parse_date +from isodate.isodatetime import datetime_isoformat, parse_datetime +from isodate.isoduration import duration_isoformat, parse_duration from isodate.isoerror import ISO8601Error +from isodate.isostrf import ( + D_ALT_BAS, + D_ALT_BAS_ORD, + D_ALT_EXT, + D_ALT_EXT_ORD, + D_DEFAULT, + D_WEEK, + DATE_BAS_COMPLETE, + DATE_BAS_MONTH, + DATE_BAS_ORD_COMPLETE, + DATE_BAS_WEEK, + DATE_BAS_WEEK_COMPLETE, + DATE_CENTURY, + DATE_EXT_COMPLETE, + DATE_EXT_MONTH, + DATE_EXT_ORD_COMPLETE, + DATE_EXT_WEEK, + DATE_EXT_WEEK_COMPLETE, + DATE_YEAR, + DT_BAS_COMPLETE, + DT_BAS_ORD_COMPLETE, + DT_BAS_WEEK_COMPLETE, + DT_EXT_COMPLETE, + DT_EXT_ORD_COMPLETE, + DT_EXT_WEEK_COMPLETE, + TIME_BAS_COMPLETE, + TIME_BAS_MINUTE, + TIME_EXT_COMPLETE, + TIME_EXT_MINUTE, + TIME_HOUR, + TZ_BAS, + TZ_EXT, + TZ_HOUR, + strftime, +) +from isodate.isotime import parse_time, time_isoformat from isodate.isotzinfo import parse_tzinfo, tz_isoformat -from isodate.tzinfo import UTC, FixedOffset, LOCAL -from isodate.duration import Duration -from isodate.isostrf import strftime -from isodate.isostrf import DATE_BAS_COMPLETE, DATE_BAS_ORD_COMPLETE -from isodate.isostrf import DATE_BAS_WEEK, DATE_BAS_WEEK_COMPLETE -from isodate.isostrf import DATE_CENTURY, DATE_EXT_COMPLETE -from isodate.isostrf import DATE_EXT_ORD_COMPLETE, DATE_EXT_WEEK -from isodate.isostrf import DATE_EXT_WEEK_COMPLETE, DATE_YEAR -from isodate.isostrf import DATE_BAS_MONTH, DATE_EXT_MONTH -from isodate.isostrf import TIME_BAS_COMPLETE, TIME_BAS_MINUTE -from isodate.isostrf import TIME_EXT_COMPLETE, TIME_EXT_MINUTE -from isodate.isostrf import TIME_HOUR -from isodate.isostrf import TZ_BAS, TZ_EXT, TZ_HOUR -from isodate.isostrf import DT_BAS_COMPLETE, DT_EXT_COMPLETE -from isodate.isostrf import DT_BAS_ORD_COMPLETE, DT_EXT_ORD_COMPLETE -from isodate.isostrf import DT_BAS_WEEK_COMPLETE, DT_EXT_WEEK_COMPLETE -from isodate.isostrf import D_DEFAULT, D_WEEK, D_ALT_EXT, D_ALT_BAS -from isodate.isostrf import D_ALT_BAS_ORD, D_ALT_EXT_ORD +from isodate.tzinfo import LOCAL, UTC, FixedOffset +from isodate.version import version as __version__ __all__ = [ "parse_date", @@ -78,4 +99,5 @@ "D_ALT_BAS", "D_ALT_BAS_ORD", "D_ALT_EXT_ORD", + "__version__", ] diff --git a/src/isodate/duration.py b/src/isodate/duration.py index d533a6f..bc8a5cb 100644 --- a/src/isodate/duration.py +++ b/src/isodate/duration.py @@ -4,8 +4,9 @@ The class Duration allows to define durations in years and months and can be used as limited replacement for timedelta objects. """ + from datetime import timedelta -from decimal import Decimal, ROUND_FLOOR +from decimal import ROUND_FLOOR, Decimal def fquotmod(val, low, high): diff --git a/src/isodate/isodates.py b/src/isodate/isodates.py index 3c5c095..d32fe25 100644 --- a/src/isodate/isodates.py +++ b/src/isodate/isodates.py @@ -6,11 +6,12 @@ standard. The only limitations it has, are given by the Python datetime.date implementation, which does not support dates before 0001-01-01. """ + import re from datetime import date, timedelta -from isodate.isostrf import strftime, DATE_EXT_COMPLETE from isodate.isoerror import ISO8601Error +from isodate.isostrf import DATE_EXT_COMPLETE, strftime DATE_REGEX_CACHE = {} # A dictionary to cache pre-compiled regular expressions. diff --git a/src/isodate/isodatetime.py b/src/isodate/isodatetime.py index 68adc7e..1b20805 100644 --- a/src/isodate/isodatetime.py +++ b/src/isodate/isodatetime.py @@ -4,12 +4,12 @@ For this job it uses the parse_date and parse_time methods defined in date and time module. """ + from datetime import datetime -from isodate.isostrf import strftime -from isodate.isostrf import DATE_EXT_COMPLETE, TIME_EXT_COMPLETE, TZ_EXT from isodate.isodates import parse_date from isodate.isoerror import ISO8601Error +from isodate.isostrf import DATE_EXT_COMPLETE, TIME_EXT_COMPLETE, TZ_EXT, strftime from isodate.isotime import parse_time diff --git a/src/isodate/isoduration.py b/src/isodate/isoduration.py index fc536a1..4f1755b 100644 --- a/src/isodate/isoduration.py +++ b/src/isodate/isoduration.py @@ -4,14 +4,15 @@ It also provides a wrapper to strftime. This wrapper makes it easier to format timedelta or Duration instances as ISO conforming strings. """ + +import re from datetime import timedelta from decimal import Decimal -import re from isodate.duration import Duration -from isodate.isoerror import ISO8601Error from isodate.isodatetime import parse_datetime -from isodate.isostrf import strftime, D_DEFAULT +from isodate.isoerror import ISO8601Error +from isodate.isostrf import D_DEFAULT, strftime ISO8601_PERIOD_REGEX = re.compile( r"^(?P[+-])?" diff --git a/src/isodate/isostrf.py b/src/isodate/isostrf.py index 94ce93f..455ce97 100644 --- a/src/isodate/isostrf.py +++ b/src/isodate/isostrf.py @@ -7,6 +7,7 @@ pr-defined format strings in this module to make ease producing of ISO 8601 conforming strings. """ + import re from datetime import date, timedelta diff --git a/src/isodate/isotime.py b/src/isodate/isotime.py index 619e4aa..f74ef5d 100644 --- a/src/isodate/isotime.py +++ b/src/isodate/isotime.py @@ -5,12 +5,13 @@ It supports all basic and extended formats including time zone specifications as described in the ISO standard. """ + import re -from decimal import Decimal, ROUND_FLOOR from datetime import time +from decimal import ROUND_FLOOR, Decimal -from isodate.isostrf import strftime, TIME_EXT_COMPLETE, TZ_EXT from isodate.isoerror import ISO8601Error +from isodate.isostrf import TIME_EXT_COMPLETE, TZ_EXT, strftime from isodate.isotzinfo import TZ_REGEX, build_tzinfo TIME_REGEX_CACHE = [] diff --git a/src/isodate/isotzinfo.py b/src/isodate/isotzinfo.py index ec6dd17..54f36de 100644 --- a/src/isodate/isotzinfo.py +++ b/src/isodate/isotzinfo.py @@ -3,10 +3,11 @@ It offers a function to parse the time zone offset as specified by ISO 8601. """ + import re from isodate.isoerror import ISO8601Error -from isodate.tzinfo import UTC, FixedOffset, ZERO +from isodate.tzinfo import UTC, ZERO, FixedOffset TZ_REGEX = ( r"(?P(Z|(?P[+-])" r"(?P[0-9]{2})(:?(?P[0-9]{2}))?)?)" diff --git a/src/isodate/tests/__init__.py b/src/isodate/tests/__init__.py deleted file mode 100644 index ea0f433..0000000 --- a/src/isodate/tests/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Collect all test suites into one TestSuite instance. -""" - -import unittest -import warnings -from isodate.tests import ( - test_date, - test_time, - test_datetime, - test_duration, - test_strf, - test_pickle, -) - - -def test_suite(): - """ - Return a new TestSuite instance consisting of all available TestSuites. - """ - warnings.filterwarnings("error", module=r"isodate(\..)*") - - return unittest.TestSuite( - [ - test_date.test_suite(), - test_time.test_suite(), - test_datetime.test_suite(), - test_duration.test_suite(), - test_strf.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 deleted file mode 100644 index cee3903..0000000 --- a/src/isodate/tests/test_date.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Test cases for the isodate module. -""" -import unittest -from datetime import date -from isodate import parse_date, ISO8601Error, date_isoformat -from isodate import DATE_CENTURY, DATE_YEAR -from isodate import DATE_BAS_MONTH, DATE_EXT_MONTH -from isodate import DATE_EXT_COMPLETE, DATE_BAS_COMPLETE -from isodate import DATE_BAS_ORD_COMPLETE, DATE_EXT_ORD_COMPLETE -from isodate import DATE_BAS_WEEK, DATE_BAS_WEEK_COMPLETE -from isodate import DATE_EXT_WEEK, DATE_EXT_WEEK_COMPLETE - -# the following list contains tuples of ISO date strings and the expected -# result from the parse_date method. A result of None means an ISO8601Error -# is expected. The test cases are grouped into dates with 4 digit years -# and 6 digit years. -TEST_CASES = { - 4: [ - ("19", date(1901, 1, 1), DATE_CENTURY), - ("1985", date(1985, 1, 1), DATE_YEAR), - ("1985-04", date(1985, 4, 1), DATE_EXT_MONTH), - ("198504", date(1985, 4, 1), DATE_BAS_MONTH), - ("1985-04-12", date(1985, 4, 12), DATE_EXT_COMPLETE), - ("19850412", date(1985, 4, 12), DATE_BAS_COMPLETE), - ("1985102", date(1985, 4, 12), DATE_BAS_ORD_COMPLETE), - ("1985-102", date(1985, 4, 12), DATE_EXT_ORD_COMPLETE), - ("1985W155", date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE), - ("1985-W15-5", date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE), - ("1985W15", date(1985, 4, 8), DATE_BAS_WEEK), - ("1985-W15", date(1985, 4, 8), DATE_EXT_WEEK), - ("1989-W15", date(1989, 4, 10), DATE_EXT_WEEK), - ("1989-W15-5", date(1989, 4, 14), DATE_EXT_WEEK_COMPLETE), - ("1-W1-1", None, DATE_BAS_WEEK_COMPLETE), - ], - 6: [ - ("+0019", date(1901, 1, 1), DATE_CENTURY), - ("+001985", date(1985, 1, 1), DATE_YEAR), - ("+001985-04", date(1985, 4, 1), DATE_EXT_MONTH), - ("+001985-04-12", date(1985, 4, 12), DATE_EXT_COMPLETE), - ("+0019850412", date(1985, 4, 12), DATE_BAS_COMPLETE), - ("+001985102", date(1985, 4, 12), DATE_BAS_ORD_COMPLETE), - ("+001985-102", date(1985, 4, 12), DATE_EXT_ORD_COMPLETE), - ("+001985W155", date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE), - ("+001985-W15-5", date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE), - ("+001985W15", date(1985, 4, 8), DATE_BAS_WEEK), - ("+001985-W15", date(1985, 4, 8), DATE_EXT_WEEK), - ], -} - - -def create_testcase(yeardigits, datestring, expectation, format): - """ - Create a TestCase class for a specific test. - - This allows having a separate TestCase for each test tuple from the - TEST_CASES list, so that a failed test won't stop other tests. - """ - - class TestDate(unittest.TestCase): - """ - A test case template to parse an ISO date string into a date - object. - """ - - def test_parse(self): - """ - Parse an ISO date string and compare it to the expected value. - """ - if expectation is None: - self.assertRaises(ISO8601Error, parse_date, datestring, yeardigits) - else: - result = parse_date(datestring, yeardigits) - self.assertEqual(result, expectation) - - def test_format(self): - """ - Take date object and create ISO string from it. - This is the reverse test to test_parse. - """ - if expectation is None: - self.assertRaises( - AttributeError, date_isoformat, expectation, format, yeardigits - ) - else: - self.assertEqual( - date_isoformat(expectation, format, yeardigits), datestring - ) - - return unittest.TestLoader().loadTestsFromTestCase(TestDate) - - -def test_suite(): - """ - Construct a TestSuite instance for all test cases. - """ - suite = unittest.TestSuite() - for yeardigits, tests in TEST_CASES.items(): - for datestring, expectation, format in tests: - suite.addTest(create_testcase(yeardigits, datestring, expectation, format)) - return suite - - -# load_tests Protocol -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 deleted file mode 100644 index 20c3f2d..0000000 --- a/src/isodate/tests/test_duration.py +++ /dev/null @@ -1,563 +0,0 @@ -""" -Test cases for the isoduration module. -""" -import unittest -import operator -from datetime import timedelta, date, datetime - -from isodate import Duration, parse_duration, ISO8601Error -from isodate import D_DEFAULT, D_WEEK, D_ALT_EXT, duration_isoformat - -# the following list contains tuples of ISO duration strings and the expected -# result from the parse_duration method. A result of None means an ISO8601Error -# is expected. -PARSE_TEST_CASES = { - "P18Y9M4DT11H9M8S": (Duration(4, 8, 0, 0, 9, 11, 0, 9, 18), D_DEFAULT, None), - "P2W": (timedelta(weeks=2), D_WEEK, None), - "P3Y6M4DT12H30M5S": (Duration(4, 5, 0, 0, 30, 12, 0, 6, 3), D_DEFAULT, None), - "P23DT23H": (timedelta(hours=23, days=23), D_DEFAULT, None), - "P4Y": (Duration(years=4), D_DEFAULT, None), - "P1M": (Duration(months=1), D_DEFAULT, None), - "PT1M": (timedelta(minutes=1), D_DEFAULT, None), - "P0.5Y": (Duration(years=0.5), D_DEFAULT, None), - "PT36H": (timedelta(hours=36), D_DEFAULT, "P1DT12H"), - "P1DT12H": (timedelta(days=1, hours=12), D_DEFAULT, None), - "+P11D": (timedelta(days=11), D_DEFAULT, "P11D"), - "-P2W": (timedelta(weeks=-2), D_WEEK, None), - "-P2.2W": (timedelta(weeks=-2.2), D_DEFAULT, "-P15DT9H36M"), - "P1DT2H3M4S": (timedelta(days=1, hours=2, minutes=3, seconds=4), D_DEFAULT, None), - "P1DT2H3M": (timedelta(days=1, hours=2, minutes=3), D_DEFAULT, None), - "P1DT2H": (timedelta(days=1, hours=2), D_DEFAULT, None), - "PT2H": (timedelta(hours=2), D_DEFAULT, None), - "PT2.3H": (timedelta(hours=2.3), D_DEFAULT, "PT2H18M"), - "PT2H3M4S": (timedelta(hours=2, minutes=3, seconds=4), D_DEFAULT, None), - "PT3M4S": (timedelta(minutes=3, seconds=4), D_DEFAULT, None), - "PT22S": (timedelta(seconds=22), D_DEFAULT, None), - "PT22.22S": (timedelta(seconds=22.22), "PT%S.%fS", "PT22.220000S"), - "-P2Y": (Duration(years=-2), D_DEFAULT, None), - "-P3Y6M4DT12H30M5S": (Duration(-4, -5, 0, 0, -30, -12, 0, -6, -3), D_DEFAULT, None), - "-P1DT2H3M4S": ( - timedelta(days=-1, hours=-2, minutes=-3, seconds=-4), - D_DEFAULT, - None, - ), - # alternative format - "P0018-09-04T11:09:08": (Duration(4, 8, 0, 0, 9, 11, 0, 9, 18), D_ALT_EXT, None), - # 'PT000022.22': timedelta(seconds=22.22), -} - -# d1 d2 '+', '-', '>' -# A list of test cases to test addition and subtraction between datetime and -# Duration objects. -# each tuple contains 2 duration strings, and a result string for addition and -# one for subtraction. The last value says, if the first duration is greater -# than the second. -MATH_TEST_CASES = ( - ( - "P5Y7M1DT9H45M16.72S", - "PT27M24.68S", - "P5Y7M1DT10H12M41.4S", - "P5Y7M1DT9H17M52.04S", - None, - ), - ("PT28M12.73S", "PT56M29.92S", "PT1H24M42.65S", "-PT28M17.19S", False), - ( - "P3Y7M23DT5H25M0.33S", - "PT1H1.95S", - "P3Y7M23DT6H25M2.28S", - "P3Y7M23DT4H24M58.38S", - None, - ), - ( - "PT1H1.95S", - "P3Y7M23DT5H25M0.33S", - "P3Y7M23DT6H25M2.28S", - "-P3Y7M23DT4H24M58.38S", - None, - ), - ("P1332DT55M0.33S", "PT1H1.95S", "P1332DT1H55M2.28S", "P1331DT23H54M58.38S", True), - ( - "PT1H1.95S", - "P1332DT55M0.33S", - "P1332DT1H55M2.28S", - "-P1331DT23H54M58.38S", - False, - ), -) - - -# A list of test cases to test addition and subtraction of date/datetime -# and Duration objects. They are tested against the results of an -# equal long timedelta duration. -DATE_TEST_CASES = ( - ( - date(2008, 2, 29), - timedelta(days=10, hours=12, minutes=20), - Duration(days=10, hours=12, minutes=20), - ), - ( - date(2008, 1, 31), - timedelta(days=10, hours=12, minutes=20), - Duration(days=10, hours=12, minutes=20), - ), - ( - datetime(2008, 2, 29), - timedelta(days=10, hours=12, minutes=20), - Duration(days=10, hours=12, minutes=20), - ), - ( - datetime(2008, 1, 31), - timedelta(days=10, hours=12, minutes=20), - Duration(days=10, hours=12, minutes=20), - ), - ( - datetime(2008, 4, 21), - timedelta(days=10, hours=12, minutes=20), - Duration(days=10, hours=12, minutes=20), - ), - ( - datetime(2008, 5, 5), - timedelta(days=10, hours=12, minutes=20), - Duration(days=10, hours=12, minutes=20), - ), - (datetime(2000, 1, 1), timedelta(hours=-33), Duration(hours=-33)), - ( - datetime(2008, 5, 5), - Duration(years=1, months=1, days=10, hours=12, minutes=20), - Duration(months=13, days=10, hours=12, minutes=20), - ), - ( - datetime(2000, 3, 30), - Duration(years=1, months=1, days=10, hours=12, minutes=20), - Duration(months=13, days=10, hours=12, minutes=20), - ), -) - -# A list of test cases of addition of date/datetime and Duration. The results -# are compared against a given expected result. -DATE_CALC_TEST_CASES = ( - (date(2000, 2, 1), Duration(years=1, months=1), date(2001, 3, 1)), - (date(2000, 2, 29), Duration(years=1, months=1), date(2001, 3, 29)), - (date(2000, 2, 29), Duration(years=1), date(2001, 2, 28)), - (date(1996, 2, 29), Duration(years=4), date(2000, 2, 29)), - (date(2096, 2, 29), Duration(years=4), date(2100, 2, 28)), - (date(2000, 2, 1), Duration(years=-1, months=-1), date(1999, 1, 1)), - (date(2000, 2, 29), Duration(years=-1, months=-1), date(1999, 1, 29)), - (date(2000, 2, 1), Duration(years=1, months=1, days=1), date(2001, 3, 2)), - (date(2000, 2, 29), Duration(years=1, months=1, days=1), date(2001, 3, 30)), - (date(2000, 2, 29), Duration(years=1, days=1), date(2001, 3, 1)), - (date(1996, 2, 29), Duration(years=4, days=1), date(2000, 3, 1)), - (date(2096, 2, 29), Duration(years=4, days=1), date(2100, 3, 1)), - (date(2000, 2, 1), Duration(years=-1, months=-1, days=-1), date(1998, 12, 31)), - (date(2000, 2, 29), Duration(years=-1, months=-1, days=-1), date(1999, 1, 28)), - (date(2001, 4, 1), Duration(years=-1, months=-1, days=-1), date(2000, 2, 29)), - (date(2000, 4, 1), Duration(years=-1, months=-1, days=-1), date(1999, 2, 28)), - ( - Duration(years=1, months=2), - Duration(years=0, months=0, days=1), - Duration(years=1, months=2, days=1), - ), - (Duration(years=-1, months=-1, days=-1), date(2000, 4, 1), date(1999, 2, 28)), - (Duration(years=1, months=1, weeks=5), date(2000, 1, 30), date(2001, 4, 4)), - (parse_duration("P1Y1M5W"), date(2000, 1, 30), date(2001, 4, 4)), - (parse_duration("P0.5Y"), date(2000, 1, 30), None), - ( - Duration(years=1, months=1, hours=3), - datetime(2000, 1, 30, 12, 15, 00), - datetime(2001, 2, 28, 15, 15, 00), - ), - ( - parse_duration("P1Y1MT3H"), - datetime(2000, 1, 30, 12, 15, 00), - datetime(2001, 2, 28, 15, 15, 00), - ), - ( - Duration(years=1, months=2), - timedelta(days=1), - Duration(years=1, months=2, days=1), - ), - ( - timedelta(days=1), - Duration(years=1, months=2), - Duration(years=1, months=2, days=1), - ), - (datetime(2008, 1, 1, 0, 2), Duration(months=1), datetime(2008, 2, 1, 0, 2)), - ( - datetime.strptime("200802", "%Y%M"), - parse_duration("P1M"), - datetime(2008, 2, 1, 0, 2), - ), - (datetime(2008, 2, 1), Duration(months=1), datetime(2008, 3, 1)), - (datetime.strptime("200802", "%Y%m"), parse_duration("P1M"), datetime(2008, 3, 1)), - # (date(2000, 1, 1), - # Duration(years=1.5), - # date(2001, 6, 1)), - # (date(2000, 1, 1), - # Duration(years=1, months=1.5), - # date(2001, 2, 14)), -) - -# A list of test cases of multiplications of durations -# are compared against a given expected result. -DATE_MUL_TEST_CASES = ( - (Duration(years=1, months=1), 3, Duration(years=3, months=3)), - (Duration(years=1, months=1), -3, Duration(years=-3, months=-3)), - (3, Duration(years=1, months=1), Duration(years=3, months=3)), - (-3, Duration(years=1, months=1), Duration(years=-3, months=-3)), - (5, Duration(years=2, minutes=40), Duration(years=10, hours=3, minutes=20)), - (-5, Duration(years=2, minutes=40), Duration(years=-10, hours=-3, minutes=-20)), - (7, Duration(years=1, months=2, weeks=40), Duration(years=8, months=2, weeks=280)), -) - - -class DurationTest(unittest.TestCase): - """ - This class tests various other aspects of the isoduration module, - which are not covered with the test cases listed above. - """ - - def test_associative(self): - """ - Adding 2 durations to a date is not associative. - """ - days1 = Duration(days=1) - months1 = Duration(months=1) - start = date(2000, 3, 30) - res1 = start + days1 + months1 - res2 = start + months1 + days1 - self.assertNotEqual(res1, res2) - - def test_typeerror(self): - """ - Test if TypError is raised with certain parameters. - """ - self.assertRaises(TypeError, parse_duration, date(2000, 1, 1)) - self.assertRaises(TypeError, operator.sub, Duration(years=1), date(2000, 1, 1)) - self.assertRaises(TypeError, operator.sub, "raise exc", Duration(years=1)) - self.assertRaises( - TypeError, - operator.add, - Duration(years=1, months=1, weeks=5), - "raise exception", - ) - self.assertRaises( - TypeError, - operator.add, - "raise exception", - Duration(years=1, months=1, weeks=5), - ) - self.assertRaises( - TypeError, - operator.mul, - Duration(years=1, months=1, weeks=5), - "raise exception", - ) - self.assertRaises( - TypeError, - operator.mul, - "raise exception", - Duration(years=1, months=1, weeks=5), - ) - self.assertRaises( - TypeError, operator.mul, Duration(years=1, months=1, weeks=5), 3.14 - ) - self.assertRaises( - TypeError, operator.mul, 3.14, Duration(years=1, months=1, weeks=5) - ) - - def test_parseerror(self): - """ - Test for unparsable duration string. - """ - self.assertRaises(ISO8601Error, parse_duration, "T10:10:10") - - def test_repr(self): - """ - Test __repr__ and __str__ for Duration objects. - """ - dur = Duration(10, 10, years=10, months=10) - 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): - """ - Test __hash__ for Duration objects. - """ - dur1 = Duration(10, 10, years=10, months=10) - dur2 = Duration(9, 9, years=9, months=9) - dur3 = Duration(10, 10, years=10, months=10) - self.assertNotEqual(hash(dur1), hash(dur2)) - self.assertNotEqual(id(dur1), id(dur2)) - self.assertEqual(hash(dur1), hash(dur3)) - self.assertNotEqual(id(dur1), id(dur3)) - durSet = set() - durSet.add(dur1) - durSet.add(dur2) - durSet.add(dur3) - self.assertEqual(len(durSet), 2) - - def test_neg(self): - """ - Test __neg__ for Duration objects. - """ - self.assertEqual(-Duration(0), Duration(0)) - self.assertEqual(-Duration(years=1, months=1), Duration(years=-1, months=-1)) - self.assertEqual(-Duration(years=1, months=1), Duration(months=-13)) - self.assertNotEqual(-Duration(years=1), timedelta(days=-365)) - self.assertNotEqual(-timedelta(days=365), Duration(years=-1)) - # FIXME: this test fails in python 3... it seems like python3 - # treats a == b the same b == a - # self.assertNotEqual(-timedelta(days=10), -Duration(days=10)) - - def test_format(self): - """ - Test various other strftime combinations. - """ - self.assertEqual(duration_isoformat(Duration(0)), "P0D") - self.assertEqual(duration_isoformat(-Duration(0)), "P0D") - self.assertEqual(duration_isoformat(Duration(seconds=10)), "PT10S") - self.assertEqual(duration_isoformat(Duration(years=-1, months=-1)), "-P1Y1M") - self.assertEqual(duration_isoformat(-Duration(years=1, months=1)), "-P1Y1M") - self.assertEqual(duration_isoformat(-Duration(years=-1, months=-1)), "P1Y1M") - self.assertEqual(duration_isoformat(-Duration(years=-1, months=-1)), "P1Y1M") - dur = Duration( - years=3, months=7, days=23, hours=5, minutes=25, milliseconds=330 - ) - self.assertEqual(duration_isoformat(dur), "P3Y7M23DT5H25M0.33S") - self.assertEqual(duration_isoformat(-dur), "-P3Y7M23DT5H25M0.33S") - - def test_equal(self): - """ - Test __eq__ and __ne__ methods. - """ - self.assertEqual(Duration(years=1, months=1), Duration(years=1, months=1)) - self.assertEqual(Duration(years=1, months=1), Duration(months=13)) - self.assertNotEqual(Duration(years=1, months=2), Duration(years=1, months=1)) - self.assertNotEqual(Duration(years=1, months=1), Duration(months=14)) - self.assertNotEqual(Duration(years=1), timedelta(days=365)) - self.assertFalse(Duration(years=1, months=1) != Duration(years=1, months=1)) - self.assertFalse(Duration(years=1, months=1) != Duration(months=13)) - self.assertTrue(Duration(years=1, months=2) != Duration(years=1, months=1)) - self.assertTrue(Duration(years=1, months=1) != Duration(months=14)) - self.assertTrue(Duration(years=1) != timedelta(days=365)) - self.assertEqual(Duration(days=1), timedelta(days=1)) - # FIXME: this test fails in python 3... it seems like python3 - # treats a != b the same b != a - # self.assertNotEqual(timedelta(days=1), Duration(days=1)) - - def test_totimedelta(self): - """ - Test conversion form Duration to timedelta. - """ - dur = Duration(years=1, months=2, days=10) - self.assertEqual(dur.totimedelta(datetime(1998, 2, 25)), timedelta(434)) - # leap year has one day more in february - self.assertEqual(dur.totimedelta(datetime(2000, 2, 25)), timedelta(435)) - dur = Duration(months=2) - # march is longer than february, but april is shorter than - # march (cause only one day difference compared to 2) - self.assertEqual(dur.totimedelta(datetime(2000, 2, 25)), timedelta(60)) - self.assertEqual(dur.totimedelta(datetime(2001, 2, 25)), timedelta(59)) - self.assertEqual(dur.totimedelta(datetime(2001, 3, 25)), timedelta(61)) - - -def create_parsetestcase(durationstring, expectation, format, altstr): - """ - Create a TestCase class for a specific test. - - This allows having a separate TestCase for each test tuple from the - PARSE_TEST_CASES list, so that a failed test won't stop other tests. - """ - - class TestParseDuration(unittest.TestCase): - """ - A test case template to parse an ISO duration string into a - timedelta or Duration object. - """ - - def test_parse(self): - """ - Parse an ISO duration string and compare it to the expected value. - """ - result = parse_duration(durationstring) - self.assertEqual(result, expectation) - - def test_format(self): - """ - Take duration/timedelta object and create ISO string from it. - This is the reverse test to test_parse. - """ - if altstr: - self.assertEqual(duration_isoformat(expectation, format), altstr) - else: - # if durationstring == '-P2W': - # import pdb; pdb.set_trace() - self.assertEqual( - duration_isoformat(expectation, format), durationstring - ) - - return unittest.TestLoader().loadTestsFromTestCase(TestParseDuration) - - -def create_mathtestcase(dur1, dur2, resadd, ressub, resge): - """ - Create a TestCase class for a specific test. - - This allows having a separate TestCase for each test tuple from the - MATH_TEST_CASES list, so that a failed test won't stop other tests. - """ - - dur1 = parse_duration(dur1) - dur2 = parse_duration(dur2) - resadd = parse_duration(resadd) - ressub = parse_duration(ressub) - - class TestMathDuration(unittest.TestCase): - """ - A test case template test addition, subtraction and > - operators for Duration objects. - """ - - def test_add(self): - """ - Test operator + (__add__, __radd__) - """ - self.assertEqual(dur1 + dur2, resadd) - - def test_sub(self): - """ - Test operator - (__sub__, __rsub__) - """ - self.assertEqual(dur1 - dur2, ressub) - - def test_ge(self): - """ - Test operator > and < - """ - - def dogetest(): - """Test greater than.""" - return dur1 > dur2 - - def doletest(): - """Test less than.""" - return dur1 < dur2 - - if resge is None: - self.assertRaises(TypeError, dogetest) - self.assertRaises(TypeError, doletest) - else: - self.assertEqual(dogetest(), resge) - self.assertEqual(doletest(), not resge) - - return unittest.TestLoader().loadTestsFromTestCase(TestMathDuration) - - -def create_datetestcase(start, tdelta, duration): - """ - Create a TestCase class for a specific test. - - This allows having a separate TestCase for each test tuple from the - DATE_TEST_CASES list, so that a failed test won't stop other tests. - """ - - class TestDateCalc(unittest.TestCase): - """ - A test case template test addition, subtraction - operators for Duration objects. - """ - - def test_add(self): - """ - Test operator +. - """ - self.assertEqual(start + tdelta, start + duration) - - def test_sub(self): - """ - Test operator -. - """ - self.assertEqual(start - tdelta, start - duration) - - return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc) - - -def create_datecalctestcase(start, duration, expectation): - """ - Create a TestCase class for a specific test. - - This allows having a separate TestCase for each test tuple from the - DATE_CALC_TEST_CASES list, so that a failed test won't stop other tests. - """ - - class TestDateCalc(unittest.TestCase): - """ - A test case template test addition operators for Duration objects. - """ - - def test_calc(self): - """ - Test operator +. - """ - if expectation is None: - self.assertRaises(ValueError, operator.add, start, duration) - else: - self.assertEqual(start + duration, expectation) - - return unittest.TestLoader().loadTestsFromTestCase(TestDateCalc) - - -def create_datemultestcase(operand1, operand2, expectation): - """ - Create a TestCase class for a specific test. - - This allows having a separate TestCase for each test tuple from the - DATE_CALC_TEST_CASES list, so that a failed test won't stop other tests. - """ - - class TestDateMul(unittest.TestCase): - """ - A test case template test addition operators for Duration objects. - """ - - def test_mul(self): - """ - Test operator *. - """ - self.assertEqual(operand1 * operand2, expectation) - - return unittest.TestLoader().loadTestsFromTestCase(TestDateMul) - - -def test_suite(): - """ - Return a test suite containing all test defined above. - """ - suite = unittest.TestSuite() - for durationstring, (expectation, format, altstr) in PARSE_TEST_CASES.items(): - suite.addTest(create_parsetestcase(durationstring, expectation, format, altstr)) - for testdata in MATH_TEST_CASES: - suite.addTest(create_mathtestcase(*testdata)) - for testdata in DATE_TEST_CASES: - suite.addTest(create_datetestcase(*testdata)) - for testdata in DATE_CALC_TEST_CASES: - suite.addTest(create_datecalctestcase(*testdata)) - for testdata in DATE_MUL_TEST_CASES: - suite.addTest(create_datemultestcase(*testdata)) - suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DurationTest)) - return suite - - -# load_tests Protocol -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 deleted file mode 100644 index 01b56e6..0000000 --- a/src/isodate/tests/test_pickle.py +++ /dev/null @@ -1,62 +0,0 @@ -import unittest - -import pickle - -import isodate - - -class TestPickle(unittest.TestCase): - """ - A test case template to parse an ISO datetime string into a - datetime object. - """ - - def test_pickle_datetime(self): - """ - Parse an ISO datetime string and compare it to the expected value. - """ - dti = isodate.parse_datetime("2012-10-26T09:33+00:00") - for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): - pikl = pickle.dumps(dti, proto) - self.assertEqual(dti, pickle.loads(pikl), "pickle proto %d failed" % proto) - - def test_pickle_duration(self): - """ - Pickle / unpickle duration objects. - """ - from isodate.duration import Duration - - dur = Duration() - failed = [] - for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): - try: - pikl = pickle.dumps(dur, proto) - if dur != pickle.loads(pikl): - raise Exception("not equal") - 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(): - """ - Construct a TestSuite instance for all test cases. - """ - suite = unittest.TestSuite() - suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestPickle)) - return suite - - -# load_tests Protocol -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 deleted file mode 100644 index 2e2b246..0000000 --- a/src/isodate/tests/test_strf.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -Test cases for the isodate module. -""" -import unittest -import time -from datetime import datetime, timedelta -from isodate import strftime -from isodate import LOCAL -from isodate import DT_EXT_COMPLETE -from isodate import tzinfo - - -TEST_CASES = ( - ( - datetime(2012, 12, 25, 13, 30, 0, 0, LOCAL), - DT_EXT_COMPLETE, - "2012-12-25T13:30:00+10:00", - ), - # DST ON - ( - datetime(1999, 12, 25, 13, 30, 0, 0, LOCAL), - DT_EXT_COMPLETE, - "1999-12-25T13:30:00+11:00", - ), - # microseconds - ( - datetime(2012, 10, 12, 8, 29, 46, 69178), - "%Y-%m-%dT%H:%M:%S.%f", - "2012-10-12T08:29:46.069178", - ), - ( - datetime(2012, 10, 12, 8, 29, 46, 691780), - "%Y-%m-%dT%H:%M:%S.%f", - "2012-10-12T08:29:46.691780", - ), -) - - -def create_testcase(dt, format, expectation): - """ - Create a TestCase class for a specific test. - - This allows having a separate TestCase for each test tuple from the - TEST_CASES list, so that a failed test won't stop other tests. - """ - - class TestDate(unittest.TestCase): - """ - A test case template to test ISO date formatting. - """ - - # local time zone mock function - def localtime_mock(self, secs): - """ - mock time.localtime so that it always returns a time_struct with - tm_idst=1 - """ - tt = self.ORIG["localtime"](secs) - # before 2000 everything is dst, after 2000 no dst. - if tt.tm_year < 2000: - dst = 1 - else: - dst = 0 - tt = ( - tt.tm_year, - tt.tm_mon, - tt.tm_mday, - tt.tm_hour, - tt.tm_min, - tt.tm_sec, - tt.tm_wday, - tt.tm_yday, - dst, - ) - return time.struct_time(tt) - - def setUp(self): - self.ORIG = {} - self.ORIG["STDOFFSET"] = tzinfo.STDOFFSET - self.ORIG["DSTOFFSET"] = tzinfo.DSTOFFSET - self.ORIG["DSTDIFF"] = tzinfo.DSTDIFF - self.ORIG["localtime"] = time.localtime - # override all saved values with fixtures. - # calculate LOCAL TZ offset, so that this test runs in - # every time zone - tzinfo.STDOFFSET = timedelta(seconds=36000) # assume LOC = +10:00 - tzinfo.DSTOFFSET = timedelta(seconds=39600) # assume DST = +11:00 - tzinfo.DSTDIFF = tzinfo.DSTOFFSET - tzinfo.STDOFFSET - time.localtime = self.localtime_mock - - def tearDown(self): - # restore test fixtures - tzinfo.STDOFFSET = self.ORIG["STDOFFSET"] - tzinfo.DSTOFFSET = self.ORIG["DSTOFFSET"] - tzinfo.DSTDIFF = self.ORIG["DSTDIFF"] - time.localtime = self.ORIG["localtime"] - - def test_format(self): - """ - Take date object and create ISO string from it. - This is the reverse test to test_parse. - """ - if expectation is None: - self.assertRaises(AttributeError, strftime(dt, format)) - else: - self.assertEqual(strftime(dt, format), expectation) - - return unittest.TestLoader().loadTestsFromTestCase(TestDate) - - -def test_suite(): - """ - Construct a TestSuite instance for all test cases. - """ - suite = unittest.TestSuite() - for dt, format, expectation in TEST_CASES: - suite.addTest(create_testcase(dt, format, expectation)) - return suite - - -# load_tests Protocol -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 102b6e3..726c54a 100644 --- a/src/isodate/tzinfo.py +++ b/src/isodate/tzinfo.py @@ -3,8 +3,9 @@ All those classes are taken from the Python documentation. """ -from datetime import timedelta, tzinfo + import time +from datetime import timedelta, tzinfo ZERO = timedelta(0) # constant for zero time offset. diff --git a/tests/test_date.py b/tests/test_date.py new file mode 100644 index 0000000..36b6fde --- /dev/null +++ b/tests/test_date.py @@ -0,0 +1,83 @@ +""" +Test cases for the isodate module. +""" + +from datetime import date + +import pytest + +from isodate import ( + DATE_BAS_COMPLETE, + DATE_BAS_MONTH, + DATE_BAS_ORD_COMPLETE, + DATE_BAS_WEEK, + DATE_BAS_WEEK_COMPLETE, + DATE_CENTURY, + DATE_EXT_COMPLETE, + DATE_EXT_MONTH, + DATE_EXT_ORD_COMPLETE, + DATE_EXT_WEEK, + DATE_EXT_WEEK_COMPLETE, + DATE_YEAR, + ISO8601Error, + date_isoformat, + parse_date, +) + +# the following list contains tuples of ISO date strings and the expected +# result from the parse_date method. A result of None means an ISO8601Error +# is expected. The test cases are grouped into dates with 4 digit years +# and 6 digit years. +TEST_CASES = { + # yeardigits = 4 + (4, "19", date(1901, 1, 1), DATE_CENTURY), + (4, "1985", date(1985, 1, 1), DATE_YEAR), + (4, "1985-04", date(1985, 4, 1), DATE_EXT_MONTH), + (4, "198504", date(1985, 4, 1), DATE_BAS_MONTH), + (4, "1985-04-12", date(1985, 4, 12), DATE_EXT_COMPLETE), + (4, "19850412", date(1985, 4, 12), DATE_BAS_COMPLETE), + (4, "1985102", date(1985, 4, 12), DATE_BAS_ORD_COMPLETE), + (4, "1985-102", date(1985, 4, 12), DATE_EXT_ORD_COMPLETE), + (4, "1985W155", date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE), + (4, "1985-W15-5", date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE), + (4, "1985W15", date(1985, 4, 8), DATE_BAS_WEEK), + (4, "1985-W15", date(1985, 4, 8), DATE_EXT_WEEK), + (4, "1989-W15", date(1989, 4, 10), DATE_EXT_WEEK), + (4, "1989-W15-5", date(1989, 4, 14), DATE_EXT_WEEK_COMPLETE), + (4, "1-W1-1", None, DATE_BAS_WEEK_COMPLETE), + # yeardigits = 6 + (6, "+0019", date(1901, 1, 1), DATE_CENTURY), + (6, "+001985", date(1985, 1, 1), DATE_YEAR), + (6, "+001985-04", date(1985, 4, 1), DATE_EXT_MONTH), + (6, "+001985-04-12", date(1985, 4, 12), DATE_EXT_COMPLETE), + (6, "+0019850412", date(1985, 4, 12), DATE_BAS_COMPLETE), + (6, "+001985102", date(1985, 4, 12), DATE_BAS_ORD_COMPLETE), + (6, "+001985-102", date(1985, 4, 12), DATE_EXT_ORD_COMPLETE), + (6, "+001985W155", date(1985, 4, 12), DATE_BAS_WEEK_COMPLETE), + (6, "+001985-W15-5", date(1985, 4, 12), DATE_EXT_WEEK_COMPLETE), + (6, "+001985W15", date(1985, 4, 8), DATE_BAS_WEEK), + (6, "+001985-W15", date(1985, 4, 8), DATE_EXT_WEEK), +} + + +@pytest.mark.parametrize("yeardigits,datestring,expected,_", TEST_CASES) +def test_parse(yeardigits, datestring, expected, _): + if expected is None: + with pytest.raises(ISO8601Error): + parse_date(datestring, yeardigits) + else: + result = parse_date(datestring, yeardigits) + assert result == expected + + +@pytest.mark.parametrize("yeardigits, datestring, expected, format", TEST_CASES) +def test_format(yeardigits, datestring, expected, format): + """ + Take date object and create ISO string from it. + This is the reverse test to test_parse. + """ + if expected is None: + with pytest.raises(AttributeError): + date_isoformat(expected, format, yeardigits) + else: + assert date_isoformat(expected, format, yeardigits) == datestring diff --git a/src/isodate/tests/test_datetime.py b/tests/test_datetime.py similarity index 61% rename from src/isodate/tests/test_datetime.py rename to tests/test_datetime.py index 19752d3..d86d84d 100644 --- a/src/isodate/tests/test_datetime.py +++ b/tests/test_datetime.py @@ -1,16 +1,31 @@ """ Test cases for the isodatetime module. """ -import unittest + from datetime import datetime -from isodate import parse_datetime, UTC, FixedOffset, datetime_isoformat -from isodate import ISO8601Error -from isodate import DATE_BAS_COMPLETE, TIME_BAS_MINUTE, TIME_BAS_COMPLETE -from isodate import DATE_EXT_COMPLETE, TIME_EXT_MINUTE, TIME_EXT_COMPLETE -from isodate import TZ_BAS, TZ_EXT, TZ_HOUR -from isodate import DATE_BAS_ORD_COMPLETE, DATE_EXT_ORD_COMPLETE -from isodate import DATE_BAS_WEEK_COMPLETE, DATE_EXT_WEEK_COMPLETE +import pytest + +from isodate import ( + DATE_BAS_COMPLETE, + DATE_BAS_ORD_COMPLETE, + DATE_BAS_WEEK_COMPLETE, + DATE_EXT_COMPLETE, + DATE_EXT_ORD_COMPLETE, + DATE_EXT_WEEK_COMPLETE, + TIME_BAS_COMPLETE, + TIME_BAS_MINUTE, + TIME_EXT_COMPLETE, + TIME_EXT_MINUTE, + TZ_BAS, + TZ_EXT, + TZ_HOUR, + UTC, + FixedOffset, + ISO8601Error, + datetime_isoformat, + parse_datetime, +) # the following list contains tuples of ISO datetime strings and the expected # result from the parse_datetime method. A result of None means an ISO8601Error @@ -124,58 +139,26 @@ ] -def create_testcase(datetimestring, expectation, format, output): +@pytest.mark.parametrize("datetimestring, expected, format, output", TEST_CASES) +def test_parse(datetimestring, expected, format, output): """ - Create a TestCase class for a specific test. - - This allows having a separate TestCase for each test tuple from the - TEST_CASES list, so that a failed test won't stop other tests. + Parse an ISO datetime string and compare it to the expected value. """ + if expected is None: + with pytest.raises(ISO8601Error): + parse_datetime(datetimestring) + else: + assert parse_datetime(datetimestring) == expected - class TestDateTime(unittest.TestCase): - """ - A test case template to parse an ISO datetime string into a - datetime object. - """ - - def test_parse(self): - """ - Parse an ISO datetime string and compare it to the expected value. - """ - if expectation is None: - self.assertRaises(ISO8601Error, parse_datetime, datetimestring) - else: - self.assertEqual(parse_datetime(datetimestring), expectation) - - def test_format(self): - """ - Take datetime object and create ISO string from it. - This is the reverse test to test_parse. - """ - if expectation is None: - self.assertRaises( - AttributeError, datetime_isoformat, expectation, format - ) - else: - self.assertEqual(datetime_isoformat(expectation, format), output) - - return unittest.TestLoader().loadTestsFromTestCase(TestDateTime) - -def test_suite(): +@pytest.mark.parametrize("datetimestring, expected, format, output", TEST_CASES) +def test_format(datetimestring, expected, format, output): """ - Construct a TestSuite instance for all test cases. + Take datetime object and create ISO string from it. + This is the reverse test to test_parse. """ - suite = unittest.TestSuite() - for datetimestring, expectation, format, output in TEST_CASES: - suite.addTest(create_testcase(datetimestring, expectation, format, output)) - return suite - - -# load_tests Protocol -def load_tests(loader, tests, pattern): - return test_suite() - - -if __name__ == "__main__": - unittest.main(defaultTest="test_suite") + if expected is None: + with pytest.raises(AttributeError): + datetime_isoformat(expected, format) + else: + assert datetime_isoformat(expected, format) == output diff --git a/tests/test_duration.py b/tests/test_duration.py new file mode 100644 index 0000000..9bf26b4 --- /dev/null +++ b/tests/test_duration.py @@ -0,0 +1,436 @@ +"""Test cases for the isoduration module.""" + +from datetime import date, datetime, timedelta + +import pytest + +from isodate import ( + D_ALT_EXT, + D_DEFAULT, + D_WEEK, + Duration, + ISO8601Error, + duration_isoformat, + parse_duration, +) + +# the following list contains tuples of ISO duration strings and the expected +# result from the parse_duration method. A result of None means an ISO8601Error +# is expected. +PARSE_TEST_CASES = ( + ("P18Y9M4DT11H9M8S", Duration(4, 8, 0, 0, 9, 11, 0, 9, 18), D_DEFAULT, None), + ("P2W", timedelta(weeks=2), D_WEEK, None), + ("P3Y6M4DT12H30M5S", Duration(4, 5, 0, 0, 30, 12, 0, 6, 3), D_DEFAULT, None), + ("P23DT23H", timedelta(hours=23, days=23), D_DEFAULT, None), + ("P4Y", Duration(years=4), D_DEFAULT, None), + ("P1M", Duration(months=1), D_DEFAULT, None), + ("PT1M", timedelta(minutes=1), D_DEFAULT, None), + ("P0.5Y", Duration(years=0.5), D_DEFAULT, None), + ("PT36H", timedelta(hours=36), D_DEFAULT, "P1DT12H"), + ("P1DT12H", timedelta(days=1, hours=12), D_DEFAULT, None), + ("+P11D", timedelta(days=11), D_DEFAULT, "P11D"), + ("-P2W", timedelta(weeks=-2), D_WEEK, None), + ("-P2.2W", timedelta(weeks=-2.2), D_DEFAULT, "-P15DT9H36M"), + ("P1DT2H3M4S", timedelta(days=1, hours=2, minutes=3, seconds=4), D_DEFAULT, None), + ("P1DT2H3M", timedelta(days=1, hours=2, minutes=3), D_DEFAULT, None), + ("P1DT2H", timedelta(days=1, hours=2), D_DEFAULT, None), + ("PT2H", timedelta(hours=2), D_DEFAULT, None), + ("PT2.3H", timedelta(hours=2.3), D_DEFAULT, "PT2H18M"), + ("PT2H3M4S", timedelta(hours=2, minutes=3, seconds=4), D_DEFAULT, None), + ("PT3M4S", timedelta(minutes=3, seconds=4), D_DEFAULT, None), + ("PT22S", timedelta(seconds=22), D_DEFAULT, None), + ("PT22.22S", timedelta(seconds=22.22), "PT%S.%fS", "PT22.220000S"), + ("-P2Y", Duration(years=-2), D_DEFAULT, None), + ("-P3Y6M4DT12H30M5S", Duration(-4, -5, 0, 0, -30, -12, 0, -6, -3), D_DEFAULT, None), + ( + "-P1DT2H3M4S", + timedelta(days=-1, hours=-2, minutes=-3, seconds=-4), + D_DEFAULT, + None, + ), + # alternative format + ("P0018-09-04T11:09:08", Duration(4, 8, 0, 0, 9, 11, 0, 9, 18), D_ALT_EXT, None), + # 'PT000022.22', timedelta(seconds=22.22), +) + +# d1 d2 '+', '-', '>' +# A list of test cases to test addition and subtraction between datetime and +# Duration objects. +# each tuple contains 2 duration strings, and a result string for addition and +# one for subtraction. The last value says, if the first duration is greater +# than the second. +MATH_TEST_CASES = ( + ( + "P5Y7M1DT9H45M16.72S", + "PT27M24.68S", + "P5Y7M1DT10H12M41.4S", + "P5Y7M1DT9H17M52.04S", + None, + ), + ("PT28M12.73S", "PT56M29.92S", "PT1H24M42.65S", "-PT28M17.19S", False), + ( + "P3Y7M23DT5H25M0.33S", + "PT1H1.95S", + "P3Y7M23DT6H25M2.28S", + "P3Y7M23DT4H24M58.38S", + None, + ), + ( + "PT1H1.95S", + "P3Y7M23DT5H25M0.33S", + "P3Y7M23DT6H25M2.28S", + "-P3Y7M23DT4H24M58.38S", + None, + ), + ("P1332DT55M0.33S", "PT1H1.95S", "P1332DT1H55M2.28S", "P1331DT23H54M58.38S", True), + ( + "PT1H1.95S", + "P1332DT55M0.33S", + "P1332DT1H55M2.28S", + "-P1331DT23H54M58.38S", + False, + ), +) + + +# A list of test cases to test addition and subtraction of date/datetime +# and Duration objects. They are tested against the results of an +# equal long timedelta duration. +DATE_TEST_CASES = ( + ( + date(2008, 2, 29), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20), + ), + ( + date(2008, 1, 31), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20), + ), + ( + datetime(2008, 2, 29), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20), + ), + ( + datetime(2008, 1, 31), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20), + ), + ( + datetime(2008, 4, 21), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20), + ), + ( + datetime(2008, 5, 5), + timedelta(days=10, hours=12, minutes=20), + Duration(days=10, hours=12, minutes=20), + ), + (datetime(2000, 1, 1), timedelta(hours=-33), Duration(hours=-33)), + ( + datetime(2008, 5, 5), + Duration(years=1, months=1, days=10, hours=12, minutes=20), + Duration(months=13, days=10, hours=12, minutes=20), + ), + ( + datetime(2000, 3, 30), + Duration(years=1, months=1, days=10, hours=12, minutes=20), + Duration(months=13, days=10, hours=12, minutes=20), + ), +) + +# A list of test cases of addition of date/datetime and Duration. The results +# are compared against a given expected result. +DATE_CALC_TEST_CASES = ( + (date(2000, 2, 1), Duration(years=1, months=1), date(2001, 3, 1)), + (date(2000, 2, 29), Duration(years=1, months=1), date(2001, 3, 29)), + (date(2000, 2, 29), Duration(years=1), date(2001, 2, 28)), + (date(1996, 2, 29), Duration(years=4), date(2000, 2, 29)), + (date(2096, 2, 29), Duration(years=4), date(2100, 2, 28)), + (date(2000, 2, 1), Duration(years=-1, months=-1), date(1999, 1, 1)), + (date(2000, 2, 29), Duration(years=-1, months=-1), date(1999, 1, 29)), + (date(2000, 2, 1), Duration(years=1, months=1, days=1), date(2001, 3, 2)), + (date(2000, 2, 29), Duration(years=1, months=1, days=1), date(2001, 3, 30)), + (date(2000, 2, 29), Duration(years=1, days=1), date(2001, 3, 1)), + (date(1996, 2, 29), Duration(years=4, days=1), date(2000, 3, 1)), + (date(2096, 2, 29), Duration(years=4, days=1), date(2100, 3, 1)), + (date(2000, 2, 1), Duration(years=-1, months=-1, days=-1), date(1998, 12, 31)), + (date(2000, 2, 29), Duration(years=-1, months=-1, days=-1), date(1999, 1, 28)), + (date(2001, 4, 1), Duration(years=-1, months=-1, days=-1), date(2000, 2, 29)), + (date(2000, 4, 1), Duration(years=-1, months=-1, days=-1), date(1999, 2, 28)), + ( + Duration(years=1, months=2), + Duration(years=0, months=0, days=1), + Duration(years=1, months=2, days=1), + ), + (Duration(years=-1, months=-1, days=-1), date(2000, 4, 1), date(1999, 2, 28)), + (Duration(years=1, months=1, weeks=5), date(2000, 1, 30), date(2001, 4, 4)), + (parse_duration("P1Y1M5W"), date(2000, 1, 30), date(2001, 4, 4)), + (parse_duration("P0.5Y"), date(2000, 1, 30), None), + ( + Duration(years=1, months=1, hours=3), + datetime(2000, 1, 30, 12, 15, 00), + datetime(2001, 2, 28, 15, 15, 00), + ), + ( + parse_duration("P1Y1MT3H"), + datetime(2000, 1, 30, 12, 15, 00), + datetime(2001, 2, 28, 15, 15, 00), + ), + ( + Duration(years=1, months=2), + timedelta(days=1), + Duration(years=1, months=2, days=1), + ), + ( + timedelta(days=1), + Duration(years=1, months=2), + Duration(years=1, months=2, days=1), + ), + (datetime(2008, 1, 1, 0, 2), Duration(months=1), datetime(2008, 2, 1, 0, 2)), + ( + datetime.strptime("200802", "%Y%M"), + parse_duration("P1M"), + datetime(2008, 2, 1, 0, 2), + ), + (datetime(2008, 2, 1), Duration(months=1), datetime(2008, 3, 1)), + (datetime.strptime("200802", "%Y%m"), parse_duration("P1M"), datetime(2008, 3, 1)), + # (date(2000, 1, 1), + # Duration(years=1.5), + # date(2001, 6, 1)), + # (date(2000, 1, 1), + # Duration(years=1, months=1.5), + # date(2001, 2, 14)), +) + +# A list of test cases of multiplications of durations +# are compared against a given expected result. +DATE_MUL_TEST_CASES = ( + (Duration(years=1, months=1), 3, Duration(years=3, months=3)), + (Duration(years=1, months=1), -3, Duration(years=-3, months=-3)), + (3, Duration(years=1, months=1), Duration(years=3, months=3)), + (-3, Duration(years=1, months=1), Duration(years=-3, months=-3)), + (5, Duration(years=2, minutes=40), Duration(years=10, hours=3, minutes=20)), + (-5, Duration(years=2, minutes=40), Duration(years=-10, hours=-3, minutes=-20)), + (7, Duration(years=1, months=2, weeks=40), Duration(years=8, months=2, weeks=280)), +) + + +def test_associative(): + """Adding 2 durations to a date is not associative.""" + days1 = Duration(days=1) + months1 = Duration(months=1) + start = date(2000, 3, 30) + res1 = start + days1 + months1 + res2 = start + months1 + days1 + assert res1 != res2 + + +def test_typeerror(): + """Test if TypError is raised with certain parameters.""" + with pytest.raises(TypeError): + parse_duration(date(2000, 1, 1)) + with pytest.raises(TypeError): + Duration(years=1) - date(2000, 1, 1) + with pytest.raises(TypeError): + "raise exc" - Duration(years=1) + with pytest.raises(TypeError): + Duration(years=1, months=1, weeks=5) + "raise exception" + with pytest.raises(TypeError): + "raise exception" + Duration(years=1, months=1, weeks=5) + with pytest.raises(TypeError): + Duration(years=1, months=1, weeks=5) * "raise exception" + with pytest.raises(TypeError): + "raise exception" * Duration(years=1, months=1, weeks=5) + with pytest.raises(TypeError): + Duration(years=1, months=1, weeks=5) * 3.14 + with pytest.raises(TypeError): + 3.14 * Duration(years=1, months=1, weeks=5) + + +def test_parseerror(): + """Test for unparseable duration string.""" + with pytest.raises(ISO8601Error): + parse_duration("T10:10:10") + + +def test_repr(): + """Test __repr__ and __str__ for Duration objects.""" + dur = Duration(10, 10, years=10, months=10) + assert "10 years, 10 months, 10 days, 0:00:10" == str(dur) + assert "isodate.duration.Duration(10, 10, 0," " years=10, months=10)" == repr(dur) + dur = Duration(months=0) + assert "0:00:00" == str(dur) + dur = Duration(months=1) + assert "1 month, 0:00:00" == str(dur) + + +def test_hash(): + """Test __hash__ for Duration objects.""" + dur1 = Duration(10, 10, years=10, months=10) + dur2 = Duration(9, 9, years=9, months=9) + dur3 = Duration(10, 10, years=10, months=10) + assert hash(dur1) != hash(dur2) + assert id(dur1) != id(dur2) + assert hash(dur1) == hash(dur3) + assert id(dur1) != id(dur3) + durSet = set() + durSet.add(dur1) + durSet.add(dur2) + durSet.add(dur3) + assert len(durSet) == 2 + + +def test_neg(): + """Test __neg__ for Duration objects.""" + assert -Duration(0) == Duration(0) + assert -Duration(years=1, months=1) == Duration(years=-1, months=-1) + assert -Duration(years=1, months=1) == Duration(months=-13) + assert -Duration(years=1) != timedelta(days=-365) + assert -timedelta(days=365) != Duration(years=-1) + # FIXME: this test fails in python 3... it seems like python3 + # treats a == b the same b == a + # assert -timedelta(days=10) != -Duration(days=10) + + +def test_format(): + """Test various other strftime combinations.""" + assert duration_isoformat(Duration(0)) == "P0D" + assert duration_isoformat(-Duration(0)) == "P0D" + assert duration_isoformat(Duration(seconds=10)) == "PT10S" + assert duration_isoformat(Duration(years=-1, months=-1)) == "-P1Y1M" + assert duration_isoformat(-Duration(years=1, months=1)) == "-P1Y1M" + assert duration_isoformat(-Duration(years=-1, months=-1)) == "P1Y1M" + assert duration_isoformat(-Duration(years=-1, months=-1)) == "P1Y1M" + dur = Duration(years=3, months=7, days=23, hours=5, minutes=25, milliseconds=330) + assert duration_isoformat(dur) == "P3Y7M23DT5H25M0.33S" + assert duration_isoformat(-dur) == "-P3Y7M23DT5H25M0.33S" + + +def test_equal(): + """Test __eq__ and __ne__ methods.""" + assert Duration(years=1, months=1) == Duration(years=1, months=1) + assert Duration(years=1, months=1) == Duration(months=13) + assert Duration(years=1, months=2) != Duration(years=1, months=1) + assert Duration(years=1, months=1) != Duration(months=14) + assert Duration(years=1) != timedelta(days=365) + assert (Duration(years=1, months=1) != Duration(years=1, months=1)) is False + assert (Duration(years=1, months=1) != Duration(months=13)) is False + assert (Duration(years=1, months=2) != Duration(years=1, months=1)) is True + assert (Duration(years=1, months=1) != Duration(months=14)) is True + assert (Duration(years=1) != timedelta(days=365)) is True + assert Duration(days=1) == timedelta(days=1) + # FIXME: this test fails in python 3... it seems like python3 + # treats a != b the same b != a + # assert timedelta(days=1) != Duration(days=1) + + +def test_totimedelta(): + """Test conversion form Duration to timedelta.""" + dur = Duration(years=1, months=2, days=10) + assert dur.totimedelta(datetime(1998, 2, 25)) == timedelta(434) + # leap year has one day more in february + assert dur.totimedelta(datetime(2000, 2, 25)) == timedelta(435) + dur = Duration(months=2) + # march is longer than february, but april is shorter than + # march (cause only one day difference compared to 2) + assert dur.totimedelta(datetime(2000, 2, 25)) == timedelta(60) + assert dur.totimedelta(datetime(2001, 2, 25)) == timedelta(59) + assert dur.totimedelta(datetime(2001, 3, 25)) == timedelta(61) + + +@pytest.mark.parametrize( + "durationstring, expectation, format, altstr", + PARSE_TEST_CASES, +) +def test_parse(durationstring, expectation, format, altstr): + """Parse an ISO duration string and compare it to the expected value.""" + result = parse_duration(durationstring) + assert result == expectation + + +@pytest.mark.parametrize( + "durationstring, expectation, format, altstr", + PARSE_TEST_CASES, +) +def test_format_parse(durationstring, expectation, format, altstr): + """Take duration/timedelta object and create ISO string from it. + + This is the reverse test to test_parse. + """ + if altstr: + assert duration_isoformat(expectation, format) == altstr + else: + # if durationstring == '-P2W': + # import pdb; pdb.set_trace() + assert duration_isoformat(expectation, format) == durationstring + + +@pytest.mark.parametrize("dur1, dur2, resadd, ressub, resge", MATH_TEST_CASES) +def test_add(dur1, dur2, resadd, ressub, resge): + dur1 = parse_duration(dur1) + dur2 = parse_duration(dur2) + resadd = parse_duration(resadd) + assert dur1 + dur2 == resadd + + +@pytest.mark.parametrize("dur1, dur2, resadd, ressub, resge", MATH_TEST_CASES) +def test_sub(dur1, dur2, resadd, ressub, resge): + """ + Test operator - (__sub__, __rsub__) + """ + dur1 = parse_duration(dur1) + dur2 = parse_duration(dur2) + ressub = parse_duration(ressub) + assert dur1 - dur2 == ressub + + +@pytest.mark.parametrize("dur1, dur2, resadd, ressub, resge", MATH_TEST_CASES) +def test_ge(dur1, dur2, resadd, ressub, resge): + """Test operator > and <.""" + dur1 = parse_duration(dur1) + dur2 = parse_duration(dur2) + + def dogetest(): + """Test greater than.""" + return dur1 > dur2 + + def doletest(): + """Test less than.""" + return dur1 < dur2 + + if resge is None: + with pytest.raises(TypeError): + dogetest() + with pytest.raises(TypeError): + doletest() + else: + assert dogetest() is resge + assert doletest() is not resge + + +@pytest.mark.parametrize("start, tdelta, duration", DATE_TEST_CASES) +def test_add_date(start, tdelta, duration): + assert start + tdelta == start + duration + + +@pytest.mark.parametrize("start, tdelta, duration", DATE_TEST_CASES) +def test_sub_date(start, tdelta, duration): + assert start - tdelta == start - duration + + +@pytest.mark.parametrize("start, duration, expectation", DATE_CALC_TEST_CASES) +def test_calc_date(start, duration, expectation): + """Test operator +.""" + if expectation is None: + with pytest.raises(ValueError): + start + duration + else: + assert start + duration == expectation + + +@pytest.mark.parametrize("operand1, operand2, expectation", DATE_MUL_TEST_CASES) +def test_mul_date(operand1, operand2, expectation): + """Test operator *.""" + assert operand1 * operand2 == expectation diff --git a/tests/test_pickle.py b/tests/test_pickle.py new file mode 100644 index 0000000..228a949 --- /dev/null +++ b/tests/test_pickle.py @@ -0,0 +1,32 @@ +import pickle + +import isodate + + +def test_pickle_datetime(): + """Parse an ISO datetime string and compare it to the expected value.""" + dti = isodate.parse_datetime("2012-10-26T09:33+00:00") + for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): + pikl = pickle.dumps(dti, proto) + assert dti == pickle.loads(pikl), "pickle proto %d failed" % proto + + +def test_pickle_duration(): + """Pickle / unpickle duration objects.""" + from isodate.duration import Duration + + dur = Duration() + failed = [] + for proto in range(0, pickle.HIGHEST_PROTOCOL + 1): + try: + pikl = pickle.dumps(dur, proto) + if dur != pickle.loads(pikl): + raise Exception("not equal") + except Exception as e: + failed.append("pickle proto %d failed (%s)" % (proto, repr(e))) + assert len(failed) == 0, "pickle protos failed: %s" % str(failed) + + +def test_pickle_utc(): + """isodate.UTC objects remain the same after pickling.""" + assert isodate.UTC is pickle.loads(pickle.dumps(isodate.UTC)) diff --git a/tests/test_strf.py b/tests/test_strf.py new file mode 100644 index 0000000..7249c89 --- /dev/null +++ b/tests/test_strf.py @@ -0,0 +1,83 @@ +"""Test cases for the isodate module.""" + +import time +from datetime import datetime, timedelta + +import pytest + +from isodate import DT_EXT_COMPLETE, LOCAL, strftime, tzinfo + +TEST_CASES = ( + ( + datetime(2012, 12, 25, 13, 30, 0, 0, LOCAL), + DT_EXT_COMPLETE, + "2012-12-25T13:30:00+10:00", + ), + # DST ON + ( + datetime(1999, 12, 25, 13, 30, 0, 0, LOCAL), + DT_EXT_COMPLETE, + "1999-12-25T13:30:00+11:00", + ), + # microseconds + ( + datetime(2012, 10, 12, 8, 29, 46, 69178), + "%Y-%m-%dT%H:%M:%S.%f", + "2012-10-12T08:29:46.069178", + ), + ( + datetime(2012, 10, 12, 8, 29, 46, 691780), + "%Y-%m-%dT%H:%M:%S.%f", + "2012-10-12T08:29:46.691780", + ), +) + + +@pytest.fixture +def tz_patch(monkeypatch): + # local time zone mock function + localtime_orig = time.localtime + + def localtime_mock(secs): + """Mock time to fixed date. + + Mock time.localtime so that it always returns a time_struct with tm_dst=1 + """ + tt = localtime_orig(secs) + # before 2000 everything is dst, after 2000 no dst. + if tt.tm_year < 2000: + dst = 1 + else: + dst = 0 + tt = ( + tt.tm_year, + tt.tm_mon, + tt.tm_mday, + tt.tm_hour, + tt.tm_min, + tt.tm_sec, + tt.tm_wday, + tt.tm_yday, + dst, + ) + return time.struct_time(tt) + + monkeypatch.setattr(time, "localtime", localtime_mock) + # assume LOC = +10:00 + monkeypatch.setattr(tzinfo, "STDOFFSET", timedelta(seconds=36000)) + # assume DST = +11:00 + monkeypatch.setattr(tzinfo, "DSTOFFSET", timedelta(seconds=39600)) + monkeypatch.setattr(tzinfo, "DSTDIFF", tzinfo.DSTOFFSET - tzinfo.STDOFFSET) + + +@pytest.mark.parametrize("dt, format, expectation", TEST_CASES) +def test_format(tz_patch, dt, format, expectation): + """Take date object and create ISO string from it. + + This is the reverse test to test_parse. + """ + if expectation is None: + with pytest.raises(AttributeError): + strftime(dt, format) + else: + assert strftime(dt, format) == expectation diff --git a/src/isodate/tests/test_time.py b/tests/test_time.py similarity index 59% rename from src/isodate/tests/test_time.py rename to tests/test_time.py index c843bc9..b605b93 100644 --- a/src/isodate/tests/test_time.py +++ b/tests/test_time.py @@ -1,14 +1,26 @@ """ Test cases for the isotime module. """ -import unittest + from datetime import time -from isodate import parse_time, UTC, FixedOffset, ISO8601Error, time_isoformat -from isodate import TIME_BAS_COMPLETE, TIME_BAS_MINUTE -from isodate import TIME_EXT_COMPLETE, TIME_EXT_MINUTE -from isodate import TIME_HOUR -from isodate import TZ_BAS, TZ_EXT, TZ_HOUR +import pytest + +from isodate import ( + TIME_BAS_COMPLETE, + TIME_BAS_MINUTE, + TIME_EXT_COMPLETE, + TIME_EXT_MINUTE, + TIME_HOUR, + TZ_BAS, + TZ_EXT, + TZ_HOUR, + UTC, + FixedOffset, + ISO8601Error, + parse_time, + time_isoformat, +) # the following list contains tuples of ISO time strings and the expected # result from the parse_time method. A result of None means an ISO8601Error @@ -92,57 +104,26 @@ ] -def create_testcase(timestring, expectation, format): +@pytest.mark.parametrize("timestring, expectation, format", TEST_CASES) +def test_parse(timestring, expectation, format): """ - Create a TestCase class for a specific test. - - This allows having a separate TestCase for each test tuple from the - TEST_CASES list, so that a failed test won't stop other tests. + Parse an ISO time string and compare it to the expected value. """ + if expectation is None: + with pytest.raises(ISO8601Error): + parse_time(timestring) + else: + assert parse_time(timestring) == expectation - class TestTime(unittest.TestCase): - """ - A test case template to parse an ISO time string into a time - object. - """ - - def test_parse(self): - """ - Parse an ISO time string and compare it to the expected value. - """ - if expectation is None: - self.assertRaises(ISO8601Error, parse_time, timestring) - else: - result = parse_time(timestring) - self.assertEqual(result, expectation) - - def test_format(self): - """ - Take time object and create ISO string from it. - This is the reverse test to test_parse. - """ - if expectation is None: - self.assertRaises(AttributeError, time_isoformat, expectation, format) - elif format is not None: - self.assertEqual(time_isoformat(expectation, format), timestring) - - return unittest.TestLoader().loadTestsFromTestCase(TestTime) - -def test_suite(): +@pytest.mark.parametrize("timestring, expectation, format", TEST_CASES) +def test_format(timestring, expectation, format): """ - Construct a TestSuite instance for all test cases. + Take time object and create ISO string from it. + This is the reverse test to test_parse. """ - suite = unittest.TestSuite() - for timestring, expectation, format in TEST_CASES: - suite.addTest(create_testcase(timestring, expectation, format)) - return suite - - -# load_tests Protocol -def load_tests(loader, tests, pattern): - return test_suite() - - -if __name__ == "__main__": - unittest.main(defaultTest="test_suite") + if expectation is None: + with pytest.raises(AttributeError): + time_isoformat(expectation, format) + elif format is not None: + assert time_isoformat(expectation, format) == timestring diff --git a/tox.ini b/tox.ini index 322d454..cc32bf7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,18 +1,19 @@ [tox] +isolated_build = True envlist = lint - py{37, 38, 39, 310, py3} + py{39, 310, 311, 312, py39} [testenv] deps = + pytest + pytest-cov setenv = PYTHONWARNINGS = default commands = - {envpython} setup.py test - pip install --quiet coverage - {envpython} setup.py clean --all - coverage run setup.py test - coverage xml + pytest + # {envpython} setup.py clean --all + # pytest --cov=isodate --cov-report=xml [testenv:lint] deps = pre-commit