python-dateutil
Advanced tools
| #!/usr/bin/env python3 | ||
| import hashlib | ||
| ZONEFILE_METADATA_TEMPLATE = """{{ | ||
| "metadata_version": 2.0, | ||
| "releases_url": [], | ||
| "tzdata_file": "{tzdata_file}", | ||
| "tzdata_file_sha512": "{tzdata_sha512}", | ||
| "tzversion": "{tzdata_version}", | ||
| "zonegroups": [ | ||
| "africa", | ||
| "antarctica", | ||
| "asia", | ||
| "australasia", | ||
| "europe", | ||
| "northamerica", | ||
| "southamerica", | ||
| "pacificnew", | ||
| "etcetera", | ||
| "systemv", | ||
| "factory", | ||
| "backzone", | ||
| "backward" | ||
| ] | ||
| }} | ||
| """ | ||
| def calculate_sha512(fpath): | ||
| with open(fpath, 'rb') as f: | ||
| sha_hasher = hashlib.sha512() | ||
| sha_hasher.update(f.read()) | ||
| return sha_hasher.hexdigest() | ||
| if __name__ == "__main__": | ||
| import argparse | ||
| parser = argparse.ArgumentParser() | ||
| parser.add_argument('tzdata', metavar='TZDATA', | ||
| help='The name tzdata tarball file') | ||
| parser.add_argument('version', metavar='VERSION', | ||
| help='The version of the tzdata tarball') | ||
| parser.add_argument('out', metavar='OUT', nargs='?', | ||
| default='zonefile_metadata.json', | ||
| help='Where to write the file') | ||
| args = parser.parse_args() | ||
| tzdata = args.tzdata | ||
| version = args.version | ||
| sha512 = calculate_sha512(tzdata) | ||
| metadata_file_text = ZONEFILE_METADATA_TEMPLATE.format( | ||
| tzdata_file=tzdata, | ||
| tzdata_version=version, | ||
| tzdata_sha512=sha512, | ||
| ) | ||
| with open(args.out, 'w') as f: | ||
| f.write(metadata_file_text) | ||
| #!/usr/bin/env bash | ||
| ### | ||
| # Runs the 'tz' tox test environment, which builds the repo against the master | ||
| # branch of the upstream tz database project. | ||
| set -e | ||
| TMP_DIR=${1} | ||
| REPO_DIR=${2} | ||
| ORIG_DIR=$(pwd) | ||
| CITOOLS_DIR=$REPO_DIR/ci_tools | ||
| REPO_TARBALL=${REPO_DIR}/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz | ||
| TMP_TARBALL=${TMP_DIR}/dateutil-zoneinfo.tar.gz | ||
| UPSTREAM_URL="https://github.com/eggert/tz.git" | ||
| function cleanup { | ||
| # Since this script modifies the original repo, whether or not | ||
| # it fails we need to restore the original file so as to not | ||
| # overwrite the user's local changes. | ||
| echo "Cleaning up." | ||
| if [ -f $TMP_TARBALL ]; then | ||
| cp -p $TMP_TARBALL $REPO_TARBALL | ||
| fi | ||
| } | ||
| trap cleanup EXIT | ||
| # Work in a temporary directory | ||
| cd $TMP_DIR | ||
| # Clone or update the repo | ||
| DIR_EXISTS=false | ||
| if [ -d tz ]; then | ||
| cd tz | ||
| if [[ $(git remote get-url origin) == ${UPSTREAM_URL} ]]; then | ||
| git fetch origin master | ||
| git reset --hard origin/master | ||
| DIR_EXISTS=true | ||
| else | ||
| cd .. | ||
| rm -rf tz | ||
| fi | ||
| fi | ||
| if [ "$DIR_EXISTS" = false ]; then | ||
| git clone ${UPSTREAM_URL} | ||
| cd tz | ||
| fi | ||
| # Get the version | ||
| make version | ||
| VERSION=$(cat version) | ||
| TARBALL_NAME=tzdata${VERSION}.tar.gz | ||
| # Make the tzdata tarball - deactivate errors because | ||
| # I don't know how to make just the .tar.gz and I don't | ||
| # care if the others fail | ||
| set +e | ||
| make traditional_tarballs | ||
| set -e | ||
| mv $TARBALL_NAME $ORIG_DIR | ||
| # Install everything else | ||
| make TOPDIR=$TMP_DIR/tzdir install | ||
| # | ||
| # Make the zoneinfo tarball | ||
| # | ||
| cd $ORIG_DIR | ||
| # Put the latest version of zic on the path | ||
| PATH=$TMP_DIR/tzdir/usr/sbin:${PATH} | ||
| # Stash the old zoneinfo file in the temporary directory | ||
| mv $REPO_TARBALL $TMP_TARBALL | ||
| # Make the metadata file | ||
| ZONEFILE_METADATA_NAME=zonefile_metadata_master.json | ||
| ${CITOOLS_DIR}/make_zonefile_metadata.py \ | ||
| $TARBALL_NAME \ | ||
| $VERSION \ | ||
| $ZONEFILE_METADATA_NAME | ||
| python ${REPO_DIR}/updatezinfo.py $ZONEFILE_METADATA_NAME | ||
| # Run the tests | ||
| python -m pytest ${REPO_DIR}/dateutil/test | ||
| import os | ||
| import pytest | ||
| # Configure pytest to ignore xfailing tests | ||
| # See: https://stackoverflow.com/a/53198349/467366 | ||
| def pytest_collection_modifyitems(items): | ||
| for item in items: | ||
| marker_getter = getattr(item, 'get_closest_marker', None) | ||
| # Python 3.3 support | ||
| if marker_getter is None: | ||
| marker_getter = item.get_marker | ||
| marker = marker_getter('xfail') | ||
| # Need to query the args because conditional xfail tests still have | ||
| # the xfail mark even if they are not expected to fail | ||
| if marker and (not marker.args or marker.args[0]): | ||
| item.add_marker(pytest.mark.no_cover) | ||
| def set_tzpath(): | ||
| """ | ||
| Sets the TZPATH variable if it's specified in an environment variable. | ||
| """ | ||
| tzpath = os.environ.get('DATEUTIL_TZPATH', None) | ||
| if tzpath is None: | ||
| return | ||
| path_components = tzpath.split(':') | ||
| print("Setting TZPATH to {}".format(path_components)) | ||
| from dateutil import tz | ||
| tz.TZPATHS.clear() | ||
| tz.TZPATHS.extend(path_components) | ||
| set_tzpath() |
| Exercises | ||
| ========= | ||
| It is often useful to work through some examples in order to understand how a module works; on this page, there are several exercises of varying difficulty that you can use to learn how to use ``dateutil``. | ||
| If you are interested in helping improve the documentation of ``dateutil``, it is recommended that you attempt to complete these exercises with no resources *other than dateutil's documentation*. If you find that the documentation is not clear enough to allow you to complete these exercises, open an issue on the `dateutil issue tracker <https://github.com/dateutil/dateutil/issues>`_ to let the developers know what part of the documentation needs improvement. | ||
| .. contents:: Table of Contents | ||
| :backlinks: top | ||
| :local: | ||
| .. _mlk-day-exercise: | ||
| Martin Luther King Day | ||
| -------------------------------- | ||
| `Martin Luther King, Jr Day <https://en.wikipedia.org/wiki/Martin_Luther_King_Jr._Day>`_ is a US holiday that occurs every year on the third Monday in January? | ||
| How would you generate a `recurrence rule <../rrule.html>`_ that generates Martin Luther King Day, starting from its first observance in 1986? | ||
| **Test Script** | ||
| To solve this exercise, copy-paste this script into a document, change anything between the ``--- YOUR CODE ---`` comment blocks. | ||
| .. raw:: html | ||
| <details> | ||
| .. code-block:: python3 | ||
| # ------- YOUR CODE -------------# | ||
| from dateutil import rrule | ||
| MLK_DAY = <<YOUR CODE HERE>> | ||
| # -------------------------------# | ||
| from datetime import datetime | ||
| MLK_TEST_CASES = [ | ||
| ((datetime(1970, 1, 1), datetime(1980, 1, 1)), | ||
| []), | ||
| ((datetime(1980, 1, 1), datetime(1989, 1, 1)), | ||
| [datetime(1986, 1, 20), | ||
| datetime(1987, 1, 19), | ||
| datetime(1988, 1, 18)]), | ||
| ((datetime(2017, 2, 1), datetime(2022, 2, 1)), | ||
| [datetime(2018, 1, 15, 0, 0), | ||
| datetime(2019, 1, 21, 0, 0), | ||
| datetime(2020, 1, 20, 0, 0), | ||
| datetime(2021, 1, 18, 0, 0), | ||
| datetime(2022, 1, 17, 0, 0)] | ||
| ), | ||
| ] | ||
| def test_mlk_day(): | ||
| for (between_args, expected) in MLK_TEST_CASES: | ||
| assert MLK_DAY.between(*between_args) == expected | ||
| if __name__ == "__main__": | ||
| test_mlk_day() | ||
| print('Success!') | ||
| .. raw:: html | ||
| </details> | ||
| A solution to this problem is provided :doc:`here <solutions/mlk-day-rrule>`. | ||
| Next Monday meeting | ||
| ------------------- | ||
| A team has a meeting at 10 AM every Monday and wants a function that tells them, given a ``datetime.datetime`` object, what is the date and time of the *next* Monday meeting? This is probably best accomplished using a `relativedelta <../relativedelta.html>`_. | ||
| **Test Script** | ||
| To solve this exercise, copy-paste this script into a document, change anything between the ``--- YOUR CODE ---`` comment blocks. | ||
| .. raw:: html | ||
| <details> | ||
| .. code-block:: python3 | ||
| # --------- YOUR CODE -------------- # | ||
| from dateutil import relativedelta | ||
| def next_monday(dt): | ||
| <<YOUR CODE HERE>> | ||
| # ---------------------------------- # | ||
| from datetime import datetime | ||
| from dateutil import tz | ||
| NEXT_MONDAY_CASES = [ | ||
| (datetime(2018, 4, 11, 14, 30, 15, 123456), | ||
| datetime(2018, 4, 16, 10, 0)), | ||
| (datetime(2018, 4, 16, 10, 0), | ||
| datetime(2018, 4, 16, 10, 0)), | ||
| (datetime(2018, 4, 16, 10, 30), | ||
| datetime(2018, 4, 23, 10, 0)), | ||
| (datetime(2018, 4, 14, 9, 30, tzinfo=tz.gettz('America/New_York')), | ||
| datetime(2018, 4, 16, 10, 0, tzinfo=tz.gettz('America/New_York'))), | ||
| ] | ||
| def test_next_monday_1(): | ||
| for dt_in, dt_out in NEXT_MONDAY_CASES: | ||
| assert next_monday(dt_in) == dt_out | ||
| if __name__ == "__main__": | ||
| test_next_monday_1() | ||
| print('Success!') | ||
| .. raw:: html | ||
| </details> | ||
| Parsing a local tzname | ||
| ---------------------- | ||
| Three-character time zone abbreviations are *not* unique in that they do not explicitly map to a time zone. A list of time zone abbreviations in use can be found `here <https://www.timeanddate.com/time/zones/>`_. This means that parsing a datetime string such as ``'2018-01-01 12:30:30 CST'`` is ambiguous without context. Using `dateutil.parser <../parser.html>`_ and `dateutil.tz <../tz.html>`_, it is possible to provide a context such that these local names are converted to proper time zones. | ||
| Problem 1 | ||
| ********* | ||
| Given the context that you will only be parsing dates coming from the continental United States, India and Japan, write a function that parses a datetime string and returns a timezone-aware ``datetime`` with an IANA-style timezone attached. | ||
| Note: For the purposes of the experiment, you may ignore the portions of the United States like Arizona and parts of Indiana that do not observe daylight saving time. | ||
| **Test Script** | ||
| To solve this exercise, copy-paste this script into a document, change anything between the ``--- YOUR CODE ---`` comment blocks. | ||
| .. raw:: html | ||
| <details> | ||
| .. code-block:: python3 | ||
| # --------- YOUR CODE -------------- # | ||
| from dateutil.parser import parse | ||
| from dateutil import tz | ||
| def parse_func_us_jp_ind(): | ||
| <<YOUR CODE HERE>> | ||
| # ---------------------------------- # | ||
| from dateutil import tz | ||
| from datetime import datetime | ||
| PARSE_TZ_TEST_DATETIMES = [ | ||
| datetime(2018, 1, 1, 12, 0), | ||
| datetime(2018, 3, 20, 2, 0), | ||
| datetime(2018, 5, 12, 3, 30), | ||
| datetime(2014, 9, 1, 23) | ||
| ] | ||
| PARSE_TZ_TEST_ZONES = [ | ||
| tz.gettz('America/New_York'), | ||
| tz.gettz('America/Chicago'), | ||
| tz.gettz('America/Denver'), | ||
| tz.gettz('America/Los_Angeles'), | ||
| tz.gettz('Asia/Kolkata'), | ||
| tz.gettz('Asia/Tokyo'), | ||
| ] | ||
| def test_parse(): | ||
| for tzi in PARSE_TZ_TEST_ZONES: | ||
| for dt in PARSE_TZ_TEST_DATETIMES: | ||
| dt_exp = dt.replace(tzinfo=tzi) | ||
| dtstr = dt_exp.strftime('%Y-%m-%d %H:%M:%S %Z') | ||
| dt_act = parse_func_us_jp_ind(dtstr) | ||
| assert dt_act == dt_exp | ||
| assert dt_act.tzinfo is dt_exp.tzinfo | ||
| if __name__ == "__main__": | ||
| test_parse() | ||
| print('Success!') | ||
| .. raw:: html | ||
| </details> | ||
| Problem 2 | ||
| ********* | ||
| Given the context that you will *only* be passed dates from India or Ireland, write a function that correctly parses all *unambiguous* time zone strings to aware datetimes localized to the correct IANA zone, and for *ambiguous* time zone strings default to India. | ||
| **Test Script** | ||
| To solve this exercise, copy-paste this script into a document, change anything between the ``--- YOUR CODE ---`` comment blocks. | ||
| .. raw:: html | ||
| <details> | ||
| .. code-block:: python3 | ||
| # --------- YOUR CODE -------------- # | ||
| from dateutil.parser import parse | ||
| from dateutil import tz | ||
| def parse_func_ind_ire(): | ||
| <<YOUR CODE HERE>> | ||
| # ---------------------------------- # | ||
| ISRAEL = tz.gettz('Asia/Jerusalem') | ||
| INDIA = tz.gettz('Asia/Kolkata') | ||
| PARSE_IXT_TEST_CASE = [ | ||
| ('2018-02-03 12:00 IST+02:00', datetime(2018, 2, 3, 12, tzinfo=ISRAEL)), | ||
| ('2018-06-14 12:00 IDT+03:00', datetime(2018, 6, 14, 12, tzinfo=ISRAEL)), | ||
| ('2018-06-14 12:00 IST', datetime(2018, 6, 14, 12, tzinfo=INDIA)), | ||
| ('2018-06-14 12:00 IST+05:30', datetime(2018, 6, 14, 12, tzinfo=INDIA)), | ||
| ('2018-02-03 12:00 IST', datetime(2018, 2, 3, 12, tzinfo=INDIA)), | ||
| ] | ||
| def test_parse_ixt(): | ||
| for dtstr, dt_exp in PARSE_IXT_TEST_CASE: | ||
| dt_act = parse_func_ind_ire(dtstr) | ||
| assert dt_act == dt_exp, (dt_act, dt_exp) | ||
| assert dt_act.tzinfo is dt_exp.tzinfo, (dt_act, dt_exp) | ||
| if __name__ == "__main__": | ||
| test_parse_ixt() | ||
| print('Success!') | ||
| .. raw:: html | ||
| </details> | ||
| # ------- YOUR CODE -------------# | ||
| from dateutil import rrule | ||
| from datetime import datetime | ||
| MLK_DAY = rrule.rrule( | ||
| dtstart=datetime(1986, 1, 20), # First celebration | ||
| freq=rrule.YEARLY, # Occurs once per year | ||
| bymonth=1, # In January | ||
| byweekday=rrule.MO(+3), # On the 3rd Monday | ||
| ) | ||
| # -------------------------------# | ||
| from datetime import datetime | ||
| MLK_TEST_CASES = [ | ||
| ((datetime(1970, 1, 1), datetime(1980, 1, 1)), | ||
| []), | ||
| ((datetime(1980, 1, 1), datetime(1989, 1, 1)), | ||
| [datetime(1986, 1, 20), | ||
| datetime(1987, 1, 19), | ||
| datetime(1988, 1, 18)]), | ||
| ((datetime(2017, 2, 1), datetime(2022, 2, 1)), | ||
| [datetime(2018, 1, 15, 0, 0), | ||
| datetime(2019, 1, 21, 0, 0), | ||
| datetime(2020, 1, 20, 0, 0), | ||
| datetime(2021, 1, 18, 0, 0), | ||
| datetime(2022, 1, 17, 0, 0)] | ||
| ), | ||
| ] | ||
| def test_mlk_day(): | ||
| for (between_args, expected) in MLK_TEST_CASES: | ||
| assert MLK_DAY.between(*between_args) == expected | ||
| if __name__ == "__main__": | ||
| test_mlk_day() | ||
| print('Success!') |
| :orphan: | ||
| Martin Luther King Day: Solution | ||
| ================================ | ||
| Presented here is a solution to the :ref:`Martin Luther King Day exercises <mlk-day-exercise>`. | ||
| .. include:: mlk_day_rrule_solution.py | ||
| :code: python3 | ||
| ====== | ||
| tz.win | ||
| ====== | ||
| .. py:currentmodule:: dateutil.tz.win | ||
| .. automodule:: dateutil.tz.win | ||
| Classes | ||
| ------- | ||
| .. autoclass:: tzres | ||
| :members: | ||
| .. autoclass:: tzwin | ||
| :members: list, display, transitions | ||
| :undoc-members: | ||
| .. autoclass:: tzwinlocal | ||
| :members: display, transitions | ||
| :undoc-members: | ||
+10
-1
@@ -21,2 +21,9 @@ sudo: false | ||
| env: TOXENV=docs | ||
| - python: 3.6 | ||
| env: TOXENV=tz | ||
| - python: 3.7 | ||
| # This is required until Travis has a default image that | ||
| # can run Python 3.7 | ||
| dist: xenial | ||
| sudo: required | ||
| allow_failures: | ||
@@ -27,3 +34,5 @@ - python: "nightly" | ||
| - pip install -U six && pip install -U tox | ||
| - ./ci_tools/retry.sh python updatezinfo.py | ||
| - if [[ $TRAVIS_PYTHON_VERSION == "3.3" ]]; then pip install 'virtualenv<16.0'; fi | ||
| - if [[ $TRAVIS_PYTHON_VERSION == "3.3" ]]; then pip install 'setuptools<40.0'; fi | ||
| - if [[ $TOXENV == "py" ]]; then ./ci_tools/retry.sh python updatezinfo.py; fi | ||
@@ -30,0 +39,0 @@ script: |
+28
-12
@@ -20,6 +20,9 @@ This is a (possibly incomplete) list of all the contributors to python-dateutil, | ||
| - Alex Verdyan <verdyan@MASKED> | ||
| - Alex Willmer <alex@MASKED> (gh: @moreati) | ||
| - Alex Willmer <alex@moreati.org.uk> (gh: @moreati) **R** | ||
| - Alexander Brugh <alexander.brugh@MASKED> (gh: @abrugh) | ||
| - Alistair McMaster <alistair@MASKED> (gh: @alimcmaster1 ) **D** | ||
| - Andrew Bennett (gh: @andrewcbennett) **D** | ||
| - Andrew Murray <radarhere@MASKED> | ||
| - Bernat Gabor <bgabor8@bloomberg.net> (gh: @gaborbernat) **D** | ||
| - Bradlee Speice <bradlee@speice.io> (gh: @bspeice) **D** | ||
| - Brandon W Maister <quodlibetor@MASKED> | ||
@@ -29,14 +32,19 @@ - Brock Mendel <jbrockmendel@MASKED> (gh: @jbrockmendel) **R** | ||
| - Carlos <carlosxl@MASKED> | ||
| - Cheuk Ting Ho <cheukting.ho@gmail.com> (gh: @cheukting) **D** | ||
| - Chris van den Berg (gh: bergvca) **D** | ||
| - Christopher Cordero <ccordero@pm.me> (gh: cs-cordero) **D** | ||
| - Christopher Corley <cscorley@MASKED> | ||
| - Claudio Canepa <ccanepacc@MASKED> | ||
| - Corey Girard <corey.r.girard@gmail.com> (gh: @coreygirard) | ||
| - Corey Girard <corey.r.girard@gmail.com> (gh: @coreygirard) **D** | ||
| - Cosimo Lupo <cosimo@anthrotype.com> (gh: @anthrotype) **D** | ||
| - Daniel Lepage <dplepage@MASKED> | ||
| - David Lehrian <david@MASKED> | ||
| - Dean Allsopp (gh: @daplantagenet) **D** | ||
| - Dominik Kozaczko <dominik@MASKED> | ||
| - Elliot Hughes <elliot.hughes@gmail.com> (gh: @ElliotJH) **D** | ||
| - Elvis Pranskevichus <el@MASKED> | ||
| - Florian Rathgeber (gh: @kynan) | ||
| - Florian Rathgeber (gh: @kynan) **D** | ||
| - Gabriel Bianconi <gabriel@MASKED> (gh: @GabrielBianconi) **D** | ||
| - Gabriel Poesia <gabriel.poesia@MASKED> | ||
| - Gökçen Nurlu <gnurlu1@bloomberg.net> (gh: @gokcennurlu) **D** | ||
| - Gustavo Niemeyer <gustavo@niemeyer.net> (gh: @niemeyer) | ||
@@ -48,10 +56,13 @@ - Holger Joukl <holger.joukl@MASKED> (gh: @hjoukl) | ||
| - Jan Studený <jendas1@MASKED> | ||
| - Jay Weisskopf <jay@jayschwa.net> (gh: @jayschwa) **D** | ||
| - Jitesh <jitesh@MASKED> | ||
| - John Purviance <jpurviance@MASKED> (gh @jpurviance) **D** | ||
| - Jon Dufresne <jon.dufresne@MASKED> (gh: @jdufresne) **R** | ||
| - Jonas Neubert <jonas@MASKED> | ||
| - Kevin Nguyen <kvn219@MASKED> | ||
| - Jonas Neubert <jonas@MASKED> (gh: @jonemo) **R** | ||
| - Kevin Nguyen <kvn219@MASKED> **D** | ||
| - Kirit Thadaka <kirit.thadaka@gmail.com> (gh: @kirit93) **D** | ||
| - Kubilay Kocak <koobs@MASKED> | ||
| - Laszlo Kiss Kollar <kiss.kollar.laszlo@MASKED> (gh: @lkollar) **D** | ||
| - Lauren Oldja <oldja@MASKED> (gh: @loldja) | ||
| - Lauren Oldja <oldja@MASKED> (gh: @loldja) **D** | ||
| - Luca Ferocino <luca.ferox@MASKED> (gh: @lucaferocino) **D** | ||
| - Mario Corchero <mcorcherojim@MASKED> (gh: @mariocj89) **R** | ||
@@ -65,9 +76,12 @@ - Mateusz Dziedzic (gh: @m-dz) **D** | ||
| - Mike Gilbert <floppym@MASKED> | ||
| - Nicholas Herrriot <Nicholas.Herriot@gmail.com> **D** | ||
| - Nicolas Évrard (gh: @nicoe) **D** | ||
| - Nick Smith <nick.smith@MASKED> | ||
| - Orson Adams <orson.network@MASKED> (gh: @parsethis) | ||
| - Orson Adams <orson.network@MASKED> (gh: @parsethis) **D** | ||
| - Paul Dickson (gh @prdickson) **D** | ||
| - Paul Ganssle <paul@ganssle.io> (gh: @pganssle) **R** | ||
| - Pascal van Kooten <kootenpv@MASKED> | ||
| - Pascal van Kooten <kootenpv@MASKED> (gh: @kootenpv) **R** | ||
| - Pavel Ponomarev <comrad.awsum@MASKED> | ||
| - Peter Bieringer <pb@MASKED> | ||
| - Pierre Gergondet <pierre.gergondet@MASKED> (gh: @gergondet) | ||
| - Pierre Gergondet <pierre.gergondet@MASKED> (gh: @gergondet) **D** | ||
| - Quentin Pradet <quentin@MASKED> | ||
@@ -78,7 +92,9 @@ - Raymond Cha (gh: @weatherpattern) **D** | ||
| - Rustem Saiargaliev (gh: @amureki) **D** | ||
| - Satyabrat Bhol <satyabrat35@MASKED> (gh: @Satyabrat35) **D** | ||
| - Savraj <savraj@MASKED> | ||
| - Sergey Vishnikin <armicron@MASKED> | ||
| - Sherry Zhou (gh: @cssherry) **D** | ||
| - Stefan Bonchev **D** | ||
| - Thierry Bastian <thierryb@MASKED> | ||
| - Thomas A Caswell <tcaswell@MASKED> (gh: @tacaswell) | ||
| - Thomas A Caswell <tcaswell@MASKED> (gh: @tacaswell) **R** | ||
| - Thomas Achtemichuk <tom@MASKED> | ||
@@ -98,6 +114,6 @@ - Thomas Kluyver <takowl@MASKED> (gh: @takluyver) | ||
| - gl <gl@MASKED> | ||
| - labrys <labrys@MASKED> (gh: @labrys) | ||
| - labrys <labrys@MASKED> (gh: @labrys) **R** | ||
| - ms-boom <ms-boom@MASKED> | ||
| - ryanss <ryanssdev@MASKED> (gh: @ryanss) | ||
| - ryanss <ryanssdev@MASKED> (gh: @ryanss) **R** | ||
| Unless someone has deliberately given permission to publish their e-mail, I have masked the domain names. If you are not on this list and believe you should be, or you *are* on this list and your information is inaccurate, please e-mail the current maintainer or the mailing list (dateutil@python.org) with your name, e-mail (if desired) and github (if desired / applicable), as you would like them displayed. Additionally, please indicate if you are willing to dual license your old contributions under Apache 2.0. |
+1
-1
@@ -31,3 +31,3 @@ # Contributing | ||
| #### Changelog | ||
| To keep users abreast of the changes to the module and to give proper credit, `dateutil` maintains a changelog, which is managed by [towncrier](https://github.com/hawkowl/towncrier). To add a changelog entry, make a new file called `<issue_no>.<type>.rst`, where `<issue_no>` is the number of the PR you've just made (it's easiest to add the changelog *after* you've created the PR so you'll have this number), and `<type>` is one of the following types: | ||
| To keep users abreast of the changes to the module and to give proper credit, `dateutil` maintains a changelog, which is managed by [towncrier](https://github.com/hawkowl/towncrier). To add a changelog entry, make a new file called `<issue_no>.<type>.rst` in the `changelog.d` directory, where `<issue_no>` is the number of the PR you've just made (it's easiest to add the changelog *after* you've created the PR so you'll have this number), and `<type>` is one of the following types: | ||
@@ -34,0 +34,0 @@ - `feature`: A new feature, (e.g. a new function, method, attribute, etc) |
| # coding: utf-8 | ||
| # file generated by setuptools_scm | ||
| # don't change, don't track in version control | ||
| version = '2.7.5' | ||
| version = '2.8.0' |
@@ -43,3 +43,3 @@ # -*- coding: utf-8 -*- | ||
| import six | ||
| from six import binary_type, integer_types, text_type | ||
| from six import integer_types, text_type | ||
@@ -67,3 +67,3 @@ from decimal import Decimal | ||
| # a 'decode' function, and we'd be double-decoding | ||
| if isinstance(instream, (binary_type, bytearray)): | ||
| if isinstance(instream, (bytes, bytearray)): | ||
| instream = instream.decode() | ||
@@ -296,3 +296,3 @@ else: | ||
| ("pm", "p")] | ||
| UTCZONE = ["UTC", "GMT", "Z"] | ||
| UTCZONE = ["UTC", "GMT", "Z", "z"] | ||
| PERTAIN = ["of"] | ||
@@ -394,3 +394,4 @@ TZOFFSET = {} | ||
| if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z': | ||
| if ((res.tzoffset == 0 and not res.tzname) or | ||
| (res.tzname == 'Z' or res.tzname == 'z')): | ||
| res.tzname = "UTC" | ||
@@ -1067,3 +1068,4 @@ res.tzoffset = 0 | ||
| len(token) <= 5 and | ||
| all(x in string.ascii_uppercase for x in token)) | ||
| (all(x in string.ascii_uppercase for x in token) | ||
| or token in self.info.UTCZONE)) | ||
@@ -1070,0 +1072,0 @@ def _ampm_valid(self, hour, ampm, fuzzy): |
@@ -91,7 +91,9 @@ # -*- coding: utf-8 -*- | ||
| - ``hh:mm:ss`` or ``hhmmss`` | ||
| - ``hh:mm:ss.sss`` or ``hh:mm:ss.ssssss`` (3-6 sub-second digits) | ||
| - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits) | ||
| Midnight is a special case for `hh`, as the standard supports both | ||
| 00:00 and 24:00 as a representation. | ||
| 00:00 and 24:00 as a representation. The decimal separator can be | ||
| either a dot or a comma. | ||
| .. caution:: | ||
@@ -141,2 +143,6 @@ | ||
| if len(components) > 3 and components[3] == 24: | ||
| components[3] = 0 | ||
| return datetime(*components) + timedelta(days=1) | ||
| return datetime(*components) | ||
@@ -172,3 +178,6 @@ | ||
| """ | ||
| return time(*self._parse_isotime(timestr)) | ||
| components = self._parse_isotime(timestr) | ||
| if components[0] == 24: | ||
| components[0] = 0 | ||
| return time(*components) | ||
@@ -196,6 +205,5 @@ @_takes_ascii | ||
| # Constants | ||
| _MICROSECOND_END_REGEX = re.compile(b'[-+Z]+') | ||
| _DATE_SEP = b'-' | ||
| _TIME_SEP = b':' | ||
| _MICRO_SEP = b'.' | ||
| _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)') | ||
@@ -340,3 +348,3 @@ def _parse_isodate(self, dt_str): | ||
| if timestr[pos:pos + 1] in b'-+Z': | ||
| if timestr[pos:pos + 1] in b'-+Zz': | ||
| # Detect time zone boundary | ||
@@ -356,12 +364,10 @@ components[-1] = self._parse_tzstr(timestr[pos:]) | ||
| if comp == 3: | ||
| # Microsecond | ||
| if timestr[pos:pos + 1] != self._MICRO_SEP: | ||
| # Fraction of a second | ||
| frac = self._FRACTION_REGEX.match(timestr[pos:]) | ||
| if not frac: | ||
| continue | ||
| pos += 1 | ||
| us_str = self._MICROSECOND_END_REGEX.split(timestr[pos:pos + 6], | ||
| 1)[0] | ||
| us_str = frac.group(1)[:6] # Truncate to microseconds | ||
| components[comp] = int(us_str) * 10**(6 - len(us_str)) | ||
| pos += len(us_str) | ||
| pos += len(frac.group()) | ||
@@ -375,3 +381,2 @@ if pos < len_str: | ||
| raise ValueError('Hour may only be 24 at 24:00:00.000') | ||
| components[0] = 0 | ||
@@ -381,3 +386,3 @@ return components | ||
| def _parse_tzstr(self, tzstr, zero_as_utc=True): | ||
| if tzstr == b'Z': | ||
| if tzstr == b'Z' or tzstr == b'z': | ||
| return tz.tzutc() | ||
@@ -384,0 +389,0 @@ |
@@ -20,4 +20,8 @@ # -*- coding: utf-8 -*- | ||
| """ | ||
| The relativedelta type is based on the specification of the excellent | ||
| work done by M.-A. Lemburg in his | ||
| The relativedelta type is designed to be applied to an existing datetime and | ||
| can replace specific components of that datetime, or represents an interval | ||
| of time. | ||
| It is based on the specification of the excellent work done by M.-A. Lemburg | ||
| in his | ||
| `mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension. | ||
@@ -49,9 +53,11 @@ However, notice that this type does *NOT* implement the same algorithm as | ||
| weekday: | ||
| One of the weekday instances (MO, TU, etc). These | ||
| instances may receive a parameter N, specifying the Nth | ||
| weekday, which could be positive or negative (like MO(+1) | ||
| or MO(-2). Not specifying it is the same as specifying | ||
| +1. You can also use an integer, where 0=MO. Notice that | ||
| if the calculated date is already Monday, for example, | ||
| using MO(1) or MO(-1) won't change the day. | ||
| One of the weekday instances (MO, TU, etc) available in the | ||
| relativedelta module. These instances may receive a parameter N, | ||
| specifying the Nth weekday, which could be positive or negative | ||
| (like MO(+1) or MO(-2)). Not specifying it is the same as specifying | ||
| +1. You can also use an integer, where 0=MO. This argument is always | ||
| relative e.g. if the calculated date is already Monday, using MO(1) | ||
| or MO(-1) won't change the day. To effectively make it absolute, use | ||
| it in combination with the day argument (e.g. day=1, MO(1) for first | ||
| Monday of the month). | ||
@@ -87,5 +93,8 @@ leapdays: | ||
| >>> from datetime import datetime | ||
| >>> from dateutil.relativedelta import relativedelta, MO | ||
| >>> dt = datetime(2018, 4, 9, 13, 37, 0) | ||
| >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) | ||
| datetime(2018, 4, 2, 14, 37, 0) | ||
| >>> dt + delta | ||
| datetime.datetime(2018, 4, 2, 14, 37) | ||
@@ -282,3 +291,3 @@ First, the day is set to 1 (the first of the month), then 25 hours | ||
| >>> relativedelta(days=1.5, hours=2).normalized() | ||
| relativedelta(days=1, hours=14) | ||
| relativedelta(days=+1, hours=+14) | ||
@@ -285,0 +294,0 @@ :return: |
@@ -5,3 +5,3 @@ from dateutil.easter import easter | ||
| from datetime import date | ||
| import unittest | ||
| import pytest | ||
@@ -77,21 +77,19 @@ # List of easters between 1990 and 2050 | ||
| class EasterTest(unittest.TestCase): | ||
| def testEasterWestern(self): | ||
| for easter_date in western_easter_dates: | ||
| self.assertEqual(easter_date, | ||
| easter(easter_date.year, EASTER_WESTERN)) | ||
| @pytest.mark.parametrize("easter_date", western_easter_dates) | ||
| def test_easter_western(easter_date): | ||
| assert easter_date == easter(easter_date.year, EASTER_WESTERN) | ||
| def testEasterOrthodox(self): | ||
| for easter_date in orthodox_easter_dates: | ||
| self.assertEqual(easter_date, | ||
| easter(easter_date.year, EASTER_ORTHODOX)) | ||
| def testEasterJulian(self): | ||
| for easter_date in julian_easter_dates: | ||
| self.assertEqual(easter_date, | ||
| easter(easter_date.year, EASTER_JULIAN)) | ||
| @pytest.mark.parametrize("easter_date", orthodox_easter_dates) | ||
| def test_easter_orthodox(easter_date): | ||
| assert easter_date == easter(easter_date.year, EASTER_ORTHODOX) | ||
| def testEasterBadMethod(self): | ||
| # Invalid methods raise ValueError | ||
| with self.assertRaises(ValueError): | ||
| easter(1975, 4) | ||
| @pytest.mark.parametrize("easter_date", julian_easter_dates) | ||
| def test_easter_julian(easter_date): | ||
| assert easter_date == easter(easter_date.year, EASTER_JULIAN) | ||
| def test_easter_bad_method(): | ||
| with pytest.raises(ValueError): | ||
| easter(1975, 4) |
| """Test for the "import *" functionality. | ||
| As imort * can be only done at module level, it has been added in a separate file | ||
| As import * can be only done at module level, it has been added in a separate file | ||
| """ | ||
| import unittest | ||
| import pytest | ||
@@ -13,22 +13,22 @@ prev_locals = list(locals()) | ||
| class ImportStarTest(unittest.TestCase): | ||
| """ Test that `from dateutil import *` adds the modules in __all__ locally""" | ||
| def testImportedModules(self): | ||
| import dateutil.easter | ||
| import dateutil.parser | ||
| import dateutil.relativedelta | ||
| import dateutil.rrule | ||
| import dateutil.tz | ||
| import dateutil.utils | ||
| import dateutil.zoneinfo | ||
| @pytest.mark.import_star | ||
| def test_imported_modules(): | ||
| """ Test that `from dateutil import *` adds modules in __all__ locally """ | ||
| import dateutil.easter | ||
| import dateutil.parser | ||
| import dateutil.relativedelta | ||
| import dateutil.rrule | ||
| import dateutil.tz | ||
| import dateutil.utils | ||
| import dateutil.zoneinfo | ||
| self.assertEquals(dateutil.easter, new_locals.pop("easter")) | ||
| self.assertEquals(dateutil.parser, new_locals.pop("parser")) | ||
| self.assertEquals(dateutil.relativedelta, new_locals.pop("relativedelta")) | ||
| self.assertEquals(dateutil.rrule, new_locals.pop("rrule")) | ||
| self.assertEquals(dateutil.tz, new_locals.pop("tz")) | ||
| self.assertEquals(dateutil.utils, new_locals.pop("utils")) | ||
| self.assertEquals(dateutil.zoneinfo, new_locals.pop("zoneinfo")) | ||
| assert dateutil.easter == new_locals.pop("easter") | ||
| assert dateutil.parser == new_locals.pop("parser") | ||
| assert dateutil.relativedelta == new_locals.pop("relativedelta") | ||
| assert dateutil.rrule == new_locals.pop("rrule") | ||
| assert dateutil.tz == new_locals.pop("tz") | ||
| assert dateutil.utils == new_locals.pop("utils") | ||
| assert dateutil.zoneinfo == new_locals.pop("zoneinfo") | ||
| self.assertFalse(new_locals) | ||
| assert not new_locals |
@@ -123,3 +123,4 @@ # -*- coding: utf-8 -*- | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('time_fmt', (x + '.%f' for x in HMS_FMTS)) | ||
| @pytest.mark.parametrize('time_fmt', (x + sep + '%f' for x in HMS_FMTS | ||
| for sep in '.,')) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
@@ -133,2 +134,11 @@ @pytest.mark.parametrize('precision', list(range(3, 7))) | ||
| ### | ||
| # Truncation of extra digits beyond microsecond precision | ||
| @pytest.mark.parametrize('dt_str', [ | ||
| '2018-07-03T14:07:00.123456000001', | ||
| '2018-07-03T14:07:00.123456999999', | ||
| ]) | ||
| def test_extra_subsecond_digits(dt_str): | ||
| assert isoparse(dt_str) == datetime(2018, 7, 3, 14, 7, 0, 123456) | ||
| @pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) | ||
@@ -144,11 +154,11 @@ def test_full_tzoffsets(tzoffset): | ||
| '2014-04-11T00', | ||
| '2014-04-11T24', | ||
| '2014-04-10T24', | ||
| '2014-04-11T00:00', | ||
| '2014-04-11T24:00', | ||
| '2014-04-10T24:00', | ||
| '2014-04-11T00:00:00', | ||
| '2014-04-11T24:00:00', | ||
| '2014-04-10T24:00:00', | ||
| '2014-04-11T00:00:00.000', | ||
| '2014-04-11T24:00:00.000', | ||
| '2014-04-10T24:00:00.000', | ||
| '2014-04-11T00:00:00.000000', | ||
| '2014-04-11T24:00:00.000000'] | ||
| '2014-04-10T24:00:00.000000'] | ||
| ) | ||
@@ -224,2 +234,4 @@ def test_datetime_midnight(dt_str): | ||
| tz.tzutc())), | ||
| (b'2014-02-04T12:30:15.224z', datetime(2014, 2, 4, 12, 30, 15, 224000, | ||
| tz.tzutc())), | ||
| (b'2014-02-04T12:30:15.224+05:00', | ||
@@ -270,8 +282,11 @@ datetime(2014, 2, 4, 12, 30, 15, 224000, | ||
| @pytest.mark.parametrize('sep_act,valid_sep', [ | ||
| ('C', 'T'), | ||
| ('T', 'C') | ||
| @pytest.mark.parametrize('sep_act, valid_sep, exception', [ | ||
| ('T', 'C', ValueError), | ||
| ('C', 'T', ValueError), | ||
| ]) | ||
| def test_iso_raises_sep(sep_act, valid_sep): | ||
| def test_iso_with_sep_raises(sep_act, valid_sep, exception): | ||
| parser = isoparser(sep=valid_sep) | ||
| isostr = '2012-04-25' + sep_act + '01:25:00' | ||
| with pytest.raises(exception): | ||
| parser.isoparse(isostr) | ||
@@ -375,3 +390,3 @@ | ||
| d_str = d_str.encode('ascii') | ||
| elif isinstance(d_str, six.binary_type) and not as_bytes: | ||
| elif isinstance(d_str, bytes) and not as_bytes: | ||
| d_str = d_str.decode('ascii') | ||
@@ -450,3 +465,3 @@ | ||
| tstr = tstr.encode('ascii') | ||
| elif isinstance(time_val, six.binary_type) and not as_bytes: | ||
| elif isinstance(time_val, bytes) and not as_bytes: | ||
| tstr = tstr.decode('ascii') | ||
@@ -458,2 +473,18 @@ | ||
| @pytest.mark.parametrize('isostr', [ | ||
| '24:00', | ||
| '2400', | ||
| '24:00:00', | ||
| '240000', | ||
| '24:00:00.000', | ||
| '24:00:00,000', | ||
| '24:00:00.000000', | ||
| '24:00:00,000000', | ||
| ]) | ||
| def test_isotime_midnight(isostr): | ||
| iparser = isoparser() | ||
| assert iparser.parse_isotime(isostr) == time(0, 0, 0, 0) | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
@@ -464,3 +495,2 @@ ('3', ValueError), # ISO string too short | ||
| ('1430:15', ValueError), # Inconsistent separator use | ||
| ('14:30:15.3684000309', ValueError), # Too much us precision | ||
| ('25', ValueError), # Invalid hours | ||
@@ -470,3 +500,3 @@ ('25:15', ValueError), # Invalid hours | ||
| ('14:59:61', ValueError), # Invalid seconds | ||
| ('14:30:15.3446830500', ValueError), # No sign in time zone | ||
| ('14:30:15.34468305:00', ValueError), # No sign in time zone | ||
| ('14:30:15+', ValueError), # Time zone too short | ||
@@ -477,2 +507,6 @@ ('14:30:15+1234567', ValueError), # Time zone invalid | ||
| ('14:59:30_344583', ValueError), # Invalid microsecond separator | ||
| ('24:01', ValueError), # 24 used for non-midnight time | ||
| ('24:00:01', ValueError), # 24 used for non-midnight time | ||
| ('24:00:00.001', ValueError), # 24 used for non-midnight time | ||
| ('24:00:00.000001', ValueError), # 24 used for non-midnight time | ||
| ]) | ||
@@ -479,0 +513,0 @@ def test_isotime_raises(isostr, exception): |
+180
-438
@@ -17,3 +17,3 @@ # -*- coding: utf-8 -*- | ||
| from six import assertRaisesRegex, PY3 | ||
| from six.moves import StringIO | ||
| from io import StringIO | ||
@@ -31,3 +31,127 @@ import pytest | ||
| # Parser test cases using no keyword arguments. Format: (parsable_text, expected_datetime, assertion_message) | ||
| PARSER_TEST_CASES = [ | ||
| ("Thu Sep 25 10:36:28 2003", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Thu Sep 25 2003", datetime(2003, 9, 25), "date command format strip"), | ||
| ("2003-09-25T10:49:41", datetime(2003, 9, 25, 10, 49, 41), "iso format strip"), | ||
| ("2003-09-25T10:49", datetime(2003, 9, 25, 10, 49), "iso format strip"), | ||
| ("2003-09-25T10", datetime(2003, 9, 25, 10), "iso format strip"), | ||
| ("2003-09-25", datetime(2003, 9, 25), "iso format strip"), | ||
| ("20030925T104941", datetime(2003, 9, 25, 10, 49, 41), "iso stripped format strip"), | ||
| ("20030925T1049", datetime(2003, 9, 25, 10, 49, 0), "iso stripped format strip"), | ||
| ("20030925T10", datetime(2003, 9, 25, 10), "iso stripped format strip"), | ||
| ("20030925", datetime(2003, 9, 25), "iso stripped format strip"), | ||
| ("2003-09-25 10:49:41,502", datetime(2003, 9, 25, 10, 49, 41, 502000), "python logger format"), | ||
| ("199709020908", datetime(1997, 9, 2, 9, 8), "no separator"), | ||
| ("19970902090807", datetime(1997, 9, 2, 9, 8, 7), "no separator"), | ||
| ("2003-09-25", datetime(2003, 9, 25), "date with dash"), | ||
| ("09-25-2003", datetime(2003, 9, 25), "date with dash"), | ||
| ("25-09-2003", datetime(2003, 9, 25), "date with dash"), | ||
| ("10-09-2003", datetime(2003, 10, 9), "date with dash"), | ||
| ("10-09-03", datetime(2003, 10, 9), "date with dash"), | ||
| ("2003.09.25", datetime(2003, 9, 25), "date with dot"), | ||
| ("09.25.2003", datetime(2003, 9, 25), "date with dot"), | ||
| ("25.09.2003", datetime(2003, 9, 25), "date with dot"), | ||
| ("10.09.2003", datetime(2003, 10, 9), "date with dot"), | ||
| ("10.09.03", datetime(2003, 10, 9), "date with dot"), | ||
| ("2003/09/25", datetime(2003, 9, 25), "date with slash"), | ||
| ("09/25/2003", datetime(2003, 9, 25), "date with slash"), | ||
| ("25/09/2003", datetime(2003, 9, 25), "date with slash"), | ||
| ("10/09/2003", datetime(2003, 10, 9), "date with slash"), | ||
| ("10/09/03", datetime(2003, 10, 9), "date with slash"), | ||
| ("2003 09 25", datetime(2003, 9, 25), "date with space"), | ||
| ("09 25 2003", datetime(2003, 9, 25), "date with space"), | ||
| ("25 09 2003", datetime(2003, 9, 25), "date with space"), | ||
| ("10 09 2003", datetime(2003, 10, 9), "date with space"), | ||
| ("10 09 03", datetime(2003, 10, 9), "date with space"), | ||
| ("25 09 03", datetime(2003, 9, 25), "date with space"), | ||
| ("03 25 Sep", datetime(2003, 9, 25), "strangely ordered date"), | ||
| ("25 03 Sep", datetime(2025, 9, 3), "strangely ordered date"), | ||
| (" July 4 , 1976 12:01:02 am ", datetime(1976, 7, 4, 0, 1, 2), "extra space"), | ||
| ("Wed, July 10, '96", datetime(1996, 7, 10, 0, 0), "random format"), | ||
| ("1996.July.10 AD 12:08 PM", datetime(1996, 7, 10, 12, 8), "random format"), | ||
| ("July 4, 1976", datetime(1976, 7, 4), "random format"), | ||
| ("7 4 1976", datetime(1976, 7, 4), "random format"), | ||
| ("4 jul 1976", datetime(1976, 7, 4), "random format"), | ||
| ("7-4-76", datetime(1976, 7, 4), "random format"), | ||
| ("19760704", datetime(1976, 7, 4), "random format"), | ||
| ("0:01:02 on July 4, 1976", datetime(1976, 7, 4, 0, 1, 2), "random format"), | ||
| ("0:01:02 on July 4, 1976", datetime(1976, 7, 4, 0, 1, 2), "random format"), | ||
| ("July 4, 1976 12:01:02 am", datetime(1976, 7, 4, 0, 1, 2), "random format"), | ||
| ("Mon Jan 2 04:24:27 1995", datetime(1995, 1, 2, 4, 24, 27), "random format"), | ||
| ("04.04.95 00:22", datetime(1995, 4, 4, 0, 22), "random format"), | ||
| ("Jan 1 1999 11:23:34.578", datetime(1999, 1, 1, 11, 23, 34, 578000), "random format"), | ||
| ("950404 122212", datetime(1995, 4, 4, 12, 22, 12), "random format"), | ||
| ("3rd of May 2001", datetime(2001, 5, 3), "random format"), | ||
| ("5th of March 2001", datetime(2001, 3, 5), "random format"), | ||
| ("1st of May 2003", datetime(2003, 5, 1), "random format"), | ||
| ('0099-01-01T00:00:00', datetime(99, 1, 1, 0, 0), "99 ad"), | ||
| ('0031-01-01T00:00:00', datetime(31, 1, 1, 0, 0), "31 ad"), | ||
| ("20080227T21:26:01.123456789", datetime(2008, 2, 27, 21, 26, 1, 123456), "high precision seconds"), | ||
| ('13NOV2017', datetime(2017, 11, 13), "dBY (See GH360)"), | ||
| ('0003-03-04', datetime(3, 3, 4), "pre 12 year same month (See GH PR #293)"), | ||
| ('December.0031.30', datetime(31, 12, 30), "BYd corner case (GH#687)") | ||
| ] | ||
| @pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_TEST_CASES) | ||
| def test_parser(parsable_text, expected_datetime, assertion_message): | ||
| assert parse(parsable_text) == expected_datetime, assertion_message | ||
| # Parser test cases using datetime(2003, 9, 25) as a default. | ||
| # Format: (parsable_text, expected_datetime, assertion_message) | ||
| PARSER_DEFAULT_TEST_CASES = [ | ||
| ("Thu Sep 25 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Thu Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Thu 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("Sep 10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("10:36:28", datetime(2003, 9, 25, 10, 36, 28), "date command format strip"), | ||
| ("10:36", datetime(2003, 9, 25, 10, 36), "date command format strip"), | ||
| ("Sep 2003", datetime(2003, 9, 25), "date command format strip"), | ||
| ("Sep", datetime(2003, 9, 25), "date command format strip"), | ||
| ("2003", datetime(2003, 9, 25), "date command format strip"), | ||
| ("10h36m28.5s", datetime(2003, 9, 25, 10, 36, 28, 500000), "hour with letters"), | ||
| ("10h36m28s", datetime(2003, 9, 25, 10, 36, 28), "hour with letters strip"), | ||
| ("10h36m", datetime(2003, 9, 25, 10, 36), "hour with letters strip"), | ||
| ("10h", datetime(2003, 9, 25, 10), "hour with letters strip"), | ||
| ("10 h 36", datetime(2003, 9, 25, 10, 36), "hour with letters strip"), | ||
| ("10 h 36.5", datetime(2003, 9, 25, 10, 36, 30), "hour with letter strip"), | ||
| ("36 m 5", datetime(2003, 9, 25, 0, 36, 5), "hour with letters spaces"), | ||
| ("36 m 5 s", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"), | ||
| ("36 m 05", datetime(2003, 9, 25, 0, 36, 5), "minute with letters spaces"), | ||
| ("36 m 05 s", datetime(2003, 9, 25, 0, 36, 5), "minutes with letters spaces"), | ||
| ("10h am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10h pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00 am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00 pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00am", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00pm", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00a.m", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00p.m", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("10:00a.m.", datetime(2003, 9, 25, 10), "hour am pm"), | ||
| ("10:00p.m.", datetime(2003, 9, 25, 22), "hour am pm"), | ||
| ("Wed", datetime(2003, 10, 1), "weekday alone"), | ||
| ("Wednesday", datetime(2003, 10, 1), "long weekday"), | ||
| ("October", datetime(2003, 10, 25), "long month"), | ||
| ("31-Dec-00", datetime(2000, 12, 31), "zero year"), | ||
| ("0:01:02", datetime(2003, 9, 25, 0, 1, 2), "random format"), | ||
| ("12h 01m02s am", datetime(2003, 9, 25, 0, 1, 2), "random format"), | ||
| ("12:08 PM", datetime(2003, 9, 25, 12, 8), "random format"), | ||
| ("01h02m03", datetime(2003, 9, 25, 1, 2, 3), "random format"), | ||
| ("01h02", datetime(2003, 9, 25, 1, 2), "random format"), | ||
| ("01h02s", datetime(2003, 9, 25, 1, 0, 2), "random format"), | ||
| ("01m02", datetime(2003, 9, 25, 0, 1, 2), "random format"), | ||
| ("01m02h", datetime(2003, 9, 25, 2, 1), "random format"), | ||
| ("2004 10 Apr 11h30m", datetime(2004, 4, 10, 11, 30), "random format") | ||
| ] | ||
| @pytest.mark.parametrize("parsable_text,expected_datetime,assertion_message", PARSER_DEFAULT_TEST_CASES) | ||
| def test_parser_default(parsable_text, expected_datetime, assertion_message): | ||
| assert parse(parsable_text, default=datetime(2003, 9, 25)) == expected_datetime, assertion_message | ||
| class TestFormat(unittest.TestCase): | ||
@@ -61,26 +185,16 @@ | ||
| class ParserTest(unittest.TestCase): | ||
| def setUp(self): | ||
| self.tzinfos = {"BRST": -10800} | ||
| self.brsttz = tzoffset("BRST", -10800) | ||
| self.default = datetime(2003, 9, 25) | ||
| # Parser should be able to handle bytestring and unicode | ||
| self.uni_str = '2014-05-01 08:00:00' | ||
| self.str_str = self.uni_str.encode() | ||
| def testEmptyString(self): | ||
| with self.assertRaises(ValueError): | ||
| class TestInputFormats(object): | ||
| def test_empty_string_invalid(self): | ||
| with pytest.raises(ValueError): | ||
| parse('') | ||
| def testNone(self): | ||
| with self.assertRaises(TypeError): | ||
| def test_none_invalid(self): | ||
| with pytest.raises(TypeError): | ||
| parse(None) | ||
| def testInvalidType(self): | ||
| with self.assertRaises(TypeError): | ||
| def test_int_invalid(self): | ||
| with pytest.raises(TypeError): | ||
| parse(13) | ||
| def testDuckTyping(self): | ||
| def test_duck_typing(self): | ||
| # We want to support arbitrary classes that implement the stream | ||
@@ -98,21 +212,45 @@ # interface. | ||
| self.assertEqual(parse(dstr), datetime(2014, 1, 19)) | ||
| res = parse(dstr) | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| def testParseStream(self): | ||
| def test_parse_stream(self): | ||
| dstr = StringIO('2014 January 19') | ||
| self.assertEqual(parse(dstr), datetime(2014, 1, 19)) | ||
| res = parse(dstr) | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| def testParseStr(self): | ||
| self.assertEqual(parse(self.str_str), | ||
| parse(self.uni_str)) | ||
| def test_parse_str(self): | ||
| # Parser should be able to handle bytestring and unicode | ||
| uni_str = '2014-05-01 08:00:00' | ||
| bytes_str = uni_str.encode() | ||
| def testParseBytes(self): | ||
| self.assertEqual(parse(b'2014 January 19'), datetime(2014, 1, 19)) | ||
| res = parse(bytes_str) | ||
| expected = parse(uni_str) | ||
| assert res == expected | ||
| def testParseBytearray(self): | ||
| # GH #417 | ||
| self.assertEqual(parse(bytearray(b'2014 January 19')), | ||
| datetime(2014, 1, 19)) | ||
| def test_parse_bytes(self): | ||
| res = parse(b'2014 January 19') | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| def test_parse_bytearray(self): | ||
| # GH#417 | ||
| res = parse(bytearray(b'2014 January 19')) | ||
| expected = datetime(2014, 1, 19) | ||
| assert res == expected | ||
| class ParserTest(unittest.TestCase): | ||
| def setUp(self): | ||
| self.tzinfos = {"BRST": -10800} | ||
| self.brsttz = tzoffset("BRST", -10800) | ||
| self.default = datetime(2003, 9, 25) | ||
| # Parser should be able to handle bytestring and unicode | ||
| self.uni_str = '2014-05-01 08:00:00' | ||
| self.str_str = self.uni_str.encode() | ||
| def testParserParseStr(self): | ||
@@ -177,2 +315,3 @@ from dateutil.parser import parser | ||
| tzinfo=self.brsttz)) | ||
| def testDateCommandFormatIgnoreTz(self): | ||
@@ -183,46 +322,2 @@ self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", | ||
| def testDateCommandFormatStrip1(self): | ||
| self.assertEqual(parse("Thu Sep 25 10:36:28 2003"), | ||
| datetime(2003, 9, 25, 10, 36, 28)) | ||
| def testDateCommandFormatStrip2(self): | ||
| self.assertEqual(parse("Thu Sep 25 10:36:28", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36, 28)) | ||
| def testDateCommandFormatStrip3(self): | ||
| self.assertEqual(parse("Thu Sep 10:36:28", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36, 28)) | ||
| def testDateCommandFormatStrip4(self): | ||
| self.assertEqual(parse("Thu 10:36:28", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36, 28)) | ||
| def testDateCommandFormatStrip5(self): | ||
| self.assertEqual(parse("Sep 10:36:28", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36, 28)) | ||
| def testDateCommandFormatStrip6(self): | ||
| self.assertEqual(parse("10:36:28", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36, 28)) | ||
| def testDateCommandFormatStrip7(self): | ||
| self.assertEqual(parse("10:36", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36)) | ||
| def testDateCommandFormatStrip8(self): | ||
| self.assertEqual(parse("Thu Sep 25 2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateCommandFormatStrip10(self): | ||
| self.assertEqual(parse("Sep 2003", default=self.default), | ||
| datetime(2003, 9, 25)) | ||
| def testDateCommandFormatStrip11(self): | ||
| self.assertEqual(parse("Sep", default=self.default), | ||
| datetime(2003, 9, 25)) | ||
| def testDateCommandFormatStrip12(self): | ||
| self.assertEqual(parse("2003", default=self.default), | ||
| datetime(2003, 9, 25)) | ||
| def testDateRCommandFormat(self): | ||
@@ -244,17 +339,6 @@ self.assertEqual(parse("Thu, 25 Sep 2003 10:49:41 -0300"), | ||
| def testISOFormatStrip2(self): | ||
| self.assertEqual(parse("2003-09-25T10:49:41"), | ||
| datetime(2003, 9, 25, 10, 49, 41)) | ||
| self.assertEqual(parse("2003-09-25T10:49:41+03:00"), | ||
| datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=tzoffset(None, 10800))) | ||
| def testISOFormatStrip3(self): | ||
| self.assertEqual(parse("2003-09-25T10:49"), | ||
| datetime(2003, 9, 25, 10, 49)) | ||
| def testISOFormatStrip4(self): | ||
| self.assertEqual(parse("2003-09-25T10"), | ||
| datetime(2003, 9, 25, 10)) | ||
| def testISOFormatStrip5(self): | ||
| self.assertEqual(parse("2003-09-25"), | ||
| datetime(2003, 9, 25)) | ||
| def testISOStrippedFormat(self): | ||
@@ -271,41 +355,6 @@ self.assertEqual(parse("20030925T104941.5-0300"), | ||
| def testISOStrippedFormatStrip2(self): | ||
| self.assertEqual(parse("20030925T104941"), | ||
| datetime(2003, 9, 25, 10, 49, 41)) | ||
| self.assertEqual(parse("20030925T104941+0300"), | ||
| datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=tzoffset(None, 10800))) | ||
| def testISOStrippedFormatStrip3(self): | ||
| self.assertEqual(parse("20030925T1049"), | ||
| datetime(2003, 9, 25, 10, 49, 0)) | ||
| def testISOStrippedFormatStrip4(self): | ||
| self.assertEqual(parse("20030925T10"), | ||
| datetime(2003, 9, 25, 10)) | ||
| def testISOStrippedFormatStrip5(self): | ||
| self.assertEqual(parse("20030925"), | ||
| datetime(2003, 9, 25)) | ||
| def testPythonLoggerFormat(self): | ||
| self.assertEqual(parse("2003-09-25 10:49:41,502"), | ||
| datetime(2003, 9, 25, 10, 49, 41, 502000)) | ||
| def testNoSeparator1(self): | ||
| self.assertEqual(parse("199709020908"), | ||
| datetime(1997, 9, 2, 9, 8)) | ||
| def testNoSeparator2(self): | ||
| self.assertEqual(parse("19970902090807"), | ||
| datetime(1997, 9, 2, 9, 8, 7)) | ||
| def testDateWithDash1(self): | ||
| self.assertEqual(parse("2003-09-25"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDash6(self): | ||
| self.assertEqual(parse("09-25-2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDash7(self): | ||
| self.assertEqual(parse("25-09-2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDash8(self): | ||
@@ -315,10 +364,2 @@ self.assertEqual(parse("10-09-2003", dayfirst=True), | ||
| def testDateWithDash9(self): | ||
| self.assertEqual(parse("10-09-2003"), | ||
| datetime(2003, 10, 9)) | ||
| def testDateWithDash10(self): | ||
| self.assertEqual(parse("10-09-03"), | ||
| datetime(2003, 10, 9)) | ||
| def testDateWithDash11(self): | ||
@@ -328,14 +369,2 @@ self.assertEqual(parse("10-09-03", yearfirst=True), | ||
| def testDateWithDot1(self): | ||
| self.assertEqual(parse("2003.09.25"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDot6(self): | ||
| self.assertEqual(parse("09.25.2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDot7(self): | ||
| self.assertEqual(parse("25.09.2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDot8(self): | ||
@@ -345,10 +374,2 @@ self.assertEqual(parse("10.09.2003", dayfirst=True), | ||
| def testDateWithDot9(self): | ||
| self.assertEqual(parse("10.09.2003"), | ||
| datetime(2003, 10, 9)) | ||
| def testDateWithDot10(self): | ||
| self.assertEqual(parse("10.09.03"), | ||
| datetime(2003, 10, 9)) | ||
| def testDateWithDot11(self): | ||
@@ -358,14 +379,2 @@ self.assertEqual(parse("10.09.03", yearfirst=True), | ||
| def testDateWithSlash1(self): | ||
| self.assertEqual(parse("2003/09/25"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSlash6(self): | ||
| self.assertEqual(parse("09/25/2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSlash7(self): | ||
| self.assertEqual(parse("25/09/2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSlash8(self): | ||
@@ -375,10 +384,2 @@ self.assertEqual(parse("10/09/2003", dayfirst=True), | ||
| def testDateWithSlash9(self): | ||
| self.assertEqual(parse("10/09/2003"), | ||
| datetime(2003, 10, 9)) | ||
| def testDateWithSlash10(self): | ||
| self.assertEqual(parse("10/09/03"), | ||
| datetime(2003, 10, 9)) | ||
| def testDateWithSlash11(self): | ||
@@ -388,14 +389,2 @@ self.assertEqual(parse("10/09/03", yearfirst=True), | ||
| def testDateWithSpace1(self): | ||
| self.assertEqual(parse("2003 09 25"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSpace6(self): | ||
| self.assertEqual(parse("09 25 2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSpace7(self): | ||
| self.assertEqual(parse("25 09 2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSpace8(self): | ||
@@ -405,10 +394,2 @@ self.assertEqual(parse("10 09 2003", dayfirst=True), | ||
| def testDateWithSpace9(self): | ||
| self.assertEqual(parse("10 09 2003"), | ||
| datetime(2003, 10, 9)) | ||
| def testDateWithSpace10(self): | ||
| self.assertEqual(parse("10 09 03"), | ||
| datetime(2003, 10, 9)) | ||
| def testDateWithSpace11(self): | ||
@@ -418,54 +399,2 @@ self.assertEqual(parse("10 09 03", yearfirst=True), | ||
| def testDateWithSpace12(self): | ||
| self.assertEqual(parse("25 09 03"), | ||
| datetime(2003, 9, 25)) | ||
| def testStrangelyOrderedDate1(self): | ||
| self.assertEqual(parse("03 25 Sep"), | ||
| datetime(2003, 9, 25)) | ||
| def testStrangelyOrderedDate3(self): | ||
| self.assertEqual(parse("25 03 Sep"), | ||
| datetime(2025, 9, 3)) | ||
| def testHourWithLetters(self): | ||
| self.assertEqual(parse("10h36m28.5s", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36, 28, 500000)) | ||
| def testHourWithLettersStrip1(self): | ||
| self.assertEqual(parse("10h36m28s", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36, 28)) | ||
| def testHourWithLettersStrip2(self): | ||
| self.assertEqual(parse("10h36m", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36)) | ||
| def testHourWithLettersStrip3(self): | ||
| self.assertEqual(parse("10h", default=self.default), | ||
| datetime(2003, 9, 25, 10)) | ||
| def testHourWithLettersStrip4(self): | ||
| self.assertEqual(parse("10 h 36", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36)) | ||
| def testHourWithLetterStrip5(self): | ||
| self.assertEqual(parse("10 h 36.5", default=self.default), | ||
| datetime(2003, 9, 25, 10, 36, 30)) | ||
| def testMinuteWithLettersSpaces1(self): | ||
| self.assertEqual(parse("36 m 5", default=self.default), | ||
| datetime(2003, 9, 25, 0, 36, 5)) | ||
| def testMinuteWithLettersSpaces2(self): | ||
| self.assertEqual(parse("36 m 5 s", default=self.default), | ||
| datetime(2003, 9, 25, 0, 36, 5)) | ||
| def testMinuteWithLettersSpaces3(self): | ||
| self.assertEqual(parse("36 m 05", default=self.default), | ||
| datetime(2003, 9, 25, 0, 36, 5)) | ||
| def testMinuteWithLettersSpaces4(self): | ||
| self.assertEqual(parse("36 m 05 s", default=self.default), | ||
| datetime(2003, 9, 25, 0, 36, 5)) | ||
| def testAMPMNoHour(self): | ||
@@ -478,50 +407,2 @@ with self.assertRaises(ValueError): | ||
| def testHourAmPm1(self): | ||
| self.assertEqual(parse("10h am", default=self.default), | ||
| datetime(2003, 9, 25, 10)) | ||
| def testHourAmPm2(self): | ||
| self.assertEqual(parse("10h pm", default=self.default), | ||
| datetime(2003, 9, 25, 22)) | ||
| def testHourAmPm3(self): | ||
| self.assertEqual(parse("10am", default=self.default), | ||
| datetime(2003, 9, 25, 10)) | ||
| def testHourAmPm4(self): | ||
| self.assertEqual(parse("10pm", default=self.default), | ||
| datetime(2003, 9, 25, 22)) | ||
| def testHourAmPm5(self): | ||
| self.assertEqual(parse("10:00 am", default=self.default), | ||
| datetime(2003, 9, 25, 10)) | ||
| def testHourAmPm6(self): | ||
| self.assertEqual(parse("10:00 pm", default=self.default), | ||
| datetime(2003, 9, 25, 22)) | ||
| def testHourAmPm7(self): | ||
| self.assertEqual(parse("10:00am", default=self.default), | ||
| datetime(2003, 9, 25, 10)) | ||
| def testHourAmPm8(self): | ||
| self.assertEqual(parse("10:00pm", default=self.default), | ||
| datetime(2003, 9, 25, 22)) | ||
| def testHourAmPm9(self): | ||
| self.assertEqual(parse("10:00a.m", default=self.default), | ||
| datetime(2003, 9, 25, 10)) | ||
| def testHourAmPm10(self): | ||
| self.assertEqual(parse("10:00p.m", default=self.default), | ||
| datetime(2003, 9, 25, 22)) | ||
| def testHourAmPm11(self): | ||
| self.assertEqual(parse("10:00a.m.", default=self.default), | ||
| datetime(2003, 9, 25, 10)) | ||
| def testHourAmPm12(self): | ||
| self.assertEqual(parse("10:00p.m.", default=self.default), | ||
| datetime(2003, 9, 25, 22)) | ||
| def testAMPMRange(self): | ||
@@ -540,18 +421,2 @@ with self.assertRaises(ValueError): | ||
| def testWeekdayAlone(self): | ||
| self.assertEqual(parse("Wed", default=self.default), | ||
| datetime(2003, 10, 1)) | ||
| def testLongWeekday(self): | ||
| self.assertEqual(parse("Wednesday", default=self.default), | ||
| datetime(2003, 10, 1)) | ||
| def testLongMonth(self): | ||
| self.assertEqual(parse("October", default=self.default), | ||
| datetime(2003, 10, 25)) | ||
| def testZeroYear(self): | ||
| self.assertEqual(parse("31-Dec-00", default=self.default), | ||
| datetime(2000, 12, 31)) | ||
| def testFuzzy(self): | ||
@@ -599,10 +464,2 @@ s = "Today is 25 of September of 2003, exactly " \ | ||
| def testExtraSpace(self): | ||
| self.assertEqual(parse(" July 4 , 1976 12:01:02 am "), | ||
| datetime(1976, 7, 4, 0, 1, 2)) | ||
| def testRandomFormat1(self): | ||
| self.assertEqual(parse("Wed, July 10, '96"), | ||
| datetime(1996, 7, 10, 0, 0)) | ||
| def testRandomFormat2(self): | ||
@@ -613,6 +470,2 @@ self.assertEqual(parse("1996.07.10 AD at 15:08:56 PDT", | ||
| def testRandomFormat3(self): | ||
| self.assertEqual(parse("1996.July.10 AD 12:08 PM"), | ||
| datetime(1996, 7, 10, 12, 8)) | ||
| def testRandomFormat4(self): | ||
@@ -638,33 +491,2 @@ self.assertEqual(parse("Tuesday, April 12, 1952 AD 3:30:42pm PST", | ||
| def testRandomFormat8(self): | ||
| self.assertEqual(parse("July 4, 1976"), datetime(1976, 7, 4)) | ||
| def testRandomFormat9(self): | ||
| self.assertEqual(parse("7 4 1976"), datetime(1976, 7, 4)) | ||
| def testRandomFormat10(self): | ||
| self.assertEqual(parse("4 jul 1976"), datetime(1976, 7, 4)) | ||
| def testRandomFormat11(self): | ||
| self.assertEqual(parse("7-4-76"), datetime(1976, 7, 4)) | ||
| def testRandomFormat12(self): | ||
| self.assertEqual(parse("19760704"), datetime(1976, 7, 4)) | ||
| def testRandomFormat13(self): | ||
| self.assertEqual(parse("0:01:02", default=self.default), | ||
| datetime(2003, 9, 25, 0, 1, 2)) | ||
| def testRandomFormat14(self): | ||
| self.assertEqual(parse("12h 01m02s am", default=self.default), | ||
| datetime(2003, 9, 25, 0, 1, 2)) | ||
| def testRandomFormat15(self): | ||
| self.assertEqual(parse("0:01:02 on July 4, 1976"), | ||
| datetime(1976, 7, 4, 0, 1, 2)) | ||
| def testRandomFormat16(self): | ||
| self.assertEqual(parse("0:01:02 on July 4, 1976"), | ||
| datetime(1976, 7, 4, 0, 1, 2)) | ||
| def testRandomFormat17(self): | ||
@@ -675,9 +497,6 @@ self.assertEqual(parse("1976-07-04T00:01:02Z", ignoretz=True), | ||
| def testRandomFormat18(self): | ||
| self.assertEqual(parse("July 4, 1976 12:01:02 am"), | ||
| datetime(1976, 7, 4, 0, 1, 2)) | ||
| self.assertEqual(parse("1986-07-05T08:15:30z", | ||
| ignoretz=True), | ||
| datetime(1986, 7, 5, 8, 15, 30)) | ||
| def testRandomFormat19(self): | ||
| self.assertEqual(parse("Mon Jan 2 04:24:27 1995"), | ||
| datetime(1995, 1, 2, 4, 24, 27)) | ||
| def testRandomFormat20(self): | ||
@@ -687,14 +506,2 @@ self.assertEqual(parse("Tue Apr 4 00:22:12 PDT 1995", ignoretz=True), | ||
| def testRandomFormat21(self): | ||
| self.assertEqual(parse("04.04.95 00:22"), | ||
| datetime(1995, 4, 4, 0, 22)) | ||
| def testRandomFormat22(self): | ||
| self.assertEqual(parse("Jan 1 1999 11:23:34.578"), | ||
| datetime(1999, 1, 1, 11, 23, 34, 578000)) | ||
| def testRandomFormat23(self): | ||
| self.assertEqual(parse("950404 122212"), | ||
| datetime(1995, 4, 4, 12, 22, 12)) | ||
| def testRandomFormat24(self): | ||
@@ -705,6 +512,2 @@ self.assertEqual(parse("0:00 PM, PST", default=self.default, | ||
| def testRandomFormat25(self): | ||
| self.assertEqual(parse("12:08 PM", default=self.default), | ||
| datetime(2003, 9, 25, 12, 8)) | ||
| def testRandomFormat26(self): | ||
@@ -716,43 +519,2 @@ with pytest.warns(UnknownTimezoneWarning): | ||
| def testRandomFormat27(self): | ||
| self.assertEqual(parse("3rd of May 2001"), datetime(2001, 5, 3)) | ||
| def testRandomFormat28(self): | ||
| self.assertEqual(parse("5th of March 2001"), datetime(2001, 3, 5)) | ||
| def testRandomFormat29(self): | ||
| self.assertEqual(parse("1st of May 2003"), datetime(2003, 5, 1)) | ||
| def testRandomFormat30(self): | ||
| self.assertEqual(parse("01h02m03", default=self.default), | ||
| datetime(2003, 9, 25, 1, 2, 3)) | ||
| def testRandomFormat31(self): | ||
| self.assertEqual(parse("01h02", default=self.default), | ||
| datetime(2003, 9, 25, 1, 2)) | ||
| def testRandomFormat32(self): | ||
| self.assertEqual(parse("01h02s", default=self.default), | ||
| datetime(2003, 9, 25, 1, 0, 2)) | ||
| def testRandomFormat33(self): | ||
| self.assertEqual(parse("01m02", default=self.default), | ||
| datetime(2003, 9, 25, 0, 1, 2)) | ||
| def testRandomFormat34(self): | ||
| self.assertEqual(parse("01m02h", default=self.default), | ||
| datetime(2003, 9, 25, 2, 1)) | ||
| def testRandomFormat35(self): | ||
| self.assertEqual(parse("2004 10 Apr 11h30m", default=self.default), | ||
| datetime(2004, 4, 10, 11, 30)) | ||
| def test_99_ad(self): | ||
| self.assertEqual(parse('0099-01-01T00:00:00'), | ||
| datetime(99, 1, 1, 0, 0)) | ||
| def test_31_ad(self): | ||
| self.assertEqual(parse('0031-01-01T00:00:00'), | ||
| datetime(31, 1, 1, 0, 0)) | ||
| def testInvalidDay(self): | ||
@@ -830,6 +592,2 @@ with self.assertRaises(ValueError): | ||
| def testHighPrecisionSeconds(self): | ||
| self.assertEqual(parse("20080227T21:26:01.123456789"), | ||
| datetime(2008, 2, 27, 21, 26, 1, 123456)) | ||
| def testCustomParserInfo(self): | ||
@@ -929,8 +687,2 @@ # Custom parser info wasn't working, as Michael Elsdörfer discovered. | ||
| def test_dBY(self): | ||
| # See GH360 | ||
| dtstr = '13NOV2017' | ||
| res = parse(dtstr) | ||
| self.assertEqual(res, datetime(2017, 11, 13)) | ||
| def test_hmBY(self): | ||
@@ -953,8 +705,3 @@ # See GH#483 | ||
| def test_pre_12_year_same_month(self): | ||
| # See GH PR #293 | ||
| dtstr = '0003-03-04' | ||
| assert parse(dtstr) == datetime(3, 3, 4) | ||
| class TestParseUnimplementedCases(object): | ||
@@ -1067,2 +814,3 @@ @pytest.mark.xfail | ||
| @pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var') | ||
@@ -1138,7 +886,1 @@ def test_parse_unambiguous_nonexistent_local(): | ||
| parse(value) | ||
| def test_BYd_corner_case(): | ||
| # GH#687 | ||
| res = parse('December.0031.30') | ||
| assert res == datetime(31, 12, 30) |
@@ -128,2 +128,10 @@ # -*- coding: utf-8 -*- | ||
| def testAddMoreThan12Months(self): | ||
| self.assertEqual(date(2003, 12, 1) + relativedelta(months=+13), | ||
| date(2005, 1, 1)) | ||
| def testAddNegativeMonths(self): | ||
| self.assertEqual(date(2003, 1, 1) + relativedelta(months=-2), | ||
| date(2002, 11, 1)) | ||
| def test15thISOYearWeek(self): | ||
@@ -354,2 +362,12 @@ self.assertEqual(date(2003, 1, 1) + | ||
| def testRelativeDeltaInvalidDatetimeObject(self): | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(dt1='2018-01-01', dt2='2018-01-02') | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(dt1=datetime(2018, 1, 1), dt2='2018-01-02') | ||
| with self.assertRaises(TypeError): | ||
| relativedelta(dt1='2018-01-01', dt2=datetime(2018, 1, 2)) | ||
| def testRelativeDeltaFractionalAbsolutes(self): | ||
@@ -356,0 +374,0 @@ # Fractional absolute values will soon be unsupported, |
@@ -1,2 +0,2 @@ | ||
| from six import PY3 | ||
| from six import PY2 | ||
@@ -19,10 +19,14 @@ from functools import wraps | ||
| """ | ||
| def adjust_encoding(*args, **kwargs): | ||
| name = namefunc(*args, **kwargs) | ||
| if name is not None and not PY3: | ||
| name = name.encode() | ||
| if PY2: | ||
| @wraps(namefunc) | ||
| def adjust_encoding(*args, **kwargs): | ||
| name = namefunc(*args, **kwargs) | ||
| if name is not None: | ||
| name = name.encode() | ||
| return name | ||
| return name | ||
| return adjust_encoding | ||
| return adjust_encoding | ||
| else: | ||
| return namefunc | ||
@@ -29,0 +33,0 @@ |
| from datetime import timedelta | ||
| import weakref | ||
| from collections import OrderedDict | ||
@@ -14,2 +16,3 @@ | ||
| class _TzFactory(type): | ||
@@ -23,3 +26,5 @@ def instance(cls, *args, **kwargs): | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instances = {} | ||
| cls.__instances = weakref.WeakValueDictionary() | ||
| cls.__strong_cache = OrderedDict() | ||
| cls.__strong_cache_size = 8 | ||
@@ -36,2 +41,10 @@ def __call__(cls, name, offset): | ||
| cls.instance(name, offset)) | ||
| cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) | ||
| # Remove an item if the strong cache is overpopulated | ||
| # TODO: Maybe this should be under a lock? | ||
| if len(cls.__strong_cache) > cls.__strong_cache_size: | ||
| cls.__strong_cache.popitem(last=False) | ||
| return instance | ||
@@ -42,3 +55,5 @@ | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instances = {} | ||
| cls.__instances = weakref.WeakValueDictionary() | ||
| cls.__strong_cache = OrderedDict() | ||
| cls.__strong_cache_size = 8 | ||
@@ -52,3 +67,12 @@ def __call__(cls, s, posix_offset=False): | ||
| cls.instance(s, posix_offset)) | ||
| cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) | ||
| # Remove an item if the strong cache is overpopulated | ||
| # TODO: Maybe this should be under a lock? | ||
| if len(cls.__strong_cache) > cls.__strong_cache_size: | ||
| cls.__strong_cache.popitem(last=False) | ||
| return instance | ||
+97
-46
@@ -16,2 +16,4 @@ # -*- coding: utf-8 -*- | ||
| import bisect | ||
| import weakref | ||
| from collections import OrderedDict | ||
@@ -32,2 +34,5 @@ import six | ||
| # For warning about rounding tzinfo | ||
| from warnings import warn | ||
| ZERO = datetime.timedelta(0) | ||
@@ -142,4 +147,5 @@ EPOCH = datetime.datetime.utcfromtimestamp(0) | ||
| pass | ||
| self._offset = datetime.timedelta(seconds=offset) | ||
| self._offset = datetime.timedelta(seconds=_get_supported_offset(offset)) | ||
| def utcoffset(self, dt): | ||
@@ -466,3 +472,3 @@ return self._offset | ||
| if not file_opened_here: | ||
| fileobj = _ContextWrapper(fileobj) | ||
| fileobj = _nullcontext(fileobj) | ||
@@ -607,6 +613,3 @@ with fileobj as file_stream: | ||
| gmtoff, isdst, abbrind = ttinfo[i] | ||
| # Round to full-minutes if that's not the case. Python's | ||
| # datetime doesn't accept sub-minute timezones. Check | ||
| # http://python.org/sf/1447945 for some information. | ||
| gmtoff = 60 * ((gmtoff + 30) // 60) | ||
| gmtoff = _get_supported_offset(gmtoff) | ||
| tti = _ttinfo() | ||
@@ -663,34 +666,41 @@ tti.offset = gmtoff | ||
| # about this. | ||
| laststdoffset = None | ||
| lastdst = None | ||
| lastoffset = None | ||
| lastdstoffset = None | ||
| lastbaseoffset = None | ||
| out.trans_list = [] | ||
| for i, tti in enumerate(out.trans_idx): | ||
| if not tti.isdst: | ||
| offset = tti.offset | ||
| laststdoffset = offset | ||
| else: | ||
| if laststdoffset is not None: | ||
| # Store the DST offset as well and update it in the list | ||
| tti.dstoffset = tti.offset - laststdoffset | ||
| out.trans_idx[i] = tti | ||
| offset = tti.offset | ||
| dstoffset = 0 | ||
| offset = laststdoffset or 0 | ||
| if lastdst is not None: | ||
| if tti.isdst: | ||
| if not lastdst: | ||
| dstoffset = offset - lastoffset | ||
| out.trans_list.append(out.trans_list_utc[i] + offset) | ||
| if not dstoffset and lastdstoffset: | ||
| dstoffset = lastdstoffset | ||
| # In case we missed any DST offsets on the way in for some reason, make | ||
| # a second pass over the list, looking for the /next/ DST offset. | ||
| laststdoffset = None | ||
| for i in reversed(range(len(out.trans_idx))): | ||
| tti = out.trans_idx[i] | ||
| if tti.isdst: | ||
| if not (tti.dstoffset or laststdoffset is None): | ||
| tti.dstoffset = tti.offset - laststdoffset | ||
| else: | ||
| laststdoffset = tti.offset | ||
| tti.dstoffset = datetime.timedelta(seconds=dstoffset) | ||
| lastdstoffset = dstoffset | ||
| if not isinstance(tti.dstoffset, datetime.timedelta): | ||
| tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset) | ||
| # If a time zone changes its base offset during a DST transition, | ||
| # then you need to adjust by the previous base offset to get the | ||
| # transition time in local time. Otherwise you use the current | ||
| # base offset. Ideally, I would have some mathematical proof of | ||
| # why this is true, but I haven't really thought about it enough. | ||
| baseoffset = offset - dstoffset | ||
| adjustment = baseoffset | ||
| if (lastbaseoffset is not None and baseoffset != lastbaseoffset | ||
| and tti.isdst != lastdst): | ||
| # The base DST has changed | ||
| adjustment = lastbaseoffset | ||
| out.trans_idx[i] = tti | ||
| lastdst = tti.isdst | ||
| lastoffset = offset | ||
| lastbaseoffset = baseoffset | ||
| out.trans_list.append(out.trans_list_utc[i] + adjustment) | ||
| out.trans_idx = tuple(out.trans_idx) | ||
@@ -1264,3 +1274,3 @@ out.trans_list = tuple(out.trans_list) | ||
| self._s = getattr(fileobj, 'name', repr(fileobj)) | ||
| fileobj = _ContextWrapper(fileobj) | ||
| fileobj = _nullcontext(fileobj) | ||
@@ -1538,3 +1548,5 @@ self._vtz = {} | ||
| self.__instances = {} | ||
| self.__instances = weakref.WeakValueDictionary() | ||
| self.__strong_cache_size = 8 | ||
| self.__strong_cache = OrderedDict() | ||
| self._cache_lock = _thread.allocate_lock() | ||
@@ -1548,13 +1560,33 @@ | ||
| rv = self.nocache(name=name) | ||
| if not (name is None or isinstance(rv, tzlocal_classes)): | ||
| if not (name is None | ||
| or isinstance(rv, tzlocal_classes) | ||
| or rv is None): | ||
| # tzlocal is slightly more complicated than the other | ||
| # time zone providers because it depends on environment | ||
| # at construction time, so don't cache that. | ||
| # | ||
| # We also cannot store weak references to None, so we | ||
| # will also not store that. | ||
| self.__instances[name] = rv | ||
| else: | ||
| # No need for strong caching, return immediately | ||
| return rv | ||
| self.__strong_cache[name] = self.__strong_cache.pop(name, rv) | ||
| if len(self.__strong_cache) > self.__strong_cache_size: | ||
| self.__strong_cache.popitem(last=False) | ||
| return rv | ||
| def set_cache_size(self, size): | ||
| with self._cache_lock: | ||
| self.__strong_cache_size = size | ||
| while len(self.__strong_cache) > size: | ||
| self.__strong_cache.popitem(last=False) | ||
| def cache_clear(self): | ||
| with self._cache_lock: | ||
| self.__instances = {} | ||
| self.__instances = weakref.WeakValueDictionary() | ||
| self.__strong_cache.clear() | ||
@@ -1613,3 +1645,4 @@ @staticmethod | ||
| tz = tzwin(name) | ||
| except WindowsError: | ||
| except (WindowsError, UnicodeEncodeError): | ||
| # UnicodeEncodeError is for Python 2.7 compat | ||
| tz = None | ||
@@ -1781,16 +1814,34 @@ | ||
| class _ContextWrapper(object): | ||
| """ | ||
| Class for wrapping contexts so that they are passed through in a | ||
| with statement. | ||
| """ | ||
| def __init__(self, context): | ||
| self.context = context | ||
| if sys.version_info >= (3, 6): | ||
| def _get_supported_offset(second_offset): | ||
| return second_offset | ||
| else: | ||
| def _get_supported_offset(second_offset): | ||
| # For python pre-3.6, round to full-minutes if that's not the case. | ||
| # Python's datetime doesn't accept sub-minute timezones. Check | ||
| # http://python.org/sf/1447945 or https://bugs.python.org/issue5288 | ||
| # for some information. | ||
| old_offset = second_offset | ||
| calculated_offset = 60 * ((second_offset + 30) // 60) | ||
| return calculated_offset | ||
| def __enter__(self): | ||
| return self.context | ||
| def __exit__(*args, **kwargs): | ||
| pass | ||
| try: | ||
| # Python 3.7 feature | ||
| from contextmanager import nullcontext as _nullcontext | ||
| except ImportError: | ||
| class _nullcontext(object): | ||
| """ | ||
| Class for wrapping contexts so that they are passed through in a | ||
| with statement. | ||
| """ | ||
| def __init__(self, context): | ||
| self.context = context | ||
| def __enter__(self): | ||
| return self.context | ||
| def __exit__(*args, **kwargs): | ||
| pass | ||
| # vim:ts=4:sw=4:et |
+42
-3
@@ -0,1 +1,9 @@ | ||
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module provides an interface to the native time zone data on Windows, | ||
| including :py:class:`datetime.tzinfo` implementations. | ||
| Attempting to import this module on a non-Windows platform will raise an | ||
| :py:obj:`ImportError`. | ||
| """ | ||
| # This code was originally contributed by Jeffrey Harris. | ||
@@ -42,3 +50,3 @@ import datetime | ||
| """ | ||
| Class for accessing `tzres.dll`, which contains timezone name related | ||
| Class for accessing ``tzres.dll``, which contains timezone name related | ||
| resources. | ||
@@ -76,5 +84,6 @@ | ||
| ..note: | ||
| .. note:: | ||
| Offsets found in the registry are generally of the form | ||
| `@tzres.dll,-114`. The offset in this case if 114, not -114. | ||
| ``@tzres.dll,-114``. The offset in this case is 114, not -114. | ||
@@ -151,2 +160,5 @@ """ | ||
| def display(self): | ||
| """ | ||
| Return the display name of the time zone. | ||
| """ | ||
| return self._display | ||
@@ -194,3 +206,14 @@ | ||
| class tzwin(tzwinbase): | ||
| """ | ||
| Time zone object created from the zone info in the Windows registry | ||
| These are similar to :py:class:`dateutil.tz.tzrange` objects in that | ||
| the time zone data is provided in the format of a single offset rule | ||
| for either 0 or 2 time zone transitions per year. | ||
| :param: name | ||
| The name of a Windows time zone key, e.g. "Eastern Standard Time". | ||
| The full list of keys can be retrieved with :func:`tzwin.list`. | ||
| """ | ||
| def __init__(self, name): | ||
@@ -241,2 +264,18 @@ self._name = name | ||
| class tzwinlocal(tzwinbase): | ||
| """ | ||
| Class representing the local time zone information in the Windows registry | ||
| While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time` | ||
| module) to retrieve time zone information, ``tzwinlocal`` retrieves the | ||
| rules directly from the Windows registry and creates an object like | ||
| :class:`dateutil.tz.tzwin`. | ||
| Because Windows does not have an equivalent of :func:`time.tzset`, on | ||
| Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the | ||
| time zone settings *at the time that the process was started*, meaning | ||
| changes to the machine's time zone settings during the run of a program | ||
| on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`. | ||
| Because ``tzwinlocal`` reads the registry directly, it is unaffected by | ||
| this issue. | ||
| """ | ||
| def __init__(self): | ||
@@ -243,0 +282,0 @@ with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: |
+14
-0
@@ -189,3 +189,17 @@ #!/usr/bin/env python3 | ||
| # -- Options for autodoc ------------------------------------------------- | ||
| autodoc_mock_imports = ['ctypes.wintypes', 'six.moves.winreg'] | ||
| # Need to mock this out specifically to avoid errors | ||
| import ctypes | ||
| def pointer_mock(*args, **kwargs): | ||
| try: | ||
| return ctypes.POINTER(*args, **kwargs) | ||
| except Exception: | ||
| return None | ||
| ctypes.POINTER = pointer_mock | ||
| sys.modules['ctypes'] = ctypes | ||
| # -- Options for LaTeX output --------------------------------------------- | ||
@@ -192,0 +206,0 @@ |
+13
-0
@@ -1193,2 +1193,15 @@ dateutil examples | ||
| Override parserinfo with a custom parserinfo | ||
| .. doctest:: tz | ||
| >>> from dateutil.parser import parse, parserinfo | ||
| >>> class CustomParserInfo(parserinfo): | ||
| ... # e.g. edit a property of parserinfo to allow a custom 12 hour format | ||
| ... AMPM = [("am", "a", "xm"), ("pm", "p")] | ||
| >>> parse('2018-06-08 12:06:58 XM', parserinfo=CustomParserInfo()) | ||
| datetime.datetime(2018, 6, 8, 0, 6, 58) | ||
| tzutc examples | ||
@@ -1195,0 +1208,0 @@ -------------- |
+3
-1
@@ -18,2 +18,4 @@ .. dateutil documentation master file, created by | ||
| Changelog <changelog> | ||
| Examples <examples> | ||
| Exercises <exercises/index> | ||
@@ -28,5 +30,5 @@ .. toctree:: | ||
| tz | ||
| tz.win <tzwin> | ||
| utils | ||
| zoneinfo | ||
| examples | ||
@@ -33,0 +35,0 @@ Indices and tables |
@@ -1,2 +0,3 @@ | ||
| Sphinx>=1.7.3,<2 | ||
| Sphinx>=1.7.3,!=1.8.0 | ||
| sphinx_rtd_theme>=0.3.0 | ||
| readme-renderer>=21.0 |
+12
-1
@@ -6,5 +6,16 @@ ===== | ||
| .. automodule:: dateutil.rrule | ||
| :members: | ||
| :undoc-members: | ||
| Classes | ||
| ------- | ||
| .. autoclass:: rrule | ||
| .. autoclass:: rruleset | ||
| Functions | ||
| --------- | ||
| .. autofunction:: rrulestr | ||
| rrule examples | ||
@@ -11,0 +22,0 @@ -------------- |
+14
-0
@@ -39,2 +39,9 @@ == | ||
| .. autoclass:: tzwinlocal | ||
| :members: display, transitions | ||
| .. note:: | ||
| Only available on Windows | ||
| .. autoclass:: tzrange | ||
@@ -46,1 +53,8 @@ | ||
| :members: | ||
| .. autoclass:: tzwin | ||
| :members: display, transitions, list | ||
| .. note:: | ||
| Only available on Windows |
+1
-1
@@ -1,4 +0,4 @@ | ||
| include LICENSE NEWS zonefile_metadata.json updatezinfo.py | ||
| include LICENSE NEWS zonefile_metadata.json updatezinfo.py pyproject.toml | ||
| recursive-include dateutil/test * | ||
| global-exclude __pycache__ | ||
| global-exclude *.py[co] |
+117
-0
@@ -0,1 +1,118 @@ | ||
| Version 2.8.0 (2019-02-04) | ||
| ========================== | ||
| Data updates | ||
| ------------ | ||
| - Updated tzdata version to to 2018i. | ||
| Features | ||
| -------- | ||
| - Added support for ``EXDATE`` parameters when parsing ``rrule`` strings. | ||
| Reported by @mlorant (gh issue #410), fixed by @nicoe (gh pr #859). | ||
| - Added support for sub-minute time zone offsets in Python 3.6+. | ||
| Fixed by @cssherry (gh issue #582, pr #763) | ||
| - Switched the ``tzoffset``, ``tzstr`` and ``gettz`` caches over to using weak | ||
| references, so that the cache expires when no other references to the | ||
| original ``tzinfo`` objects exist. This cache-expiry behavior is not | ||
| guaranteed in the public interface and may change in the future. To improve | ||
| performance in the case where transient references to the same time zones | ||
| are repeatedly created but no strong reference is continuously held, a | ||
| smaller "strong value" cache was also added. Weak value cache implemented by | ||
| @cs-cordero (gh pr #672, #801), strong cache added by | ||
| Gökçen Nurlu (gh issue #691, gh pr #761) | ||
| Bugfixes | ||
| -------- | ||
| - Added time zone inference when initializing an ``rrule`` with a specified | ||
| ``UNTIL`` but without an explicitly specified ``DTSTART``; the time zone | ||
| of the generated ``DTSTART`` will now be taken from the ``UNTIL`` rule. | ||
| Reported by @href (gh issue #652). Fixed by @absreim (gh pr #693). | ||
| - Fixed an issue where ``parser.parse`` would raise ``Decimal``-specific errors | ||
| instead of a standard ``ValueError`` if certain malformed values were parsed | ||
| (e.g. ``NaN`` or infinite values). Reported and fixed by | ||
| @amureki (gh issue #662, gh pr #679). | ||
| - Fixed issue in ``parser`` where a ``tzinfos`` call explicitly returning | ||
| ``None`` would throw a ``ValueError``. | ||
| Fixed by @parsethis (gh issue #661, gh pr #681) | ||
| - Fixed incorrect parsing of certain dates earlier than 100 AD when repesented | ||
| in the form "%B.%Y.%d", e.g. "December.0031.30". (gh issue #687, pr #700) | ||
| - Add support for ISO 8601 times with comma as the decimal separator in the | ||
| ``dateutil.parser.isoparse`` function. (gh pr #721) | ||
| - Changed handling of ``T24:00`` to be compliant with the standard. ``T24:00`` | ||
| now represents midnight on the *following* day. | ||
| Fixed by @cheukting (gh issue #658, gh pr #751) | ||
| - Fixed an issue where ``isoparser.parse_isotime`` was unable to handle the | ||
| ``24:00`` variant representation of midnight. (gh pr #773) | ||
| - Added support for more than 6 fractional digits in `isoparse`. | ||
| Reported and fixed by @jayschwa (gh issue #786, gh pr #787). | ||
| - Added 'z' (lower case Z) as valid UTC time zone in isoparser. | ||
| Reported by @cjgibson (gh issue #820). Fixed by @Cheukting (gh pr #822) | ||
| - Fixed a bug with base offset changes during DST in ``tzfile``, and refactored | ||
| the way base offset changes are detected. Originally reported on | ||
| StackOverflow by @MartinThoma. (gh issue #812, gh pr #810) | ||
| - Fixed error condition in ``tz.gettz`` when a non-ASCII timezone is passed on | ||
| Windows in Python 2.7. (gh issue #802, pr #861) | ||
| - Improved performance and inspection properties of ``tzname`` methods. | ||
| (gh pr #811) | ||
| - Removed unnecessary binary_type compatibility shims. | ||
| Added by @jdufresne (gh pr #817) | ||
| - Changed ``python setup.py test`` to print an error to ``stderr`` and exit | ||
| with 1 instead of 0. Reported and fixed by @hroncok (gh pr #814) | ||
| - Added a ``pyproject.toml`` file with build requirements and an explicitly | ||
| specified build backend. (gh issue #736, gh prs #746, #863) | ||
| Documentation changes | ||
| --------------------- | ||
| - Added documentation for the ``rrule.rrulestr`` function. | ||
| Fixed by @prdickson (gh issue #623, gh pr #762) | ||
| - Added documentation for ``dateutil.tz.gettz``. | ||
| Fixed by @weatherpattern (gh issue #647, gh pr #704) | ||
| - Add documentation for the ``dateutil.tz.win`` module and mocked out certain | ||
| Windows-specific modules so that autodoc can still be run on non-Windows | ||
| systems. (gh issue #442, pr #715) | ||
| - Added changelog to documentation. (gh issue #692, gh pr #707) | ||
| - Changed order of keywords in the ``rrule`` docstring. | ||
| Reported and fixed by @rmahajan14 (gh issue #686, gh pr #695). | ||
| - Improved documentation on the use of ``until`` and ``count`` parameters in | ||
| ``rrule``. Fixed by @lucaferocino (gh pr #755). | ||
| - Added an example of how to use a custom ``parserinfo`` subclass to parse | ||
| non-standard datetime formats in the examples documentation for ``parser``. | ||
| Added by @prdickson (gh #753) | ||
| - Added doctest examples to ``tzfile`` documentation. | ||
| Patch by @weatherpattern (gh pr #671) | ||
| - Updated the documentation for ``relativedelta``'s ``weekday`` arguments. | ||
| Fixed by @kvn219 @huangy22 and @ElliotJH (gh pr #673) | ||
| - Improved explanation of the order that ``relativedelta`` components are | ||
| applied in. Fixed by @kvn219 @huangy22 and @ElliotJH (gh pr #673) | ||
| - Expanded the description and examples in the ``relativedelta`` class. | ||
| Contributed by @andrewcbennett (gh pr #759) | ||
| - Improved the contributing documentation to clarify where to put new changelog | ||
| files. Contributed by @andrewcbennett (gh pr #757) | ||
| - Fixed a broken doctest in the ``relativedelta`` module. | ||
| Fixed by @nherriot (gh pr #758). | ||
| - Changed the default theme to ``sphinx_rtd_theme``, and changed the sphinx | ||
| configuration accordingly. (gh pr #707) | ||
| - Reorganized ``dateutil.tz`` documentation and fixed issue with the | ||
| ``dateutil.tz`` docstring. (gh pr #714) | ||
| - Cleaned up malformed RST in the ``tz`` documentation. | ||
| (gh issue #702, gh pr #706) | ||
| - Corrected link syntax and updated URL to https for ISO year week number | ||
| notation in ``relativedelta`` examples. (gh issue #670, pr #711) | ||
| Misc | ||
| ---- | ||
| - GH #674, GH #688, GH #699, GH #720, GH #723, GH #726, GH #727, GH #740, | ||
| GH #750, GH #760, GH #767, GH #772, GH #773, GH #780, GH #784, GH #785, | ||
| GH #791, GH #799, GH #813, GH #836, GH #839, GH #857 | ||
| Version 2.7.5 (2018-10-27) | ||
@@ -2,0 +119,0 @@ ========================== |
+7
-2
| Metadata-Version: 2.1 | ||
| Name: python-dateutil | ||
| Version: 2.7.5 | ||
| Version: 2.8.0 | ||
| Summary: Extensions to the standard Python datetime module | ||
@@ -55,2 +55,8 @@ Home-page: https://dateutil.readthedocs.io | ||
| Installation | ||
| ============ | ||
| `dateutil` can be installed from PyPI using `pip` (note that the package name is | ||
| different from the importable name):: | ||
| pip install python-dateutil | ||
@@ -186,4 +192,3 @@ Download | ||
| Classifier: Topic :: Software Development :: Libraries | ||
| Requires: six | ||
| Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.* | ||
| Description-Content-Type: text/x-rst |
+9
-0
@@ -0,1 +1,10 @@ | ||
| [build-system] | ||
| requires = [ | ||
| "setuptools; python_version != '3.3'", | ||
| "setuptools<40.0; python_version == '3.3'", | ||
| "wheel", | ||
| "setuptools_scm" | ||
| ] | ||
| build-backend = "setuptools.build_meta" | ||
| [tool.towncrier] | ||
@@ -2,0 +11,0 @@ package = "dateutil" |
| Metadata-Version: 2.1 | ||
| Name: python-dateutil | ||
| Version: 2.7.5 | ||
| Version: 2.8.0 | ||
| Summary: Extensions to the standard Python datetime module | ||
@@ -55,2 +55,8 @@ Home-page: https://dateutil.readthedocs.io | ||
| Installation | ||
| ============ | ||
| `dateutil` can be installed from PyPI using `pip` (note that the package name is | ||
| different from the importable name):: | ||
| pip install python-dateutil | ||
@@ -186,4 +192,3 @@ Download | ||
| Classifier: Topic :: Software Development :: Libraries | ||
| Requires: six | ||
| Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.* | ||
| Description-Content-Type: text/x-rst |
@@ -24,4 +24,6 @@ .gitattributes | ||
| changelog.d/template.rst | ||
| ci_tools/make_zonefile_metadata.py | ||
| ci_tools/retry.bat | ||
| ci_tools/retry.sh | ||
| ci_tools/run_tz_master_env.sh | ||
| dateutil/__init__.py | ||
@@ -40,2 +42,3 @@ dateutil/_common.py | ||
| dateutil/test/_common.py | ||
| dateutil/test/conftest.py | ||
| dateutil/test/test_easter.py | ||
@@ -73,4 +76,8 @@ dateutil/test/test_import_star.py | ||
| docs/tz.rst | ||
| docs/tzwin.rst | ||
| docs/utils.rst | ||
| docs/zoneinfo.rst | ||
| docs/exercises/index.rst | ||
| docs/exercises/solutions/mlk-day-rrule.rst | ||
| docs/exercises/solutions/mlk_day_rrule_solution.py | ||
| docs/samples/EST5EDT.ics | ||
@@ -77,0 +84,0 @@ python_dateutil.egg-info/PKG-INFO |
+6
-0
@@ -45,2 +45,8 @@ dateutil - powerful extensions to datetime | ||
| Installation | ||
| ============ | ||
| `dateutil` can be installed from PyPI using `pip` (note that the package name is | ||
| different from the importable name):: | ||
| pip install python-dateutil | ||
@@ -47,0 +53,0 @@ Download |
+8
-0
@@ -8,3 +8,11 @@ [bdist_wheel] | ||
| [tool:pytest] | ||
| python_files = | ||
| test_*.py | ||
| *_test.py | ||
| *_solution.py | ||
| xfail_strict = true | ||
| filterwarnings = | ||
| error | ||
| error::DeprecationWarning | ||
| error:PendingDeprecationWarning | ||
@@ -11,0 +19,0 @@ [egg_info] |
+9
-7
@@ -13,2 +13,3 @@ #!/usr/bin/python | ||
| import io | ||
| import sys | ||
@@ -25,5 +26,7 @@ if isfile("MANIFEST"): | ||
| def run(self): | ||
| print("Running 'test' with setup.py is not supported. " | ||
| "Use 'pytest' or 'tox' to run the tests.") | ||
| sys.stderr.write("Running 'test' with setup.py is not supported. " | ||
| "Use 'pytest' or 'tox' to run the tests.\n") | ||
| sys.exit(1) | ||
| ### | ||
@@ -33,2 +36,3 @@ # Load metadata | ||
| def README(): | ||
@@ -40,6 +44,4 @@ with io.open('README.rst', encoding='utf-8') as f: | ||
| lines_out = [] | ||
| doctest_line_found = False | ||
| for line in readme_lines: | ||
| if line.startswith('.. doctest'): | ||
| doctest_line_found = True | ||
| lines_out.append('.. code-block:: python3\n') | ||
@@ -50,4 +52,5 @@ else: | ||
| return ''.join(lines_out) | ||
| README = README() | ||
| README = README() # NOQA | ||
| setup(name="python-dateutil", | ||
@@ -70,5 +73,4 @@ use_scm_version={ | ||
| zip_safe=True, | ||
| requires=["six"], | ||
| setup_requires=['setuptools_scm'], | ||
| install_requires=["six >=1.5"], # XXX fix when packaging is sane again | ||
| install_requires=["six >=1.5"], | ||
| classifiers=[ | ||
@@ -75,0 +77,0 @@ 'Development Status :: 5 - Production/Stable', |
+13
-2
@@ -19,4 +19,3 @@ [tox] | ||
| passenv = DATEUTIL_MAY_CHANGE_TZ TOXENV CI TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* CODECOV_* | ||
| commands = python -m pytest -m "not xfail" {posargs: "{toxinidir}/dateutil/test" --cov-config="{toxinidir}/tox.ini" --cov=dateutil} | ||
| python -m pytest -m "xfail" {posargs: "{toxinidir}/dateutil/test"} | ||
| commands = python -m pytest {posargs: "{toxinidir}/dateutil/test" --cov-config="{toxinidir}/tox.ini" --cov=dateutil} | ||
| deps = -rrequirements-dev.txt | ||
@@ -54,2 +53,13 @@ | ||
| [testenv:tz] | ||
| # Warning: This will modify the repository and is only intended to be run | ||
| # as part of the CI process, not locally. | ||
| description = Run the tests against the master of the tz database | ||
| basepython = python3.6 | ||
| deps = -r {toxinidir}/requirements-dev.txt | ||
| setenv = DATEUTIL_TZPATH = {envtmpdir}/tzdir/usr/share/zoneinfo | ||
| changedir = {toxworkdir} | ||
| commands = | ||
| {toxinidir}/ci_tools/run_tz_master_env.sh {envtmpdir} {toxinidir} | ||
| [testenv:docs] | ||
@@ -62,1 +72,2 @@ description = invoke sphinx-build to build the HTML docs, check that URIs are valid | ||
| sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" {posargs:-W --color -blinkcheck} | ||
| python setup.py check -r -s |
+10
-3
@@ -15,4 +15,4 @@ #!/usr/bin/env python | ||
| def main(): | ||
| with io.open(METADATA_FILE, 'r') as f: | ||
| def main(metadata_file): | ||
| with io.open(metadata_file, 'r') as f: | ||
| metadata = json.load(f) | ||
@@ -56,2 +56,9 @@ | ||
| if __name__ == "__main__": | ||
| main() | ||
| import argparse | ||
| parser = argparse.ArgumentParser() | ||
| parser.add_argument('metadata', metavar='METADATA_FILE', | ||
| default=METADATA_FILE, | ||
| nargs='?') | ||
| args = parser.parse_args() | ||
| main(args.metadata) |
@@ -7,5 +7,5 @@ { | ||
| ], | ||
| "tzdata_file": "tzdata2018g.tar.gz", | ||
| "tzdata_file_sha512": "92e9bbd61f51be8f2cf7ec9491691e5e2f97803914dbad77b7fb8b6600ed68fc3b98450fc808bb2d4c6c835df5f9eb7bf4529d059d9b1370f2ab4c12e7f1adfa", | ||
| "tzversion": "2018g", | ||
| "tzdata_file": "tzdata2018i.tar.gz", | ||
| "tzdata_file_sha512": "6afcacb377842190648ed26f01abcf3db37aa2e7c63d8c509c29b4bc0078b7ff2d4e5375291b9f53498215b9e2f04936bc6145e2f651ae0be6d8166d8d336f6a", | ||
| "tzversion": "2018i", | ||
| "zonegroups": [ | ||
@@ -12,0 +12,0 @@ "africa", |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
1051292
3.47%87
8.75%15342
2.28%