aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2019-06-10 20:32:38 -0700
committerandroid-build-merger <android-build-merger@google.com>2019-06-10 20:32:38 -0700
commit6602baf626cc7cc3e2a72a8f0ca2763543704a8f (patch)
tree7f1bce790a13af6dee746104bd81809a4bd6c934
parentd0da7aa65f2f29c46325bfb511aaa98790da47d3 (diff)
parent2e0711f38ab5d7dc7a0497516a115d769e563663 (diff)
downloaddateutil-6602baf626cc7cc3e2a72a8f0ca2763543704a8f.tar.gz
Merge "Upgrade python/dateutil to 2.8.0" am: f88174c9fb
am: 2e0711f38a Change-Id: I9bab8c27f546d2a1741943d8d8e88e7c81507480
-rw-r--r--.github/pull_request_template.md12
-rw-r--r--.gitignore4
-rw-r--r--.travis.yml11
-rw-r--r--AUTHORS.md44
-rw-r--r--CONTRIBUTING.md64
-rw-r--r--MANIFEST.in2
-rw-r--r--METADATA13
-rw-r--r--NEWS198
-rw-r--r--README.rst42
-rwxr-xr-xci_tools/make_zonefile_metadata.py63
-rwxr-xr-xci_tools/run_tz_master_env.sh93
-rw-r--r--dateutil/parser/_parser.py88
-rw-r--r--dateutil/parser/isoparser.py39
-rw-r--r--dateutil/relativedelta.py72
-rw-r--r--dateutil/rrule.py201
-rw-r--r--dateutil/test/conftest.py41
-rw-r--r--dateutil/test/property/test_isoparse_prop.py27
-rw-r--r--dateutil/test/property/test_parser_prop.py22
-rw-r--r--dateutil/test/test_easter.py34
-rw-r--r--dateutil/test/test_import_star.py40
-rw-r--r--dateutil/test/test_isoparser.py62
-rw-r--r--dateutil/test/test_parser.py627
-rw-r--r--dateutil/test/test_relativedelta.py18
-rw-r--r--dateutil/test/test_rrule.py103
-rw-r--r--dateutil/test/test_tz.py244
-rw-r--r--dateutil/tz/__init__.py2
-rw-r--r--dateutil/tz/_common.py22
-rw-r--r--dateutil/tz/_factories.py28
-rw-r--r--dateutil/tz/tz.py332
-rw-r--r--dateutil/tz/win.py45
-rw-r--r--dateutil/utils.py6
-rw-r--r--dateutil/zoneinfo/dateutil-zoneinfo.tar.gzbin139080 -> 154405 bytes
-rw-r--r--docs/changelog.rst7
-rw-r--r--docs/conf.py25
-rw-r--r--docs/examples.rst15
-rw-r--r--docs/exercises/index.rst242
-rw-r--r--docs/exercises/solutions/mlk-day-rrule.rst11
-rw-r--r--docs/exercises/solutions/mlk_day_rrule_solution.py40
-rw-r--r--docs/index.rst11
-rw-r--r--docs/relativedelta.rst2
-rw-r--r--docs/requirements-docs.txt3
-rw-r--r--docs/rrule.rst13
-rw-r--r--docs/tz.rst56
-rw-r--r--docs/tzwin.rst22
-rw-r--r--pyproject.toml9
-rw-r--r--requirements-dev.txt4
-rw-r--r--setup.cfg10
-rw-r--r--setup.py16
-rw-r--r--tox.ini17
-rw-r--r--updatezinfo.py13
-rw-r--r--zonefile_metadata.json6
51 files changed, 2299 insertions, 822 deletions
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..9e1e491
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,12 @@
+<!-- First time contributors: Take a moment to review CONTRIBUTING.md! -->
+<!-- Remove sections if not applicable -->
+## Summary of changes
+
+<!-- Summary goes here -->
+
+Closes <!-- issue number here -->
+
+### Pull Request Checklist
+- [ ] Changes have tests
+- [ ] Authors have been added to [AUTHORS.md](https://github.com/dateutil/dateutil/blob/master/AUTHORS.md)
+- [ ] News fragment added in changelog.d. See [CONTRIBUTING.md](https://github.com/dateutil/dateutil/blob/master/CONTRIBUTING.md#changelog) for details
diff --git a/.gitignore b/.gitignore
index 11b4a9a..ae11b69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,9 @@ dist/
# Test detritus
.tox/
.pytest_cache/
+venv/
+.venv/
+.hypothesis/
# Autogenerated version information
dateutil/_version.py
@@ -24,3 +27,4 @@ tzdata*.tar.gz
.idea
.cache
+.mypy_cache
diff --git a/.travis.yml b/.travis.yml
index 3329ab0..733767f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -18,12 +18,21 @@ matrix:
include:
- python: 3.6
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:
- python: "nightly"
install:
- 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
script:
- tox
diff --git a/AUTHORS.md b/AUTHORS.md
index 8e8bc2b..5628440 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -17,33 +17,51 @@ switch, and thus all their contributions are dual-licensed.
- Alec Reiter <areiter@MASKED>
- Alex Chamberlain (gh: @alexchamberlain) **D**
- 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>
- Brock Mendel <jbrockmendel@MASKED> (gh: @jbrockmendel) **R**
+- Brook Li (gh: @absreim) **D**
- 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) **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)
- Holger Joukl <holger.joukl@MASKED> (gh: @hjoukl)
- Igor <mrigor83@MASKED>
- Ionuț Ciocîrlan <jdxlark@MASKED>
- Jake Chorley (gh: @jakec-github) **D**
- 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>
+- 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) **D**
+- Luca Ferocino <luca.ferox@MASKED> (gh: @lucaferocino) **D**
- Mario Corchero <mcorcherojim@MASKED> (gh: @mariocj89) **R**
- Mateusz Dziedzic (gh: @m-dz) **D**
- Matthew Schinckel <matt@MASKED>
@@ -52,19 +70,28 @@ switch, and thus all their contributions are dual-licensed.
- Michael Aquilina <michaelaquilina@MASKED> (gh: @MichaelAquilina)
- Michael J. Schultz <mjschultz@MASKED>
- 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) **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>
+- Raymond Cha (gh: @weatherpattern) **D**
+- Ridhi Mahajan <ridhikmahajan@MASKED> **D**
- Roy Williams <rwilliams@MASKED>
+- 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>
- Thomas Kluyver <takowl@MASKED> (gh: @takluyver)
- Tomasz Kluczkowski (gh: @Tomasz-Kluczkowski) **D**
@@ -74,12 +101,13 @@ switch, and thus all their contributions are dual-licensed.
- X O <xo@MASKED>
- Yaron de Leeuw <me@jarondl.net> (gh: @jarondl)
- Yoney <alper_yoney@hotmail.com> **D**
+- Yuan Huang <huangy22@gmail.com> (gh: @huangy22) **D**
- Zbigniew Jędrzejewski-Szmek <zbyszek@MASKED>
- bachmann <bachmann.matt@MASKED>
- bjv <brandon.vanvaerenbergh@MASKED> (@bjamesvERT)
- 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.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bad2ccd..e93dab2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -28,7 +28,7 @@ If you would like to fix something in `dateutil` - improvements to documentatio
The most important thing to include in your pull request are *tests* - please write one or more tests to cover the behavior you intend your patch to improve. Ideally, tests would use only the public interface - try to get 100% difference coverage using only supported behavior of the API.
#### 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:
- `feature`: A new feature, (e.g. a new function, method, attribute, etc)
- `bugfix`: A fix to a bug
@@ -59,42 +59,54 @@ Starting December 1, 2017, all contributions will be assumed to be released unde
All contributions before December 1, 2017 except those explicitly relicensed, are only under the 3-clause BSD license.
-## Building and releasing
-
-When you get the source, it does not contain the internal zoneinfo
-database. To get (and update) the database, run the updatezinfo.py script. Make sure
-that the zic command is in your path, and that you have network connectivity
-to get the latest timezone information from IANA, or from [our mirror of the
-IANA database](https://dateutil.github.io/tzdata/).
## Development Setup
-Install the the dependencies for running the test suite using `pip` or `conda`.
+### Using a virtual environment
-### pip
+It is advisable to work in a virtual environment for development of `dateutil`. This can be done using [virtualenv](https://virtualenv.pypa.io):
-Run the following commands to create a [virtual environment](https://virtualenv.pypa.io) with all dependencies installed:
+```bash
+python -m virtualenv .venv # Create virtual environment in .venv directory
+source .venv/bin/activate # Activate the virtual environment
+```
- python -m virtualenv .venv # Create virtual environment in .venv directory
- . .venv/bin/activate # Activate the virtual environment
- pip install -r requirements.txt # Install the dependencies
+Alternatively you can create a [conda environment](https://conda.io/docs/user-guide/tasks/manage-environments.html):
-### conda
+```bash
+conda create -n dateutil # Create a conda environment
+# conda create -n dateutil python=3.6 # Or specify a version
+source activate dateutil # Activate the conda environment
+```
-Run the following commands to create a [conda environment](https://conda.io) with all dependencies installed:
+Once your virtual environment is created, install the library in development mode:
- conda create -n dateutil # Create a conda environment
- # conda create -n dateutil python=3.6 # or specify a version
- source activate dateutil # Activate the conda environment
- pip install -r requirements.txt # Install the dependencies
+```bash
+pip install -e .
+```
+
+This will allow scripts run in your virtual environment to use the version of `dateutil` in your local directory. If you also want to run the tests in your local directory, install the test dependencies:
+
+```bash
+pip install -r requirements-dev.txt
+```
## Testing
-dateutil has a comprehensive test suite, which can be run simply by running
-`python -m pytest` in the project root. Note that if you don't have the internal
-zoneinfo database, some tests will fail. Apart from that, all tests should pass.
+The best way to test `dateutil` is to run `tox`. By default, `tox` will test against all supported versions of Python installed on your system. To limit the number of tests, run a specific subset of environments. For example, to run only on Python 2.7 and Python 3.6:
+
+```bash
+tox -e py27,py36
+```
+
+You can also pass arguments to `pytest` through `tox` by placing them after `--`:
+
+```bash
+tox -e py36 -- -m tzstr
+```
+
+This will pass the `-m tzstr` parameter to `pytest`, running only the tests with the `tzstr` mark.
-To easily test dateutil against all supported Python versions, you can use
-[tox](https://tox.readthedocs.io/en/latest/).
+The tests can also be run directly by running `pytest` or `python -m pytest` in the root directory. This will be likely be less thorough but is often faster and is a good first pass to check your changes.
-All GitHub pull requests are automatically tested using travis and appveyor.
+All GitHub pull requests are automatically tested using [Travis](https://travis-ci.org/dateutil/dateutil/) and [Appveyor](https://ci.appveyor.com/project/dateutil/dateutil).
diff --git a/MANIFEST.in b/MANIFEST.in
index dd5142e..d4d9f32 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,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]
diff --git a/METADATA b/METADATA
index df039a2..a7117e8 100644
--- a/METADATA
+++ b/METADATA
@@ -1,8 +1,5 @@
name: "dateutil"
-description:
- "The dateutil module provides powerful extensions to the standard datetime "
- "module, available in Python."
-
+description: "The dateutil module provides powerful extensions to the standard datetime module, available in Python."
third_party {
url {
type: HOMEPAGE
@@ -12,6 +9,10 @@ third_party {
type: GIT
value: "https://github.com/dateutil/dateutil/"
}
- version: "2.7.2"
- last_upgrade_date { year: 2018 month: 5 day: 30 }
+ version: "2.8.0"
+ last_upgrade_date {
+ year: 2019
+ month: 2
+ day: 5
+ }
}
diff --git a/NEWS b/NEWS
index 92a27c8..1968421 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,193 @@
+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)
+==========================
+
+Data updates
+------------
+
+- Update tzdata to 2018g
+
+
+Version 2.7.4 (2018-10-24)
+==========================
+
+Data updates
+------------
+
+- Updated tzdata version to 2018f.
+
+
+Version 2.7.3 (2018-05-09)
+==========================
+
+Data updates
+------------
+
+- Update tzdata to 2018e. (gh pr #710)
+
+
+Bugfixes
+--------
+
+- Fixed an issue where decimal.Decimal would cast `NaN` or infinite value in a
+ parser.parse, which will raise decimal.Decimal-specific errors. Reported and
+ fixed by @amureki (gh issue #662, gh pr #679).
+- Fixed a ValueError being thrown if tzinfos call explicity returns ``None``.
+ Reported by @pganssle (gh issue #661) Fixed by @parsethis (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)
+- Fixed a bug where automatically generated DTSTART was naive even if a
+ specified UNTIL had a time zone. Automatically generated DTSTART will now
+ take on the timezone of an UNTIL date, if provided. Reported by @href (gh
+ issue #652). Fixed by @absreim (gh pr #693).
+
+
+Documentation changes
+---------------------
+
+- Corrected link syntax and updated URL to https for ISO year week number
+ notation in relativedelta examples. (gh issue #670, pr #711)
+- Add doctest examples to tzfile documentation. Done by @weatherpattern and
+ @pganssle (gh pr #671)
+- Updated the documentation for relativedelta. Removed references to tuple
+ arguments for weekday, explained effect of weekday(_, 1) and better explained
+ the order of operations that relativedelta applies. Fixed by @kvn219
+ @huangy22 and @ElliotJH (gh pr #673)
+- Added changelog to documentation. (gh issue #692, gh pr #707)
+- Changed order of keywords in rrule docstring. Reported and fixed by
+ @rmahajan14 (gh issue #686, gh pr #695).
+- Added documentation for ``dateutil.tz.gettz``. Reported by @pganssle (gh
+ issue #647). Fixed by @weatherpattern (gh pr #704)
+- Cleaned up malformed RST in the ``tz`` documentation. (gh issue #702, gh pr
+ #706)
+- Changed the default theme to sphinx_rtd_theme, and changed the sphinx
+ configuration to go along with that. (gh pr #707)
+- Reorganized ``dateutil.tz`` documentation and fixed issue with the
+ ``dateutil.tz`` docstring. (gh pr #714)
+
+
+Misc
+----
+
+- GH #674, GH #688, GH #699
+
+
Version 2.7.2 (2018-03-26)
==========================
@@ -162,7 +352,7 @@ Version 2.7.0
- Style improvement to zoneinfo.tzfile that was confusing to static type
checkers. Reported and fixed by @quodlibetor (gh pr #485)
- Several unused imports were removed by @jdufresne. (gh pr #486)
-- Switched isinstance(*, collections.Callable) to callable, which is available
+- Switched ``isinstance(*, collections.Callable)`` to callable, which is available
on all supported Python versions. Implemented by @jdufresne (gh pr #612)
- Added CONTRIBUTING.md (gh pr #533)
- Added AUTHORS.md (gh pr #542)
@@ -609,7 +799,7 @@ Version 0.9
Andreas Köhler.
- Implemented internal timezone information with binary
- timezone files [1]. datautil.tz.gettz() function will now
+ timezone files. datautil.tz.gettz() function will now
try to use the system timezone files, and fallback to
the internal versions. It's also possible to ask for
the internal versions directly by using
@@ -628,13 +818,11 @@ Version 0.9
- Fixed other reported bugs.
-[1] http://www.twinsun.com/tz/tz-link.htm
-
Version 0.5
===========
-- Removed FREQ_ prefix from rrule frequency constants
+- Removed ``FREQ_`` prefix from rrule frequency constants
WARNING: this breaks compatibility with previous versions.
- Fixed rrule.between() for cases where "after" is achieved
diff --git a/README.rst b/README.rst
index f0ee814..e70120d 100644
--- a/README.rst
+++ b/README.rst
@@ -1,30 +1,53 @@
dateutil - powerful extensions to datetime
==========================================
-.. image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square
+|pypi| |support| |licence|
+
+|gitter| |readthedocs|
+
+|travis| |appveyor| |coverage|
+
+.. |pypi| image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square
:target: https://pypi.org/project/python-dateutil/
:alt: pypi version
-.. image:: https://img.shields.io/travis/dateutil/dateutil/master.svg?style=flat-square
+.. |support| image:: https://img.shields.io/pypi/pyversions/python-dateutil.svg?style=flat-square
+ :target: https://pypi.org/project/python-dateutil/
+ :alt: supported Python version
+
+.. |travis| image:: https://img.shields.io/travis/dateutil/dateutil/master.svg?style=flat-square&label=Travis%20Build
:target: https://travis-ci.org/dateutil/dateutil
:alt: travis build status
-.. image:: https://img.shields.io/appveyor/ci/dateutil/dateutil/master.svg?style=flat-square
+.. |appveyor| image:: https://img.shields.io/appveyor/ci/dateutil/dateutil/master.svg?style=flat-square&logo=appveyor
:target: https://ci.appveyor.com/project/dateutil/dateutil
:alt: appveyor build status
-.. image:: https://codecov.io/github/dateutil/dateutil/coverage.svg?branch=master
+.. |coverage| image:: https://codecov.io/github/dateutil/dateutil/coverage.svg?branch=master
:target: https://codecov.io/github/dateutil/dateutil?branch=master
:alt: Code coverage
-.. image:: https://badges.gitter.im/dateutil/dateutil.svg
+.. |gitter| image:: https://badges.gitter.im/dateutil/dateutil.svg
:alt: Join the chat at https://gitter.im/dateutil/dateutil
:target: https://gitter.im/dateutil/dateutil
+.. |licence| image:: https://img.shields.io/pypi/l/python-dateutil.svg?style=flat-square
+ :target: https://pypi.org/project/python-dateutil/
+ :alt: licence
+
+.. |readthedocs| image:: https://img.shields.io/readthedocs/dateutil/latest.svg?style=flat-square&label=Read%20the%20Docs
+ :alt: Read the documentation at https://dateutil.readthedocs.io/en/latest/
+ :target: https://dateutil.readthedocs.io/en/latest/
The `dateutil` module provides powerful extensions to
the standard `datetime` module, available in Python.
+Installation
+============
+`dateutil` can be installed from PyPI using `pip` (note that the package name is
+different from the importable name)::
+
+ pip install python-dateutil
Download
========
@@ -116,14 +139,10 @@ Starting with version 2.4.1, all source and binary distributions will be signed
by a PGP key that has, at the very least, been signed by the key which made the
previous release. A table of release signing keys can be found below:
-Starting with version 2.4.1, all source and binary distributions will be signed
-by a PGP key that has, at the very least, been signed by the key which made the
-previous release. A table of release signing keys can be found below:
-
=========== ============================
Releases Signing key fingerprint
=========== ============================
-2.4.1- `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_
+2.4.1- `6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB`_ (|pgp_mirror|_)
=========== ============================
@@ -140,3 +159,6 @@ All contributions after December 1, 2017 released under dual license - either `A
.. _6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB:
https://pgp.mit.edu/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB
+
+.. |pgp_mirror| replace:: mirror
+.. _pgp_mirror: https://sks-keyservers.net/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB
diff --git a/ci_tools/make_zonefile_metadata.py b/ci_tools/make_zonefile_metadata.py
new file mode 100755
index 0000000..39bd551
--- /dev/null
+++ b/ci_tools/make_zonefile_metadata.py
@@ -0,0 +1,63 @@
+#!/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)
+
diff --git a/ci_tools/run_tz_master_env.sh b/ci_tools/run_tz_master_env.sh
new file mode 100755
index 0000000..059c631
--- /dev/null
+++ b/ci_tools/run_tz_master_env.sh
@@ -0,0 +1,93 @@
+#!/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
+
diff --git a/dateutil/parser/_parser.py b/dateutil/parser/_parser.py
index 0eac592..0da0f3e 100644
--- a/dateutil/parser/_parser.py
+++ b/dateutil/parser/_parser.py
@@ -40,7 +40,7 @@ from calendar import monthrange
from io import StringIO
import six
-from six import binary_type, integer_types, text_type
+from six import integer_types, text_type
from decimal import Decimal
@@ -63,7 +63,7 @@ class _timelex(object):
if six.PY2:
# In Python 2, we can't duck type properly because unicode has
# a 'decode' function, and we'd be double-decoding
- if isinstance(instream, (binary_type, bytearray)):
+ if isinstance(instream, (bytes, bytearray)):
instream = instream.decode()
else:
if getattr(instream, 'decode', None) is not None:
@@ -291,7 +291,7 @@ class parserinfo(object):
("s", "second", "seconds")]
AMPM = [("am", "a"),
("pm", "p")]
- UTCZONE = ["UTC", "GMT", "Z"]
+ UTCZONE = ["UTC", "GMT", "Z", "z"]
PERTAIN = ["of"]
TZOFFSET = {}
# TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate",
@@ -364,13 +364,23 @@ class parserinfo(object):
return self.TZOFFSET.get(name)
def convertyear(self, year, century_specified=False):
+ """
+ Converts two-digit years to year within [-50, 49]
+ range of self._year (current local time)
+ """
+
+ # Function contract is that the year is always positive
+ assert year >= 0
+
if year < 100 and not century_specified:
+ # assume current century to start
year += self._century
- if abs(year - self._year) >= 50:
- if year < self._year:
- year += 100
- else:
- year -= 100
+
+ if year >= self._year + 50: # if too far in future
+ year -= 100
+ elif year < self._year - 50: # if too far in past
+ year += 100
+
return year
def validate(self, res):
@@ -378,7 +388,8 @@ class parserinfo(object):
if res.year is not None:
res.year = self.convertyear(res.year, res.century_specified)
- 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"
res.tzoffset = 0
elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
@@ -448,10 +459,37 @@ class _ymd(list):
raise ValueError('Year is already set')
self.ystridx = len(self) - 1
+ def _resolve_from_stridxs(self, strids):
+ """
+ Try to resolve the identities of year/month/day elements using
+ ystridx, mstridx, and dstridx, if enough of these are specified.
+ """
+ if len(self) == 3 and len(strids) == 2:
+ # we can back out the remaining stridx value
+ missing = [x for x in range(3) if x not in strids.values()]
+ key = [x for x in ['y', 'm', 'd'] if x not in strids]
+ assert len(missing) == len(key) == 1
+ key = key[0]
+ val = missing[0]
+ strids[key] = val
+
+ assert len(self) == len(strids) # otherwise this should not be called
+ out = {key: self[strids[key]] for key in strids}
+ return (out.get('y'), out.get('m'), out.get('d'))
+
def resolve_ymd(self, yearfirst, dayfirst):
len_ymd = len(self)
year, month, day = (None, None, None)
+ strids = (('y', self.ystridx),
+ ('m', self.mstridx),
+ ('d', self.dstridx))
+
+ strids = {key: val for key, val in strids if val is not None}
+ if (len(self) == len(strids) > 0 or
+ (len(self) == 3 and len(strids) == 2)):
+ return self._resolve_from_stridxs(strids)
+
mstridx = self.mstridx
if len_ymd > 3:
@@ -460,13 +498,17 @@ class _ymd(list):
# One member, or two members with a month string
if mstridx is not None:
month = self[mstridx]
- del self[mstridx]
+ # since mstridx is 0 or 1, self[mstridx-1] always
+ # looks up the other element
+ other = self[mstridx - 1]
+ else:
+ other = self[0]
if len_ymd > 1 or mstridx is None:
- if self[0] > 31:
- year = self[0]
+ if other > 31:
+ year = other
else:
- day = self[0]
+ day = other
elif len_ymd == 2:
# Two members with numbers
@@ -1019,7 +1061,8 @@ class parser(object):
tzname is None and
tzoffset is None and
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))
def _ampm_valid(self, hour, ampm, fuzzy):
"""
@@ -1115,16 +1158,14 @@ class parser(object):
tzdata = tzinfos(tzname, tzoffset)
else:
tzdata = tzinfos.get(tzname)
-
- if isinstance(tzdata, datetime.tzinfo):
+ # handle case where tzinfo is paased an options that returns None
+ # eg tzinfos = {'BRST' : None}
+ if isinstance(tzdata, datetime.tzinfo) or tzdata is None:
tzinfo = tzdata
elif isinstance(tzdata, text_type):
tzinfo = tz.tzstr(tzdata)
elif isinstance(tzdata, integer_types):
tzinfo = tz.tzoffset(tzname, tzdata)
- else:
- raise ValueError("Offset must be tzinfo subclass, "
- "tz string, or int offset.")
return tzinfo
def _build_tzaware(self, naive, res, tzinfos):
@@ -1160,7 +1201,7 @@ class parser(object):
warnings.warn("tzname {tzname} identified but not understood. "
"Pass `tzinfos` argument in order to correctly "
"return a timezone-aware datetime. In a future "
- "version, this raise an "
+ "version, this will raise an "
"exception.".format(tzname=res.tzname),
category=UnknownTimezoneWarning)
aware = naive
@@ -1202,10 +1243,15 @@ class parser(object):
def _to_decimal(self, val):
try:
- return Decimal(val)
+ decimal_value = Decimal(val)
+ # See GH 662, edge case, infinite value should not be converted via `_to_decimal`
+ if not decimal_value.is_finite():
+ raise ValueError("Converted decimal value is infinite or NaN")
except Exception as e:
msg = "Could not convert %s to decimal" % val
six.raise_from(ValueError(msg), e)
+ else:
+ return decimal_value
DEFAULTPARSER = parser()
diff --git a/dateutil/parser/isoparser.py b/dateutil/parser/isoparser.py
index 844bb5d..e3cf6d8 100644
--- a/dateutil/parser/isoparser.py
+++ b/dateutil/parser/isoparser.py
@@ -4,6 +4,8 @@ This module offers a parser for ISO-8601 strings
It is intended to support all valid date, time and datetime formats per the
ISO-8601 specification.
+
+..versionadded:: 2.7.0
"""
from datetime import datetime, timedelta, time, date
import calendar
@@ -86,10 +88,12 @@ class isoparser(object):
- ``hh``
- ``hh:mm`` or ``hhmm``
- ``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::
@@ -124,6 +128,8 @@ class isoparser(object):
currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
guaranteed to continue failing in future versions if they encode
a valid date.
+
+ .. versionadded:: 2.7.0
"""
components, pos = self._parse_isodate(dt_str)
@@ -133,6 +139,10 @@ class isoparser(object):
else:
raise ValueError('String contains unknown ISO components')
+ if len(components) > 3 and components[3] == 24:
+ components[3] = 0
+ return datetime(*components) + timedelta(days=1)
+
return datetime(*components)
@_takes_ascii
@@ -163,7 +173,10 @@ class isoparser(object):
:return:
Returns a :class:`datetime.time` object
"""
- return time(*self._parse_isotime(timestr))
+ components = self._parse_isotime(timestr)
+ if components[0] == 24:
+ components[0] = 0
+ return time(*components)
@_takes_ascii
def parse_tzstr(self, tzstr, zero_as_utc=True):
@@ -186,10 +199,9 @@ class isoparser(object):
return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
# Constants
- _MICROSECOND_END_REGEX = re.compile(b'[-+Z]+')
_DATE_SEP = b'-'
_TIME_SEP = b':'
- _MICRO_SEP = b'.'
+ _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)')
def _parse_isodate(self, dt_str):
try:
@@ -329,7 +341,7 @@ class isoparser(object):
while pos < len_str and comp < 5:
comp += 1
- if timestr[pos:pos + 1] in b'-+Z':
+ if timestr[pos:pos + 1] in b'-+Zz':
# Detect time zone boundary
components[-1] = self._parse_tzstr(timestr[pos:])
pos = len_str
@@ -344,16 +356,14 @@ class isoparser(object):
pos += 1
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())
if pos < len_str:
raise ValueError('Unused components in ISO string')
@@ -362,12 +372,11 @@ class isoparser(object):
# Standard supports 00:00 and 24:00 as representations of midnight
if any(component != 0 for component in components[1:4]):
raise ValueError('Hour may only be 24 at 24:00:00.000')
- components[0] = 0
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()
if len(tzstr) not in {3, 5, 6}:
diff --git a/dateutil/relativedelta.py b/dateutil/relativedelta.py
index 584ed5a..c65c66e 100644
--- a/dateutil/relativedelta.py
+++ b/dateutil/relativedelta.py
@@ -17,8 +17,12 @@ __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
class relativedelta(object):
"""
- 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.
However, notice that this type does *NOT* implement the same algorithm as
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
@@ -44,12 +48,16 @@ class relativedelta(object):
the corresponding aritmetic operation on the original datetime value
with the information in the relativedelta.
- 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.
+ weekday:
+ 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).
leapdays:
Will add given days to the date found, if year is a leap
@@ -59,33 +67,39 @@ class relativedelta(object):
Set the yearday or the non-leap year day (jump leap days).
These are converted to day/month/leapdays information.
- Here is the behavior of operations with relativedelta:
+ There are relative and absolute forms of the keyword
+ arguments. The plural is relative, and the singular is
+ absolute. For each argument in the order below, the absolute form
+ is applied first (by setting each attribute to that value) and
+ then the relative form (by adding the value to the attribute).
- 1. Calculate the absolute year, using the 'year' argument, or the
- original datetime year, if the argument is not present.
+ The order of attributes considered when this relativedelta is
+ added to a datetime is:
- 2. Add the relative 'years' argument to the absolute year.
+ 1. Year
+ 2. Month
+ 3. Day
+ 4. Hours
+ 5. Minutes
+ 6. Seconds
+ 7. Microseconds
- 3. Do steps 1 and 2 for month/months.
+ Finally, weekday is applied, using the rule described above.
- 4. Calculate the absolute day, using the 'day' argument, or the
- original datetime day, if the argument is not present. Then,
- subtract from the day until it fits in the year and month
- found after their operations.
+ For example
- 5. Add the relative 'days' argument to the absolute day. Notice
- that the 'weeks' argument is multiplied by 7 and added to
- 'days'.
+ >>> 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))
+ >>> dt + delta
+ datetime.datetime(2018, 4, 2, 14, 37)
- 6. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
- microsecond/microseconds.
+ First, the day is set to 1 (the first of the month), then 25 hours
+ are added, to get to the 2nd day and 14th hour, finally the
+ weekday is applied, but since the 2nd is already a Monday there is
+ no effect.
- 7. If the 'weekday' argument is present, calculate the weekday,
- with the given (wday, nth) tuple. wday is the index of the
- weekday (0-6, 0=Mon), and nth is the number of weeks to add
- forward or backward, depending on its signal. Notice that if
- the calculated date is already Monday, for example, using
- (0, 1) or (0, -1) won't change the day.
"""
def __init__(self, dt1=None, dt2=None,
@@ -271,7 +285,7 @@ class relativedelta(object):
values for the relative attributes.
>>> relativedelta(days=1.5, hours=2).normalized()
- relativedelta(days=1, hours=14)
+ relativedelta(days=+1, hours=+14)
:return:
Returns a :class:`dateutil.relativedelta.relativedelta` object.
diff --git a/dateutil/rrule.py b/dateutil/rrule.py
index ef4607a..20a0c4a 100644
--- a/dateutil/rrule.py
+++ b/dateutil/rrule.py
@@ -337,10 +337,6 @@ class rrule(rrulebase):
Additionally, it supports the following keyword arguments:
- :param cache:
- If given, it must be a boolean value specifying to enable or disable
- caching of results. If you will use the same rrule instance multiple
- times, enabling caching will improve the performance considerably.
:param dtstart:
The recurrence start. Besides being the base for the recurrence,
missing parameters in the final recurrence instances will also be
@@ -357,20 +353,26 @@ class rrule(rrulebase):
from calendar.firstweekday(), and may be modified by
calendar.setfirstweekday().
:param count:
- How many occurrences will be generated.
+ If given, this determines how many occurrences will be generated.
.. note::
- As of version 2.5.0, the use of the ``until`` keyword together
- with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10.
+ As of version 2.5.0, the use of the keyword ``until`` in conjunction
+ with ``count`` is deprecated, to make sure ``dateutil`` is fully
+ compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
+ html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
+ **must not** occur in the same call to ``rrule``.
:param until:
- If given, this must be a datetime instance, that will specify the
+ If given, this must be a datetime instance specifying the upper-bound
limit of the recurrence. The last recurrence in the rule is the greatest
datetime that is less than or equal to the value specified in the
``until`` parameter.
.. note::
- As of version 2.5.0, the use of the ``until`` keyword together
- with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10.
+ As of version 2.5.0, the use of the keyword ``until`` in conjunction
+ with ``count`` is deprecated, to make sure ``dateutil`` is fully
+ compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
+ html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
+ **must not** occur in the same call to ``rrule``.
:param bysetpos:
If given, it must be either an integer, or a sequence of integers,
positive or negative. Each given integer will specify an occurrence
@@ -387,6 +389,11 @@ class rrule(rrulebase):
:param byyearday:
If given, it must be either an integer, or a sequence of integers,
meaning the year days to apply the recurrence to.
+ :param byeaster:
+ If given, it must be either an integer, or a sequence of integers,
+ positive or negative. Each integer will define an offset from the
+ Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
+ Sunday itself. This is an extension to the RFC specification.
:param byweekno:
If given, it must be either an integer, or a sequence of integers,
meaning the week numbers to apply the recurrence to. Week numbers
@@ -412,11 +419,10 @@ class rrule(rrulebase):
:param bysecond:
If given, it must be either an integer, or a sequence of integers,
meaning the seconds to apply the recurrence to.
- :param byeaster:
- If given, it must be either an integer, or a sequence of integers,
- positive or negative. Each integer will define an offset from the
- Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
- Sunday itself. This is an extension to the RFC specification.
+ :param cache:
+ If given, it must be a boolean value specifying to enable or disable
+ caching of results. If you will use the same rrule instance multiple
+ times, enabling caching will improve the performance considerably.
"""
def __init__(self, freq, dtstart=None,
interval=1, wkst=None, count=None, until=None, bysetpos=None,
@@ -427,7 +433,10 @@ class rrule(rrulebase):
super(rrule, self).__init__(cache)
global easter
if not dtstart:
- dtstart = datetime.datetime.now().replace(microsecond=0)
+ if until and until.tzinfo:
+ dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
+ else:
+ dtstart = datetime.datetime.now().replace(microsecond=0)
elif not isinstance(dtstart, datetime.datetime):
dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
else:
@@ -1403,7 +1412,52 @@ class rruleset(rrulebase):
self._len = total
+
+
class _rrulestr(object):
+ """ Parses a string representation of a recurrence rule or set of
+ recurrence rules.
+
+ :param s:
+ Required, a string defining one or more recurrence rules.
+
+ :param dtstart:
+ If given, used as the default recurrence start if not specified in the
+ rule string.
+
+ :param cache:
+ If set ``True`` caching of results will be enabled, improving
+ performance of multiple queries considerably.
+
+ :param unfold:
+ If set ``True`` indicates that a rule string is split over more
+ than one line and should be joined before processing.
+
+ :param forceset:
+ If set ``True`` forces a :class:`dateutil.rrule.rruleset` to
+ be returned.
+
+ :param compatible:
+ If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``.
+
+ :param ignoretz:
+ If set ``True``, time zones in parsed strings are ignored and a naive
+ :class:`datetime.datetime` object is returned.
+
+ :param tzids:
+ If given, a callable or mapping used to retrieve a
+ :class:`datetime.tzinfo` from a string representation.
+ Defaults to :func:`dateutil.tz.gettz`.
+
+ :param tzinfos:
+ Additional time zone names / aliases which may be present in a string
+ representation. See :func:`dateutil.parser.parse` for more
+ information.
+
+ :return:
+ Returns a :class:`dateutil.rrule.rruleset` or
+ :class:`dateutil.rrule.rrule`
+ """
_freq_map = {"YEARLY": YEARLY,
"MONTHLY": MONTHLY,
@@ -1505,6 +1559,58 @@ class _rrulestr(object):
raise ValueError("invalid '%s': %s" % (name, value))
return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
+ def _parse_date_value(self, date_value, parms, rule_tzids,
+ ignoretz, tzids, tzinfos):
+ global parser
+ if not parser:
+ from dateutil import parser
+
+ datevals = []
+ value_found = False
+ TZID = None
+
+ for parm in parms:
+ if parm.startswith("TZID="):
+ try:
+ tzkey = rule_tzids[parm.split('TZID=')[-1]]
+ except KeyError:
+ continue
+ if tzids is None:
+ from . import tz
+ tzlookup = tz.gettz
+ elif callable(tzids):
+ tzlookup = tzids
+ else:
+ tzlookup = getattr(tzids, 'get', None)
+ if tzlookup is None:
+ msg = ('tzids must be a callable, mapping, or None, '
+ 'not %s' % tzids)
+ raise ValueError(msg)
+
+ TZID = tzlookup(tzkey)
+ continue
+
+ # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
+ # only once.
+ if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
+ raise ValueError("unsupported parm: " + parm)
+ else:
+ if value_found:
+ msg = ("Duplicate value parameter found in: " + parm)
+ raise ValueError(msg)
+ value_found = True
+
+ for datestr in date_value.split(','):
+ date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
+ if TZID is not None:
+ if date.tzinfo is None:
+ date = date.replace(tzinfo=TZID)
+ else:
+ raise ValueError('DTSTART/EXDATE specifies multiple timezone')
+ datevals.append(date)
+
+ return datevals
+
def _parse_rfc(self, s,
dtstart=None,
cache=False,
@@ -1577,54 +1683,18 @@ class _rrulestr(object):
raise ValueError("unsupported EXRULE parm: "+parm)
exrulevals.append(value)
elif name == "EXDATE":
- for parm in parms:
- if parm != "VALUE=DATE-TIME":
- raise ValueError("unsupported EXDATE parm: "+parm)
- exdatevals.append(value)
+ exdatevals.extend(
+ self._parse_date_value(value, parms,
+ TZID_NAMES, ignoretz,
+ tzids, tzinfos)
+ )
elif name == "DTSTART":
- # RFC 5445 3.8.2.4: The VALUE parameter is optional, but
- # may be found only once.
- value_found = False
- TZID = None
- valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"}
- for parm in parms:
- if parm.startswith("TZID="):
- try:
- tzkey = TZID_NAMES[parm.split('TZID=')[-1]]
- except KeyError:
- continue
- if tzids is None:
- from . import tz
- tzlookup = tz.gettz
- elif callable(tzids):
- tzlookup = tzids
- else:
- tzlookup = getattr(tzids, 'get', None)
- if tzlookup is None:
- msg = ('tzids must be a callable, ' +
- 'mapping, or None, ' +
- 'not %s' % tzids)
- raise ValueError(msg)
-
- TZID = tzlookup(tzkey)
- continue
- if parm not in valid_values:
- raise ValueError("unsupported DTSTART parm: "+parm)
- else:
- if value_found:
- msg = ("Duplicate value parameter found in " +
- "DTSTART: " + parm)
- raise ValueError(msg)
- value_found = True
- if not parser:
- from dateutil import parser
- dtstart = parser.parse(value, ignoretz=ignoretz,
- tzinfos=tzinfos)
- if TZID is not None:
- if dtstart.tzinfo is None:
- dtstart = dtstart.replace(tzinfo=TZID)
- else:
- raise ValueError('DTSTART specifies multiple timezones')
+ dtvals = self._parse_date_value(value, parms, TZID_NAMES,
+ ignoretz, tzids, tzinfos)
+ if len(dtvals) != 1:
+ raise ValueError("Multiple DTSTART values specified:" +
+ value)
+ dtstart = dtvals[0]
else:
raise ValueError("unsupported property: "+name)
if (forceset or len(rrulevals) > 1 or rdatevals
@@ -1646,10 +1716,7 @@ class _rrulestr(object):
ignoretz=ignoretz,
tzinfos=tzinfos))
for value in exdatevals:
- for datestr in value.split(','):
- rset.exdate(parser.parse(datestr,
- ignoretz=ignoretz,
- tzinfos=tzinfos))
+ rset.exdate(value)
if compatible and dtstart:
rset.rdate(dtstart)
return rset
diff --git a/dateutil/test/conftest.py b/dateutil/test/conftest.py
new file mode 100644
index 0000000..78ed70a
--- /dev/null
+++ b/dateutil/test/conftest.py
@@ -0,0 +1,41 @@
+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()
diff --git a/dateutil/test/property/test_isoparse_prop.py b/dateutil/test/property/test_isoparse_prop.py
new file mode 100644
index 0000000..c6a4b82
--- /dev/null
+++ b/dateutil/test/property/test_isoparse_prop.py
@@ -0,0 +1,27 @@
+from hypothesis import given, assume
+from hypothesis import strategies as st
+
+from dateutil import tz
+from dateutil.parser import isoparse
+
+import pytest
+
+# Strategies
+TIME_ZONE_STRATEGY = st.sampled_from([None, tz.tzutc()] +
+ [tz.gettz(zname) for zname in ('US/Eastern', 'US/Pacific',
+ 'Australia/Sydney', 'Europe/London')])
+ASCII_STRATEGY = st.characters(max_codepoint=127)
+
+
+@pytest.mark.isoparser
+@given(dt=st.datetimes(timezones=TIME_ZONE_STRATEGY), sep=ASCII_STRATEGY)
+def test_timespec_auto(dt, sep):
+ if dt.tzinfo is not None:
+ # Assume offset has no sub-second components
+ assume(dt.utcoffset().total_seconds() % 60 == 0)
+
+ sep = str(sep) # Python 2.7 requires bytes
+ dtstr = dt.isoformat(sep=sep)
+ dt_rt = isoparse(dtstr)
+
+ assert dt_rt == dt
diff --git a/dateutil/test/property/test_parser_prop.py b/dateutil/test/property/test_parser_prop.py
new file mode 100644
index 0000000..fdfd171
--- /dev/null
+++ b/dateutil/test/property/test_parser_prop.py
@@ -0,0 +1,22 @@
+from hypothesis.strategies import integers
+from hypothesis import given
+
+import pytest
+
+from dateutil.parser import parserinfo
+
+
+@pytest.mark.parserinfo
+@given(integers(min_value=100, max_value=9999))
+def test_convertyear(n):
+ assert n == parserinfo().convertyear(n)
+
+
+@pytest.mark.parserinfo
+@given(integers(min_value=-50,
+ max_value=49))
+def test_convertyear_no_specified_century(n):
+ p = parserinfo()
+ new_year = p._year + n
+ result = p.convertyear(new_year % 100, century_specified=False)
+ assert result == new_year
diff --git a/dateutil/test/test_easter.py b/dateutil/test/test_easter.py
index eeb094e..cf2ec7f 100644
--- a/dateutil/test/test_easter.py
+++ b/dateutil/test/test_easter.py
@@ -2,7 +2,7 @@ from dateutil.easter import easter
from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN
from datetime import date
-import unittest
+import pytest
# List of easters between 1990 and 2050
western_easter_dates = [
@@ -73,23 +73,21 @@ julian_easter_dates = [
]
-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)
diff --git a/dateutil/test/test_import_star.py b/dateutil/test/test_import_star.py
index 8e66f38..2fb7098 100644
--- a/dateutil/test/test_import_star.py
+++ b/dateutil/test/test_import_star.py
@@ -1,8 +1,8 @@
"""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
prev_locals = list(locals())
from dateutil import *
@@ -10,24 +10,24 @@ new_locals = {name:value for name,value in locals().items()
if name not in prev_locals}
new_locals.pop('prev_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
diff --git a/dateutil/test/test_isoparser.py b/dateutil/test/test_isoparser.py
index 28c1bf7..ecd6e84 100644
--- a/dateutil/test/test_isoparser.py
+++ b/dateutil/test/test_isoparser.py
@@ -120,7 +120,8 @@ def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset):
DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)]
@pytest.mark.parametrize('dt', tuple(DATETIMES))
@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)
@pytest.mark.parametrize('precision', list(range(3, 7)))
def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision):
@@ -129,6 +130,15 @@ def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision):
_isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision)
+###
+# 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)
def test_full_tzoffsets(tzoffset):
dt = datetime(2017, 11, 27, 6, 14, 30, 123456)
@@ -139,15 +149,15 @@ def test_full_tzoffsets(tzoffset):
@pytest.mark.parametrize('dt_str', [
'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']
)
def test_datetime_midnight(dt_str):
assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0)
@@ -218,6 +228,8 @@ def test_iso_ordinal(isoord, dt_expected):
(b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)),
(b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000,
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',
datetime(2014, 2, 4, 12, 30, 15, 224000,
tzinfo=tz.tzoffset(None, timedelta(hours=5))))])
@@ -263,12 +275,15 @@ def test_iso_raises(isostr, exception):
isoparse(isostr)
-@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)
@pytest.mark.xfail()
@@ -367,7 +382,7 @@ def test_parse_isodate(d, dt_fmt, as_bytes):
d_str = d.strftime(dt_fmt)
if isinstance(d_str, six.text_type) and as_bytes:
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')
iparser = isoparser()
@@ -441,29 +456,48 @@ def test_isotime(time_val, time_fmt, as_bytes):
tstr = time_val.strftime(time_fmt)
if isinstance(time_val, six.text_type) and as_bytes:
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')
iparser = isoparser()
assert iparser.parse_isotime(tstr) == time_val
+
+@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', [
('3', ValueError), # ISO string too short
('14時30分15秒', ValueError), # Not ASCII
('14_30_15', ValueError), # Invalid separators
('1430:15', ValueError), # Inconsistent separator use
- ('14:30:15.3684000309', ValueError), # Too much us precision
('25', ValueError), # Invalid hours
('25:15', ValueError), # Invalid hours
('14:60', ValueError), # Invalid minutes
('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
('14:30:15+1234567', ValueError), # Time zone invalid
('14:59:59+25:00', ValueError), # Invalid tz hours
('14:59:59+12:62', ValueError), # Invalid tz minutes
('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
])
def test_isotime_raises(isostr, exception):
iparser = isoparser()
diff --git a/dateutil/test/test_parser.py b/dateutil/test/test_parser.py
index 3c7b403..dcaa7cc 100644
--- a/dateutil/test/test_parser.py
+++ b/dateutil/test/test_parser.py
@@ -14,7 +14,7 @@ from dateutil.parser import UnknownTimezoneWarning
from ._common import TZEnvContext
from six import assertRaisesRegex, PY3
-from six.moves import StringIO
+from io import StringIO
import pytest
@@ -27,6 +27,130 @@ try:
except ValueError:
PLATFORM_HAS_DASH_D = False
+# 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):
@@ -56,30 +180,20 @@ class TestFormat(unittest.TestCase):
self.assertEqual(res, actual)
-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
# interface.
@@ -92,24 +206,48 @@ class ParserTest(unittest.TestCase):
dstr = StringPassThrough(StringIO('2014 January 19'))
- 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 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()
+
+ res = parse(bytes_str)
+ expected = parse(uni_str)
+ assert res == expected
+
+ 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
- def testParseStr(self):
- self.assertEqual(parse(self.str_str),
- parse(self.uni_str))
- def testParseBytes(self):
- self.assertEqual(parse(b'2014 January 19'), datetime(2014, 1, 19))
+class ParserTest(unittest.TestCase):
- def testParseBytearray(self):
- # GH #417
- self.assertEqual(parse(bytearray(b'2014 January 19')),
- datetime(2014, 1, 19))
+ 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):
from dateutil.parser import parser
@@ -176,50 +314,6 @@ class ParserTest(unittest.TestCase):
ignoretz=True),
datetime(2003, 9, 25, 10, 36, 28))
- 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):
self.assertEqual(parse("Thu, 25 Sep 2003 10:49:41 -0300"),
datetime(2003, 9, 25, 10, 49, 41,
@@ -236,20 +330,9 @@ class ParserTest(unittest.TestCase):
tzinfo=self.brsttz))
def testISOFormatStrip2(self):
- self.assertEqual(parse("2003-09-25T10:49:41"),
- datetime(2003, 9, 25, 10, 49, 41))
-
- 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))
+ self.assertEqual(parse("2003-09-25T10:49:41+03:00"),
+ datetime(2003, 9, 25, 10, 49, 41,
+ tzinfo=tzoffset(None, 10800)))
def testISOStrippedFormat(self):
self.assertEqual(parse("20030925T104941.5-0300"),
@@ -262,197 +345,42 @@ class ParserTest(unittest.TestCase):
tzinfo=self.brsttz))
def testISOStrippedFormatStrip2(self):
- self.assertEqual(parse("20030925T104941"),
- datetime(2003, 9, 25, 10, 49, 41))
-
- 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))
+ self.assertEqual(parse("20030925T104941+0300"),
+ datetime(2003, 9, 25, 10, 49, 41,
+ tzinfo=tzoffset(None, 10800)))
def testDateWithDash8(self):
self.assertEqual(parse("10-09-2003", dayfirst=True),
datetime(2003, 9, 10))
- 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):
self.assertEqual(parse("10-09-03", yearfirst=True),
datetime(2010, 9, 3))
- 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):
self.assertEqual(parse("10.09.2003", dayfirst=True),
datetime(2003, 9, 10))
- 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):
self.assertEqual(parse("10.09.03", yearfirst=True),
datetime(2010, 9, 3))
- 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):
self.assertEqual(parse("10/09/2003", dayfirst=True),
datetime(2003, 9, 10))
- 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):
self.assertEqual(parse("10/09/03", yearfirst=True),
datetime(2010, 9, 3))
- 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):
self.assertEqual(parse("10 09 2003", dayfirst=True),
datetime(2003, 9, 10))
- 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):
self.assertEqual(parse("10 09 03", yearfirst=True),
datetime(2010, 9, 3))
- 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):
with self.assertRaises(ValueError):
parse("AM")
@@ -460,54 +388,6 @@ class ParserTest(unittest.TestCase):
with self.assertRaises(ValueError):
parse("Jan 20, 2015 PM")
- 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):
with self.assertRaises(ValueError):
parse("13:44 AM")
@@ -521,22 +401,6 @@ class ParserTest(unittest.TestCase):
self.assertEqual(parse("Sep of 03", default=self.default),
datetime(2003, 9, 25))
- 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):
s = "Today is 25 of September of 2003, exactly " \
"at 10:49:41 with timezone -03:00."
@@ -579,23 +443,11 @@ class ParserTest(unittest.TestCase):
res = parse(s1, fuzzy=True)
self.assertEqual(res, datetime(1945, 1, 29, 14, 45))
- 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):
self.assertEqual(parse("1996.07.10 AD at 15:08:56 PDT",
ignoretz=True),
datetime(1996, 7, 10, 15, 8, 56))
- def testRandomFormat3(self):
- self.assertEqual(parse("1996.July.10 AD 12:08 PM"),
- datetime(1996, 7, 10, 12, 8))
-
def testRandomFormat4(self):
self.assertEqual(parse("Tuesday, April 12, 1952 AD 3:30:42pm PST",
ignoretz=True),
@@ -616,121 +468,30 @@ class ParserTest(unittest.TestCase):
ignoretz=True),
datetime(1994, 11, 5, 8, 15, 30))
- 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):
self.assertEqual(parse("1976-07-04T00:01:02Z", ignoretz=True),
datetime(1976, 7, 4, 0, 1, 2))
def testRandomFormat18(self):
- self.assertEqual(parse("July 4, 1976 12:01:02 am"),
- datetime(1976, 7, 4, 0, 1, 2))
-
- def testRandomFormat19(self):
- self.assertEqual(parse("Mon Jan 2 04:24:27 1995"),
- datetime(1995, 1, 2, 4, 24, 27))
+ self.assertEqual(parse("1986-07-05T08:15:30z",
+ ignoretz=True),
+ datetime(1986, 7, 5, 8, 15, 30))
def testRandomFormat20(self):
self.assertEqual(parse("Tue Apr 4 00:22:12 PDT 1995", ignoretz=True),
datetime(1995, 4, 4, 0, 22, 12))
- 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):
self.assertEqual(parse("0:00 PM, PST", default=self.default,
ignoretz=True),
datetime(2003, 9, 25, 12, 0))
- def testRandomFormat25(self):
- self.assertEqual(parse("12:08 PM", default=self.default),
- datetime(2003, 9, 25, 12, 8))
-
def testRandomFormat26(self):
with pytest.warns(UnknownTimezoneWarning):
res = parse("5:50 A.M. on June 13, 1990")
self.assertEqual(res, datetime(1990, 6, 13, 5, 50))
- 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):
with self.assertRaises(ValueError):
parse("Feb 30, 2007")
@@ -748,6 +509,14 @@ class ParserTest(unittest.TestCase):
self.assertEqual(parse("Feb 2008", default=datetime(2010, 1, 31)),
datetime(2008, 2, 29))
+ def testTzinfoDictionaryCouldReturnNone(self):
+ self.assertEqual(parse('2017-02-03 12:40 BRST', tzinfos={"BRST": None}),
+ datetime(2017, 2, 3, 12, 40))
+
+ def testTzinfosCallableCouldReturnNone(self):
+ self.assertEqual(parse('2017-02-03 12:40 BRST', tzinfos=lambda *args: None),
+ datetime(2017, 2, 3, 12, 40))
+
def testErrorType01(self):
self.assertRaises(ValueError,
parse, 'shouldfail')
@@ -795,10 +564,6 @@ class ParserTest(unittest.TestCase):
dt = datetime(2008, 2, 27, 21, 26, 1, ms)
self.assertEqual(parse(dt.isoformat()), dt)
- def testHighPrecisionSeconds(self):
- self.assertEqual(parse("20080227T21:26:01.123456789"),
- datetime(2008, 2, 27, 21, 26, 1, 123456))
-
def testCustomParserInfo(self):
# Custom parser info wasn't working, as Michael Elsdörfer discovered.
from dateutil.parser import parserinfo, parser
@@ -893,12 +658,6 @@ class ParserTest(unittest.TestCase):
res = parse(dtstr, fuzzy=True)
self.assertEqual(res, datetime(2017, 7, 17, 6, 15))
- def test_dBY(self):
- # See GH360
- dtstr = '13NOV2017'
- res = parse(dtstr)
- self.assertEqual(res, datetime(2017, 11, 13))
-
def test_hmBY(self):
# See GH#483
dtstr = '02:17NOV2017'
@@ -916,11 +675,6 @@ class ParserTest(unittest.TestCase):
res = parse(dstr)
assert res.year == 2001, res
- 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):
@pytest.mark.xfail
@@ -1029,6 +783,7 @@ class TestParseUnimplementedCases(object):
expected = datetime(2017, 12, 1)
assert res == expected
+
@pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var')
def test_parse_unambiguous_nonexistent_local():
# When dates are specified "EST" even when they should be "EDT" in the
@@ -1093,9 +848,9 @@ def test_rounding_floatlike_strings(dtstr, dt):
assert parse(dtstr, default=datetime(2003, 9, 25)) == dt
-def test_decimal_error():
- # GH 632 - decimal.Decimal raises some non-ValueError exception when
+@pytest.mark.parametrize('value', ['1: test', 'Nan'])
+def test_decimal_error(value):
+ # GH 632, GH 662 - decimal.Decimal raises some non-ValueError exception when
# constructed with an invalid value
with pytest.raises(ValueError):
- parse('1: test')
-
+ parse(value)
diff --git a/dateutil/test/test_relativedelta.py b/dateutil/test/test_relativedelta.py
index 70cb543..89cc808 100644
--- a/dateutil/test/test_relativedelta.py
+++ b/dateutil/test/test_relativedelta.py
@@ -125,6 +125,14 @@ class RelativeDeltaTest(WarningTestMixin, unittest.TestCase):
self.assertEqual(self.today+relativedelta(days=+1, weekday=WE),
date(2003, 9, 24))
+ 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):
self.assertEqual(date(2003, 1, 1) +
relativedelta(day=4, weeks=+14, weekday=MO(-1)),
@@ -350,6 +358,16 @@ class RelativeDeltaTest(WarningTestMixin, unittest.TestCase):
with self.assertRaises(ValueError):
relativedelta(months=1.5)
+ 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):
# Fractional absolute values will soon be unsupported,
# check for the deprecation warning.
diff --git a/dateutil/test/test_rrule.py b/dateutil/test/test_rrule.py
index 9ac948d..9dfa544 100644
--- a/dateutil/test/test_rrule.py
+++ b/dateutil/test/test_rrule.py
@@ -14,7 +14,12 @@ from dateutil.rrule import (
MO, TU, WE, TH, FR, SA, SU
)
+from freezegun import freeze_time
+import pytest
+
+
+@pytest.mark.rrule
class RRuleTest(WarningTestMixin, unittest.TestCase):
def _rrulestr_reverse_test(self, rule):
"""
@@ -2848,6 +2853,74 @@ class RRuleTest(WarningTestMixin, unittest.TestCase):
datetime(1997, 9, 9, 9, 0),
datetime(1997, 9, 16, 9, 0)])
+ def testStrSetExDateMultiple(self):
+ rrstr = ("DTSTART:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n"
+ "EXDATE:19970904T090000,19970911T090000,19970918T090000\n")
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 2, 9, 0),
+ datetime(1997, 9, 9, 9, 0),
+ datetime(1997, 9, 16, 9, 0)]
+
+ def testStrSetExDateWithTZID(self):
+ BXL = tz.gettz('Europe/Brussels')
+ rr = rrulestr("DTSTART;TZID=Europe/Brussels:19970902T090000\n"
+ "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n"
+ "EXDATE;TZID=Europe/Brussels:19970904T090000\n"
+ "EXDATE;TZID=Europe/Brussels:19970911T090000\n"
+ "EXDATE;TZID=Europe/Brussels:19970918T090000\n")
+
+ assert list(rr) == [datetime(1997, 9, 2, 9, 0, tzinfo=BXL),
+ datetime(1997, 9, 9, 9, 0, tzinfo=BXL),
+ datetime(1997, 9, 16, 9, 0, tzinfo=BXL)]
+
+ def testStrSetExDateValueDateTimeNoTZID(self):
+ rrstr = '\n'.join([
+ "DTSTART:19970902T090000",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE-TIME:19970902T090000",
+ "EXDATE;VALUE=DATE-TIME:19970909T090000",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4, 9), datetime(1997, 9, 11, 9)]
+
+ def testStrSetExDateValueMixDateTimeNoTZID(self):
+ rrstr = '\n'.join([
+ "DTSTART:19970902T090000",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE-TIME:19970902T090000",
+ "EXDATE:19970909T090000",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4, 9), datetime(1997, 9, 11, 9)]
+
+ def testStrSetExDateValueDateTimeWithTZID(self):
+ BXL = tz.gettz('Europe/Brussels')
+ rrstr = '\n'.join([
+ "DTSTART;VALUE=DATE-TIME;TZID=Europe/Brussels:19970902T090000",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE-TIME;TZID=Europe/Brussels:19970902T090000",
+ "EXDATE;VALUE=DATE-TIME;TZID=Europe/Brussels:19970909T090000",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4, 9, tzinfo=BXL),
+ datetime(1997, 9, 11, 9, tzinfo=BXL)]
+
+ def testStrSetExDateValueDate(self):
+ rrstr = '\n'.join([
+ "DTSTART;VALUE=DATE:19970902",
+ "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH",
+ "EXDATE;VALUE=DATE:19970902",
+ "EXDATE;VALUE=DATE:19970909",
+ ])
+
+ rr = rrulestr(rrstr)
+ assert list(rr) == [datetime(1997, 9, 4), datetime(1997, 9, 11)]
+
def testStrSetDateAndExDate(self):
self.assertEqual(list(rrulestr(
"DTSTART:19970902T090000\n"
@@ -2924,6 +2997,11 @@ class RRuleTest(WarningTestMixin, unittest.TestCase):
self.assertEqual(list(rr), [datetime(1997, 9, 2, 0, 0, 0),
datetime(1998, 9, 2, 0, 0, 0)])
+ def testStrMultipleDTStartComma(self):
+ with pytest.raises(ValueError):
+ rr = rrulestr("DTSTART:19970101T000000,19970202T000000\n"
+ "RRULE:FREQ=YEARLY;COUNT=1")
+
def testStrInvalidUntil(self):
with self.assertRaises(ValueError):
list(rrulestr("DTSTART:19970902T090000\n"
@@ -4538,6 +4616,31 @@ class RRuleTest(WarningTestMixin, unittest.TestCase):
[datetime(1997, 1, 6)])
+@pytest.mark.rrule
+@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC))
+def test_generated_aware_dtstart():
+ dtstart_exp = datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC)
+ UNTIL = datetime(2018, 3, 6, 8, 0, tzinfo=tz.UTC)
+
+ rule_without_dtstart = rrule(freq=HOURLY, until=UNTIL)
+ rule_with_dtstart = rrule(freq=HOURLY, dtstart=dtstart_exp, until=UNTIL)
+ assert list(rule_without_dtstart) == list(rule_with_dtstart)
+
+
+@pytest.mark.rrule
+@pytest.mark.rrulestr
+@pytest.mark.xfail(reason="rrulestr loses time zone, gh issue #637")
+@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC))
+def test_generated_aware_dtstart_rrulestr():
+ rrule_without_dtstart = rrule(freq=HOURLY,
+ until=datetime(2018, 3, 6, 8, 0,
+ tzinfo=tz.UTC))
+ rrule_r = rrulestr(str(rrule_without_dtstart))
+
+ assert list(rrule_r) == list(rrule_without_dtstart)
+
+
+@pytest.mark.rruleset
class RRuleSetTest(unittest.TestCase):
def testSet(self):
rrset = rruleset()
diff --git a/dateutil/test/test_tz.py b/dateutil/test/test_tz.py
index fd7e47d..bb0f4b7 100644
--- a/dateutil/test/test_tz.py
+++ b/dateutil/test/test_tz.py
@@ -8,12 +8,15 @@ from ._common import ComparesEqual
from datetime import datetime, timedelta
from datetime import time as dt_time
from datetime import tzinfo
-from six import BytesIO, StringIO
+from six import PY2
+from io import BytesIO, StringIO
import unittest
import sys
import base64
import copy
+import gc
+import weakref
from functools import partial
@@ -154,6 +157,8 @@ END:VTIMEZONE
EST_TUPLE = ('EST', timedelta(hours=-5), timedelta(hours=0))
EDT_TUPLE = ('EDT', timedelta(hours=-4), timedelta(hours=1))
+SUPPORTS_SUB_MINUTE_OFFSETS = sys.version_info >= (3, 6)
+
###
# Helper functions
@@ -731,6 +736,28 @@ class TzOffsetTest(unittest.TestCase):
assert tz1 is tz2
+
+@pytest.mark.smoke
+@pytest.mark.tzoffset
+def test_tzoffset_weakref():
+ UTC1 = tz.tzoffset('UTC', 0)
+ UTC_ref = weakref.ref(tz.tzoffset('UTC', 0))
+ UTC1 is UTC_ref()
+ del UTC1
+ gc.collect()
+
+ assert UTC_ref() is not None # Should be in the strong cache
+ assert UTC_ref() is tz.tzoffset('UTC', 0)
+
+ # Fill the strong cache with other items
+ for offset in range(5,15):
+ tz.tzoffset('RandomZone', offset)
+
+ gc.collect()
+ assert UTC_ref() is None
+ assert UTC_ref() is not tz.tzoffset('UTC', 0)
+
+
@pytest.mark.tzoffset
@pytest.mark.parametrize('args', [
('UTC', 0),
@@ -744,6 +771,25 @@ def test_tzoffset_singleton(args):
assert tz1 is tz2
+
+@pytest.mark.tzoffset
+@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS,
+ reason='Sub-minute offsets not supported')
+def test_tzoffset_sub_minute():
+ delta = timedelta(hours=12, seconds=30)
+ test_datetime = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta))
+ assert test_datetime.utcoffset() == delta
+
+
+@pytest.mark.tzoffset
+@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS,
+ reason='Sub-minute offsets supported')
+def test_tzoffset_sub_minute_rounding():
+ delta = timedelta(hours=12, seconds=30)
+ test_date = datetime(2000, 1, 1, tzinfo=tz.tzoffset(None, delta))
+ assert test_date.utcoffset() == timedelta(hours=12, minutes=1)
+
+
@pytest.mark.tzlocal
class TzLocalTest(unittest.TestCase):
def testEquality(self):
@@ -1037,6 +1083,25 @@ class GettzTest(unittest.TestCase, TzFoldMixin):
assert local1 is not local2
+
+@pytest.mark.gettz
+@pytest.mark.parametrize('badzone', [
+ 'Fake.Region/Abcdefghijklmnop', # Violates several tz project name rules
+])
+def test_gettz_badzone(badzone):
+ # Make sure passing a bad TZ string to gettz returns None (GH #800)
+ tzi = tz.gettz(badzone)
+ assert tzi is None
+
+
+@pytest.mark.gettz
+def test_gettz_badzone_unicode():
+ # Make sure a unicode string can be passed to TZ (GH #802)
+ # When fixed, combine this with test_gettz_badzone
+ tzi = tz.gettz('🐼')
+ assert tzi is None
+
+
@pytest.mark.gettz
@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached')
def test_gettz_cache_clear():
@@ -1047,6 +1112,52 @@ def test_gettz_cache_clear():
assert NYC1 is not NYC2
+@pytest.mark.gettz
+@pytest.mark.xfail(IS_WIN, reason='zoneinfo separately cached')
+def test_gettz_set_cache_size():
+ tz.gettz.cache_clear()
+ tz.gettz.set_cache_size(3)
+
+ MONACO_ref = weakref.ref(tz.gettz('Europe/Monaco'))
+ EASTER_ref = weakref.ref(tz.gettz('Pacific/Easter'))
+ CURRIE_ref = weakref.ref(tz.gettz('Australia/Currie'))
+
+ gc.collect()
+
+ assert MONACO_ref() is not None
+ assert EASTER_ref() is not None
+ assert CURRIE_ref() is not None
+
+ tz.gettz.set_cache_size(2)
+ gc.collect()
+
+ assert MONACO_ref() is None
+
+@pytest.mark.xfail(IS_WIN, reason="Windows does not use system zoneinfo")
+@pytest.mark.smoke
+@pytest.mark.gettz
+def test_gettz_weakref():
+ tz.gettz.cache_clear()
+ tz.gettz.set_cache_size(2)
+ NYC1 = tz.gettz('America/New_York')
+ NYC_ref = weakref.ref(tz.gettz('America/New_York'))
+
+ assert NYC1 is NYC_ref()
+
+ del NYC1
+ gc.collect()
+
+ assert NYC_ref() is not None # Should still be in the strong cache
+ assert tz.gettz('America/New_York') is NYC_ref()
+
+ # Populate strong cache with other timezones
+ tz.gettz('Europe/Monaco')
+ tz.gettz('Pacific/Easter')
+ tz.gettz('Australia/Currie')
+
+ gc.collect()
+ assert NYC_ref() is None # Should have been pushed out
+ assert tz.gettz('America/New_York') is not NYC_ref()
class ZoneInfoGettzTest(GettzTest, WarningTestMixin):
def gettz(self, name):
@@ -1375,6 +1486,28 @@ class TZStrTest(unittest.TestCase, TzFoldMixin):
# Ensure that these still are all the same zone
assert tz1 == tz2 == tz3
+
+@pytest.mark.smoke
+@pytest.mark.tzstr
+def test_tzstr_weakref():
+ tz_t1 = tz.tzstr('EST5EDT')
+ tz_t2_ref = weakref.ref(tz.tzstr('EST5EDT'))
+ assert tz_t1 is tz_t2_ref()
+
+ del tz_t1
+ gc.collect()
+
+ assert tz_t2_ref() is not None
+ assert tz.tzstr('EST5EDT') is tz_t2_ref()
+
+ for offset in range(5,15):
+ tz.tzstr('GMT+{}'.format(offset))
+ gc.collect()
+
+ assert tz_t2_ref() is None
+ assert tz.tzstr('EST5EDT') is not tz_t2_ref()
+
+
@pytest.mark.tzstr
@pytest.mark.parametrize('tz_str,expected', [
# From https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
@@ -1851,11 +1984,13 @@ class TZTest(unittest.TestCase):
with self.assertRaises(ValueError):
tz.tzfile(BytesIO(b'BadFile'))
- def testRoundNonFullMinutes(self):
- # This timezone has an offset of 5992 seconds in 1900-01-01.
- tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
- self.assertEqual(str(datetime(1900, 1, 1, 0, 0, tzinfo=tzc)),
- "1900-01-01 00:00:00+01:40")
+ def testFilestreamWithNameRepr(self):
+ # If fileobj is a filestream with a "name" attribute this name should
+ # be reflected in the tz object's repr
+ fileobj = BytesIO(base64.b64decode(TZFILE_EST5EDT))
+ fileobj.name = 'foo'
+ tzc = tz.tzfile(fileobj)
+ self.assertEqual(repr(tzc), 'tzfile(' + repr('foo') + ')')
def testLeapCountDecodesProperly(self):
# This timezone has leapcnt, and failed to decode until
@@ -1917,6 +2052,40 @@ class TZTest(unittest.TestCase):
self.assertEqual(str(dt), '2014-07-20 12:34:56+00:00')
+@pytest.mark.tzfile
+@pytest.mark.skipif(not SUPPORTS_SUB_MINUTE_OFFSETS,
+ reason='Sub-minute offsets not supported')
+def test_tzfile_sub_minute_offset():
+ # If user running python 3.6 or newer, exact offset is used
+ tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
+ offset = timedelta(hours=1, minutes=39, seconds=52)
+ assert datetime(1900, 1, 1, 0, 0, tzinfo=tzc).utcoffset() == offset
+
+
+@pytest.mark.tzfile
+@pytest.mark.skipif(SUPPORTS_SUB_MINUTE_OFFSETS,
+ reason='Sub-minute offsets supported.')
+def test_sub_minute_rounding_tzfile():
+ # This timezone has an offset of 5992 seconds in 1900-01-01.
+ # For python version pre-3.6, this will be rounded
+ tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI)))
+ offset = timedelta(hours=1, minutes=40)
+ assert datetime(1900, 1, 1, 0, 0, tzinfo=tzc).utcoffset() == offset
+
+
+@pytest.mark.tzfile
+def test_samoa_transition():
+ # utcoffset() was erroneously returning +14:00 an hour early (GH #812)
+ APIA = tz.gettz('Pacific/Apia')
+ dt = datetime(2011, 12, 29, 23, 59, tzinfo=APIA)
+ assert dt.utcoffset() == timedelta(hours=-10)
+
+ # Make sure the transition actually works, too
+ dt_after = (dt.astimezone(tz.UTC) + timedelta(minutes=1)).astimezone(APIA)
+ assert dt_after == datetime(2011, 12, 31, tzinfo=APIA)
+ assert dt_after.utcoffset() == timedelta(hours=14)
+
+
@unittest.skipUnless(IS_WIN, "Requires Windows")
class TzWinTest(unittest.TestCase, TzWinFoldMixin):
def setUp(self):
@@ -2464,22 +2633,44 @@ class DatetimeExistsTest(unittest.TestCase):
self.assertFalse(tz.datetime_exists(dt, tz=AEST))
-class EnfoldTest(unittest.TestCase):
- def testEnterFoldDefault(self):
+class TestEnfold:
+ def test_enter_fold_default(self):
dt = tz.enfold(datetime(2020, 1, 19, 3, 32))
- self.assertEqual(dt.fold, 1)
+ assert dt.fold == 1
- def testEnterFold(self):
+ def test_enter_fold(self):
dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1)
- self.assertEqual(dt.fold, 1)
+ assert dt.fold == 1
- def testExitFold(self):
+ def test_exit_fold(self):
dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=0)
# Before Python 3.6, dt.fold won't exist if fold is 0.
- self.assertEqual(getattr(dt, 'fold', 0), 0)
+ assert getattr(dt, 'fold', 0) == 0
+
+ def test_defold(self):
+ dt = tz.enfold(datetime(2020, 1, 19, 3, 32), fold=1)
+
+ dt2 = tz.enfold(dt, fold=0)
+
+ assert getattr(dt2, 'fold', 0) == 0
+
+ def test_fold_replace_args(self):
+ # This test can be dropped when Python < 3.6 is dropped, since it
+ # is mainly to cover the `replace` method on _DatetimeWithFold
+ dt = tz.enfold(datetime(1950, 1, 2, 12, 30, 15, 8), fold=1)
+
+ dt2 = dt.replace(1952, 2, 3, 13, 31, 16, 9)
+ assert dt2 == tz.enfold(datetime(1952, 2, 3, 13, 31, 16, 9), fold=1)
+ assert dt2.fold == 1
+
+ def test_fold_replace_exception_duplicate_args(self):
+ dt = tz.enfold(datetime(1999, 1, 3), fold=1)
+
+ with pytest.raises(TypeError):
+ dt.replace(1950, year=2000)
@pytest.mark.tz_resolve_imaginary
@@ -2561,8 +2752,7 @@ def __get_kiritimati_resolve_imaginary_test():
return (tzi, ) + dates
-@pytest.mark.tz_resolve_imaginary
-@pytest.mark.parametrize('tzi, dt, dt_exp', [
+resolve_imaginary_tests = [
(tz.gettz('Europe/London'),
datetime(2018, 3, 25, 1, 30), datetime(2018, 3, 25, 2, 30)),
(tz.gettz('America/New_York'),
@@ -2570,24 +2760,20 @@ def __get_kiritimati_resolve_imaginary_test():
(tz.gettz('Australia/Sydney'),
datetime(2014, 10, 5, 2, 0), datetime(2014, 10, 5, 3, 0)),
__get_kiritimati_resolve_imaginary_test(),
-])
-def test_resolve_imaginary(tzi, dt, dt_exp):
- dt = dt.replace(tzinfo=tzi)
- dt_exp = dt_exp.replace(tzinfo=tzi)
+]
- dt_r = tz.resolve_imaginary(dt)
- assert dt_r == dt_exp
- assert dt_r.tzname() == dt_exp.tzname()
- assert dt_r.utcoffset() == dt_exp.utcoffset()
+
+if SUPPORTS_SUB_MINUTE_OFFSETS:
+ resolve_imaginary_tests.append(
+ (tz.gettz('Africa/Monrovia'),
+ datetime(1972, 1, 7, 0, 30), datetime(1972, 1, 7, 1, 14, 30)))
-@pytest.mark.xfail
@pytest.mark.tz_resolve_imaginary
-def test_resolve_imaginary_monrovia():
- # See GH #582 - When that is resolved, move this into test_resolve_imaginary
- tzi = tz.gettz('Africa/Monrovia')
- dt = datetime(1972, 1, 7, hour=0, minute=30, second=0, tzinfo=tzi)
- dt_exp = datetime(1972, 1, 7, hour=1, minute=14, second=30, tzinfo=tzi)
+@pytest.mark.parametrize('tzi, dt, dt_exp', resolve_imaginary_tests)
+def test_resolve_imaginary(tzi, dt, dt_exp):
+ dt = dt.replace(tzinfo=tzi)
+ dt_exp = dt_exp.replace(tzinfo=tzi)
dt_r = tz.resolve_imaginary(dt)
assert dt_r == dt_exp
diff --git a/dateutil/tz/__init__.py b/dateutil/tz/__init__.py
index df03deb..5a2d9cd 100644
--- a/dateutil/tz/__init__.py
+++ b/dateutil/tz/__init__.py
@@ -1,4 +1,6 @@
+# -*- coding: utf-8 -*-
from .tz import *
+from .tz import __doc__
#: Convenience constant providing a :class:`tzutc()` instance
#:
diff --git a/dateutil/tz/_common.py b/dateutil/tz/_common.py
index ccabb7d..594e082 100644
--- a/dateutil/tz/_common.py
+++ b/dateutil/tz/_common.py
@@ -1,4 +1,4 @@
-from six import PY3
+from six import PY2
from functools import wraps
@@ -16,14 +16,18 @@ def tzname_in_python2(namefunc):
tzname() API changed in Python 3. It used to return bytes, but was changed
to unicode strings
"""
- def adjust_encoding(*args, **kwargs):
- name = namefunc(*args, **kwargs)
- if name is not None and not PY3:
- name = name.encode()
-
- return name
-
- return adjust_encoding
+ if PY2:
+ @wraps(namefunc)
+ def adjust_encoding(*args, **kwargs):
+ name = namefunc(*args, **kwargs)
+ if name is not None:
+ name = name.encode()
+
+ return name
+
+ return adjust_encoding
+ else:
+ return namefunc
# The following is adapted from Alexander Belopolsky's tz library
diff --git a/dateutil/tz/_factories.py b/dateutil/tz/_factories.py
index de2e0c1..d2560eb 100644
--- a/dateutil/tz/_factories.py
+++ b/dateutil/tz/_factories.py
@@ -1,4 +1,6 @@
from datetime import timedelta
+import weakref
+from collections import OrderedDict
class _TzSingleton(type):
@@ -11,6 +13,7 @@ class _TzSingleton(type):
cls.__instance = super(_TzSingleton, cls).__call__()
return cls.__instance
+
class _TzFactory(type):
def instance(cls, *args, **kwargs):
"""Alternate constructor that returns a fresh instance"""
@@ -19,7 +22,9 @@ class _TzFactory(type):
class _TzOffsetFactory(_TzFactory):
def __init__(cls, *args, **kwargs):
- cls.__instances = {}
+ cls.__instances = weakref.WeakValueDictionary()
+ cls.__strong_cache = OrderedDict()
+ cls.__strong_cache_size = 8
def __call__(cls, name, offset):
if isinstance(offset, timedelta):
@@ -31,12 +36,22 @@ class _TzOffsetFactory(_TzFactory):
if instance is None:
instance = cls.__instances.setdefault(key,
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
class _TzStrFactory(_TzFactory):
def __init__(cls, *args, **kwargs):
- cls.__instances = {}
+ cls.__instances = weakref.WeakValueDictionary()
+ cls.__strong_cache = OrderedDict()
+ cls.__strong_cache_size = 8
def __call__(cls, s, posix_offset=False):
key = (s, posix_offset)
@@ -45,5 +60,14 @@ class _TzStrFactory(_TzFactory):
if instance is None:
instance = cls.__instances.setdefault(key,
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
diff --git a/dateutil/tz/tz.py b/dateutil/tz/tz.py
index bfb4b47..d05414e 100644
--- a/dateutil/tz/tz.py
+++ b/dateutil/tz/tz.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
"""
This module offers timezone implementations subclassing the abstract
-:py:`datetime.tzinfo` type. There are classes to handle tzfile format files
-(usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, etc), TZ
-environment string (in all known formats), given ranges (with help from
-relative deltas), local machine timezone, fixed offset timezone, and UTC
+:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format
+files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`,
+etc), TZ environment string (in all known formats), given ranges (with help
+from relative deltas), local machine timezone, fixed offset timezone, and UTC
timezone.
"""
import datetime
@@ -13,6 +13,8 @@ import time
import sys
import os
import bisect
+import weakref
+from collections import OrderedDict
import six
from six import string_types
@@ -28,6 +30,9 @@ try:
except ImportError:
tzwin = tzwinlocal = None
+# For warning about rounding tzinfo
+from warnings import warn
+
ZERO = datetime.timedelta(0)
EPOCH = datetime.datetime.utcfromtimestamp(0)
EPOCHORDINAL = EPOCH.toordinal()
@@ -137,7 +142,8 @@ class tzoffset(datetime.tzinfo):
offset = offset.total_seconds()
except (TypeError, AttributeError):
pass
- self._offset = datetime.timedelta(seconds=offset)
+
+ self._offset = datetime.timedelta(seconds=_get_supported_offset(offset))
def utcoffset(self, dt):
return self._offset
@@ -387,10 +393,60 @@ class tzfile(_tzinfo):
``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
See `Sources for Time Zone and Daylight Saving Time Data
- <https://data.iana.org/time-zones/tz-link.html>`_ for more information. Time
- zone files can be compiled from the `IANA Time Zone database files
+ <https://data.iana.org/time-zones/tz-link.html>`_ for more information.
+ Time zone files can be compiled from the `IANA Time Zone database files
<https://www.iana.org/time-zones>`_ with the `zic time zone compiler
<https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_
+
+ .. note::
+
+ Only construct a ``tzfile`` directly if you have a specific timezone
+ file on disk that you want to read into a Python ``tzinfo`` object.
+ If you want to get a ``tzfile`` representing a specific IANA zone,
+ (e.g. ``'America/New_York'``), you should call
+ :func:`dateutil.tz.gettz` with the zone identifier.
+
+
+ **Examples:**
+
+ Using the US Eastern time zone as an example, we can see that a ``tzfile``
+ provides time zone information for the standard Daylight Saving offsets:
+
+ .. testsetup:: tzfile
+
+ from dateutil.tz import gettz
+ from datetime import datetime
+
+ .. doctest:: tzfile
+
+ >>> NYC = gettz('America/New_York')
+ >>> NYC
+ tzfile('/usr/share/zoneinfo/America/New_York')
+
+ >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST
+ 2016-01-03 00:00:00-05:00
+
+ >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT
+ 2016-07-07 00:00:00-04:00
+
+
+ The ``tzfile`` structure contains a fully history of the time zone,
+ so historical dates will also have the right offsets. For example, before
+ the adoption of the UTC standards, New York used local solar mean time:
+
+ .. doctest:: tzfile
+
+ >>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT
+ 1901-04-12 00:00:00-04:56
+
+ And during World War II, New York was on "Eastern War Time", which was a
+ state of permanent daylight saving time:
+
+ .. doctest:: tzfile
+
+ >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT
+ 1944-02-07 00:00:00-04:00
+
"""
def __init__(self, fileobj, filename=None):
@@ -410,7 +466,7 @@ class tzfile(_tzinfo):
if fileobj is not None:
if not file_opened_here:
- fileobj = _ContextWrapper(fileobj)
+ fileobj = _nullcontext(fileobj)
with fileobj as file_stream:
tzobj = self._read_tzfile(file_stream)
@@ -487,7 +543,7 @@ class tzfile(_tzinfo):
if timecnt:
out.trans_idx = struct.unpack(">%dB" % timecnt,
- fileobj.read(timecnt))
+ fileobj.read(timecnt))
else:
out.trans_idx = []
@@ -550,10 +606,7 @@ class tzfile(_tzinfo):
out.ttinfo_list = []
for i in range(typecnt):
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()
tti.offset = gmtoff
tti.dstoffset = datetime.timedelta(0)
@@ -605,37 +658,44 @@ class tzfile(_tzinfo):
# isgmt are off, so it should be in wall time. OTOH, it's
# always in gmt time. Let me know if you have comments
# 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 = laststdoffset or 0
-
- out.trans_list.append(out.trans_list_utc[i] + offset)
-
- # 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
-
- if not isinstance(tti.dstoffset, datetime.timedelta):
- tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset)
- out.trans_idx[i] = tti
+ for i, tti in enumerate(out.trans_idx):
+ offset = tti.offset
+ dstoffset = 0
+
+ if lastdst is not None:
+ if tti.isdst:
+ if not lastdst:
+ dstoffset = offset - lastoffset
+
+ if not dstoffset and lastdstoffset:
+ dstoffset = lastdstoffset
+
+ tti.dstoffset = datetime.timedelta(seconds=dstoffset)
+ lastdstoffset = 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
+
+ lastdst = tti.isdst
+ lastoffset = offset
+ lastbaseoffset = baseoffset
+
+ out.trans_list.append(out.trans_list_utc[i] + adjustment)
out.trans_idx = tuple(out.trans_idx)
out.trans_list = tuple(out.trans_list)
@@ -840,8 +900,9 @@ class tzrange(tzrangebase):
:param start:
A :class:`relativedelta.relativedelta` object or equivalent specifying
- the time and time of year that daylight savings time starts. To specify,
- for example, that DST starts at 2AM on the 2nd Sunday in March, pass:
+ the time and time of year that daylight savings time starts. To
+ specify, for example, that DST starts at 2AM on the 2nd Sunday in
+ March, pass:
``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))``
@@ -849,12 +910,12 @@ class tzrange(tzrangebase):
value is 2 AM on the first Sunday in April.
:param end:
- A :class:`relativedelta.relativedelta` object or equivalent representing
- the time and time of year that daylight savings time ends, with the
- same specification method as in ``start``. One note is that this should
- point to the first time in the *standard* zone, so if a transition
- occurs at 2AM in the DST zone and the clocks are set back 1 hour to 1AM,
- set the `hours` parameter to +1.
+ A :class:`relativedelta.relativedelta` object or equivalent
+ representing the time and time of year that daylight savings time
+ ends, with the same specification method as in ``start``. One note is
+ that this should point to the first time in the *standard* zone, so if
+ a transition occurs at 2AM in the DST zone and the clocks are set back
+ 1 hour to 1AM, set the ``hours`` parameter to +1.
**Examples:**
@@ -985,8 +1046,9 @@ class tzstr(tzrange):
:param s:
A time zone string in ``TZ`` variable format. This can be a
- :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: :class:`unicode`)
- or a stream emitting unicode characters (e.g. :class:`StringIO`).
+ :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x:
+ :class:`unicode`) or a stream emitting unicode characters
+ (e.g. :class:`StringIO`).
:param posix_offset:
Optional. If set to ``True``, interpret strings such as ``GMT+3`` or
@@ -1203,7 +1265,7 @@ class tzical(object):
fileobj = open(fileobj, 'r')
else:
self._s = getattr(fileobj, 'name', repr(fileobj))
- fileobj = _ContextWrapper(fileobj)
+ fileobj = _nullcontext(fileobj)
self._vtz = {}
@@ -1398,15 +1460,87 @@ else:
TZFILES = []
TZPATHS = []
+
def __get_gettz():
tzlocal_classes = (tzlocal,)
if tzwinlocal is not None:
tzlocal_classes += (tzwinlocal,)
class GettzFunc(object):
+ """
+ Retrieve a time zone object from a string representation
+
+ This function is intended to retrieve the :py:class:`tzinfo` subclass
+ that best represents the time zone that would be used if a POSIX
+ `TZ variable`_ were set to the same value.
+
+ If no argument or an empty string is passed to ``gettz``, local time
+ is returned:
+
+ .. code-block:: python3
+
+ >>> gettz()
+ tzfile('/etc/localtime')
+
+ This function is also the preferred way to map IANA tz database keys
+ to :class:`tzfile` objects:
+
+ .. code-block:: python3
+
+ >>> gettz('Pacific/Kiritimati')
+ tzfile('/usr/share/zoneinfo/Pacific/Kiritimati')
+
+ On Windows, the standard is extended to include the Windows-specific
+ zone names provided by the operating system:
+
+ .. code-block:: python3
+
+ >>> gettz('Egypt Standard Time')
+ tzwin('Egypt Standard Time')
+
+ Passing a GNU ``TZ`` style string time zone specification returns a
+ :class:`tzstr` object:
+
+ .. code-block:: python3
+
+ >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
+ tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
+
+ :param name:
+ A time zone name (IANA, or, on Windows, Windows keys), location of
+ a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone
+ specifier. An empty string, no argument or ``None`` is interpreted
+ as local time.
+
+ :return:
+ Returns an instance of one of ``dateutil``'s :py:class:`tzinfo`
+ subclasses.
+
+ .. versionchanged:: 2.7.0
+
+ After version 2.7.0, any two calls to ``gettz`` using the same
+ input strings will return the same object:
+
+ .. code-block:: python3
+
+ >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago')
+ True
+
+ In addition to improving performance, this ensures that
+ `"same zone" semantics`_ are used for datetimes in the same zone.
+
+
+ .. _`TZ variable`:
+ https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
+
+ .. _`"same zone" semantics`:
+ https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html
+ """
def __init__(self):
- self.__instances = {}
+ self.__instances = weakref.WeakValueDictionary()
+ self.__strong_cache_size = 8
+ self.__strong_cache = OrderedDict()
self._cache_lock = _thread.allocate_lock()
def __call__(self, name=None):
@@ -1415,17 +1549,37 @@ def __get_gettz():
if rv is None:
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()
@staticmethod
def nocache(name=None):
@@ -1479,7 +1633,8 @@ def __get_gettz():
if tzwin is not None:
try:
tz = tzwin(name)
- except WindowsError:
+ except (WindowsError, UnicodeEncodeError):
+ # UnicodeEncodeError is for Python 2.7 compat
tz = None
if not tz:
@@ -1488,7 +1643,10 @@ def __get_gettz():
if not tz:
for c in name:
- # name must have at least one offset to be a tzstr
+ # name is not a tzstr unless it has at least
+ # one offset. For short values of "name", an
+ # explicit for loop seems to be the fastest way
+ # To determine if a string contains a digit
if c in "0123456789":
try:
tz = tzstr(name)
@@ -1504,9 +1662,11 @@ def __get_gettz():
return GettzFunc()
+
gettz = __get_gettz()
del __get_gettz
+
def datetime_exists(dt, tz=None):
"""
Given a datetime and a time zone, determine whether or not a given datetime
@@ -1521,9 +1681,10 @@ def datetime_exists(dt, tz=None):
``None`` or not provided, the datetime's own time zone will be used.
:return:
- Returns a boolean value whether or not the "wall time" exists in ``tz``.
+ Returns a boolean value whether or not the "wall time" exists in
+ ``tz``.
- ..versionadded:: 2.7.0
+ .. versionadded:: 2.7.0
"""
if tz is None:
if dt.tzinfo is None:
@@ -1571,7 +1732,7 @@ def datetime_ambiguous(dt, tz=None):
if is_ambiguous_fn is not None:
try:
return tz.is_ambiguous(dt)
- except:
+ except Exception:
pass
# If it doesn't come out and tell us it's ambiguous, we'll just check if
@@ -1594,7 +1755,8 @@ def resolve_imaginary(dt):
wall time would be in a zone had the offset transition not occurred, so
it will always fall forward by the transition's change in offset.
- ..doctest::
+ .. doctest::
+
>>> from dateutil import tz
>>> from datetime import datetime
>>> NYC = tz.gettz('America/New_York')
@@ -1619,7 +1781,7 @@ def resolve_imaginary(dt):
imaginary, the datetime returned is guaranteed to be the same object
passed to the function.
- ..versionadded:: 2.7.0
+ .. versionadded:: 2.7.0
"""
if dt.tzinfo is not None and not datetime_exists(dt):
@@ -1633,24 +1795,42 @@ def resolve_imaginary(dt):
def _datetime_to_timestamp(dt):
"""
- Convert a :class:`datetime.datetime` object to an epoch timestamp in seconds
- since January 1, 1970, ignoring the time zone.
+ Convert a :class:`datetime.datetime` object to an epoch timestamp in
+ seconds since January 1, 1970, ignoring the time zone.
"""
return (dt.replace(tzinfo=None) - EPOCH).total_seconds()
-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
diff --git a/dateutil/tz/win.py b/dateutil/tz/win.py
index def4353..cde07ba 100644
--- a/dateutil/tz/win.py
+++ b/dateutil/tz/win.py
@@ -1,3 +1,11 @@
+# -*- 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.
import datetime
import struct
@@ -39,7 +47,7 @@ TZKEYNAME = _settzkeyname()
class tzres(object):
"""
- Class for accessing `tzres.dll`, which contains timezone name related
+ Class for accessing ``tzres.dll``, which contains timezone name related
resources.
.. versionadded:: 2.5.0
@@ -72,9 +80,10 @@ class tzres(object):
:param offset:
A positive integer value referring to a string from the tzres dll.
- ..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.
"""
resource = self.p_wchar()
@@ -146,6 +155,9 @@ class tzwinbase(tzrangebase):
return result
def display(self):
+ """
+ Return the display name of the time zone.
+ """
return self._display
def transitions(self, year):
@@ -188,6 +200,17 @@ class tzwinbase(tzrangebase):
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):
self._name = name
@@ -234,6 +257,22 @@ class tzwin(tzwinbase):
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):
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
diff --git a/dateutil/utils.py b/dateutil/utils.py
index 29f8181..ebcce6a 100644
--- a/dateutil/utils.py
+++ b/dateutil/utils.py
@@ -1,4 +1,10 @@
# -*- coding: utf-8 -*-
+"""
+This module offers general convenience and utility functions for dealing with
+datetimes.
+
+.. versionadded:: 2.7.0
+"""
from __future__ import unicode_literals
from datetime import datetime, time
diff --git a/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
index 5b76ea1..124f3e1 100644
--- a/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
+++ b/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
Binary files differ
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 0000000..6f40cd1
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1,7 @@
+.. Changelog transcluded from the NEWS file
+
+=========
+Changelog
+=========
+
+.. include:: ../NEWS
diff --git a/docs/conf.py b/docs/conf.py
index 79b71f8..2cf9436 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -106,7 +106,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = 'default'
+html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -186,6 +186,20 @@ html_static_path = []
# Output file base name for HTML help builder.
htmlhelp_basename = 'dateutildoc'
+# -- 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 ---------------------------------------------
@@ -264,3 +278,12 @@ texinfo_documents = [
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
+
+# -- Link checking options -------------------------------------------------
+linkcheck_ignore = [
+ # This has been spotty lately so we're adding a mirror
+ r'https://pgp.mit.edu',
+]
+
+# Reduce problems with ephemeral failures
+linkcheck_retries = 5
diff --git a/docs/examples.rst b/docs/examples.rst
index 8ca6a87..21270ca 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -138,7 +138,7 @@ Next wednesday, but not today.
datetime.date(2003, 9, 24)
Following
-[http://www.cl.cam.ac.uk/~mgk25/iso-time.html ISO year week number notation]
+`ISO year week number notation <https://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_
find the first day of the 15th week of 1997.
.. doctest:: relativedelta
@@ -1190,6 +1190,19 @@ Other random formats:
datetime.datetime(1990, 6, 13, 5, 50)
+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
--------------
diff --git a/docs/exercises/index.rst b/docs/exercises/index.rst
new file mode 100644
index 0000000..e5e5783
--- /dev/null
+++ b/docs/exercises/index.rst
@@ -0,0 +1,242 @@
+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>
+
diff --git a/docs/exercises/solutions/mlk-day-rrule.rst b/docs/exercises/solutions/mlk-day-rrule.rst
new file mode 100644
index 0000000..3292616
--- /dev/null
+++ b/docs/exercises/solutions/mlk-day-rrule.rst
@@ -0,0 +1,11 @@
+: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
+
diff --git a/docs/exercises/solutions/mlk_day_rrule_solution.py b/docs/exercises/solutions/mlk_day_rrule_solution.py
new file mode 100644
index 0000000..025ebf2
--- /dev/null
+++ b/docs/exercises/solutions/mlk_day_rrule_solution.py
@@ -0,0 +1,40 @@
+# ------- 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!')
diff --git a/docs/index.rst b/docs/index.rst
index 55dfd7e..1953aba 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -11,17 +11,24 @@ Documentation
Contents:
.. toctree::
+ :maxdepth: 1
+
+ Overview <self>
+ Changelog <changelog>
+ Examples <examples>
+ Exercises <exercises/index>
+
+.. toctree::
:maxdepth: 2
- self
easter
parser
relativedelta
rrule
tz
+ tz.win <tzwin>
utils
zoneinfo
- examples
Indices and tables
==================
diff --git a/docs/relativedelta.rst b/docs/relativedelta.rst
index 8769cc7..0899f34 100644
--- a/docs/relativedelta.rst
+++ b/docs/relativedelta.rst
@@ -140,7 +140,7 @@ Next wednesday, but not today.
datetime.date(2003, 9, 24)
Following
-[http://www.cl.cam.ac.uk/~mgk25/iso-time.html ISO year week number notation]
+`ISO year week number notation <https://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_
find the first day of the 15th week of 1997.
.. doctest:: relativedelta
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
new file mode 100644
index 0000000..bff9cae
--- /dev/null
+++ b/docs/requirements-docs.txt
@@ -0,0 +1,3 @@
+Sphinx>=1.7.3,!=1.8.0
+sphinx_rtd_theme>=0.3.0
+readme-renderer>=21.0
diff --git a/docs/rrule.rst b/docs/rrule.rst
index 0e54fdb..3fe569e 100644
--- a/docs/rrule.rst
+++ b/docs/rrule.rst
@@ -3,9 +3,20 @@ rrule
=====
.. automodule:: dateutil.rrule
- :members:
:undoc-members:
+Classes
+-------
+
+.. autoclass:: rrule
+.. autoclass:: rruleset
+
+Functions
+---------
+
+.. autofunction:: rrulestr
+
+
rrule examples
--------------
These examples were converted from the RFC.
diff --git a/docs/tz.rst b/docs/tz.rst
index 5fd5bf5..25870dd 100644
--- a/docs/tz.rst
+++ b/docs/tz.rst
@@ -1,6 +1,58 @@
==
tz
==
+.. py:currentmodule:: dateutil.tz
+
.. automodule:: dateutil.tz
- :members:
- :undoc-members:
+
+Objects
+-------
+.. py:data:: dateutil.tz.UTC
+
+ A convenience instance of :class:`dateutil.tz.tzutc`.
+
+Functions
+---------
+
+.. autofunction:: gettz
+
+ .. automethod:: gettz.nocache
+ .. automethod:: gettz.cache_clear
+
+.. autofunction:: enfold
+
+.. autofunction:: datetime_ambiguous
+.. autofunction:: datetime_exists
+
+.. autofunction:: resolve_imaginary
+
+
+Classes
+-------
+
+.. autoclass:: tzutc
+
+.. autoclass:: tzoffset
+
+.. autoclass:: tzlocal
+
+.. autoclass:: tzwinlocal
+ :members: display, transitions
+
+ .. note::
+
+ Only available on Windows
+
+.. autoclass:: tzrange
+
+.. autoclass:: tzstr
+
+.. autoclass:: tzical
+ :members:
+
+.. autoclass:: tzwin
+ :members: display, transitions, list
+
+ .. note::
+
+ Only available on Windows
diff --git a/docs/tzwin.rst b/docs/tzwin.rst
new file mode 100644
index 0000000..0a200de
--- /dev/null
+++ b/docs/tzwin.rst
@@ -0,0 +1,22 @@
+======
+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:
+
diff --git a/pyproject.toml b/pyproject.toml
index 32bf08e..2c73368 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,12 @@
+[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]
package = "dateutil"
package_dir = "dateutil"
diff --git a/requirements-dev.txt b/requirements-dev.txt
index cf0d325..c29f645 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -2,6 +2,8 @@ six
pytest >= 3.0; python_version != '3.3'
pytest < 3.3; python_version == '3.3'
pytest-cov >= 2.0.0
-freezegun
+freezegun < 0.3.11; python_version == '3.3'
+freezegun ; python_version != '3.3'
+hypothesis >= 3.30
coverage
mock ; python_version < '3.0'
diff --git a/setup.cfg b/setup.cfg
index 3395a23..22ab896 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,4 +5,12 @@ universal = 1
license_file = LICENSE
[tool:pytest]
-xfail_strict = true \ No newline at end of file
+python_files=
+ test_*.py
+ *_test.py
+ *_solution.py
+xfail_strict = true
+filterwarnings =
+ error
+ error::DeprecationWarning
+ error:PendingDeprecationWarning
diff --git a/setup.py b/setup.py
index b59bd63..f8317ce 100644
--- a/setup.py
+++ b/setup.py
@@ -10,6 +10,7 @@ from distutils.version import LooseVersion
import warnings
import io
+import sys
if isfile("MANIFEST"):
os.unlink("MANIFEST")
@@ -21,29 +22,31 @@ if LooseVersion(setuptools.__version__) <= LooseVersion("24.3"):
class Unsupported(TestCommand):
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)
+
###
# Load metadata
PACKAGES = find_packages(where='.', exclude=['dateutil.test'])
+
def README():
with io.open('README.rst', encoding='utf-8') as f:
readme_lines = f.readlines()
# The .. doctest directive is not supported by PyPA
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')
else:
lines_out.append(line)
return ''.join(lines_out)
-README = README()
+README = README() # NOQA
+
setup(name="python-dateutil",
use_scm_version={
@@ -62,9 +65,8 @@ setup(name="python-dateutil",
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*",
package_data={"dateutil.zoneinfo": ["dateutil-zoneinfo.tar.gz"]},
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=[
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
diff --git a/tox.ini b/tox.ini
index 53c317b..9e343c9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -16,8 +16,7 @@ skip_missing_interpreters = true
description = run the unit tests with pytest under {basepython}
setenv = COVERAGE_FILE={toxworkdir}/.coverage.{envname}
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
[testenv:coverage]
@@ -50,10 +49,22 @@ source = dateutil
skip_covered = True
show_missing = True
+[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]
description = invoke sphinx-build to build the HTML docs, check that URIs are valid
basepython = python3.6
-deps = sphinx >= 1.6.3, < 2
+deps = -r docs/requirements-docs.txt
{[testenv]deps}
commands = sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" {posargs:-W --color -bhtml}
sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" {posargs:-W --color -blinkcheck}
+ python setup.py check -r -s
diff --git a/updatezinfo.py b/updatezinfo.py
index f99eece..1edf2e1 100644
--- a/updatezinfo.py
+++ b/updatezinfo.py
@@ -12,8 +12,8 @@ from dateutil.zoneinfo import rebuild
METADATA_FILE = "zonefile_metadata.json"
-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)
releases_urls = metadata['releases_url']
@@ -52,4 +52,11 @@ def main():
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)
diff --git a/zonefile_metadata.json b/zonefile_metadata.json
index 90a982e..86a5956 100644
--- a/zonefile_metadata.json
+++ b/zonefile_metadata.json
@@ -4,9 +4,9 @@
"https://dateutil.github.io/tzdata/tzdata/",
"ftp://ftp.iana.org/tz/releases/"
],
- "tzdata_file": "tzdata2018d.tar.gz",
- "tzdata_file_sha512": "ee961aedc34e134172523a29fb8a9358f42649d06ffcd2d8a6ad86eeb174a80af5bfc4637e9e52ecdc51fa3d01afef9fff660a69ed72904ff747a59b5634830b",
- "tzversion": "2018d",
+ "tzdata_file": "tzdata2018i.tar.gz",
+ "tzdata_file_sha512": "6afcacb377842190648ed26f01abcf3db37aa2e7c63d8c509c29b4bc0078b7ff2d4e5375291b9f53498215b9e2f04936bc6145e2f651ae0be6d8166d8d336f6a",
+ "tzversion": "2018i",
"zonegroups": [
"africa",
"antarctica",