From 37c0e427468c43b8dc1be981dcf0b6ae738f2b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B3mas=20=C3=81rni=20J=C3=B3nasson?= Date: Tue, 5 Nov 2019 18:28:45 +0000 Subject: [PATCH 1/5] Compare two datetimes using timestamps (avoids issues during DST off-transition) --- pendulum/datetime.py | 36 ++++++++++++++++--------------- tests/datetime/test_comparison.py | 22 +++++++++++++++++++ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/pendulum/datetime.py b/pendulum/datetime.py index d79257ca..4b925424 100644 --- a/pendulum/datetime.py +++ b/pendulum/datetime.py @@ -1534,26 +1534,28 @@ def __reduce__(self): def __reduce_ex__(self, protocol): return self.__class__, self._getstate(protocol) - def _cmp(self, other, **kwargs): - # Fix for pypy which compares using this method - # which would lead to infinite recursion if we didn't override - kwargs = {"tzinfo": self.tz} + def __le__(self, other): + if isinstance(other, DateTime): + return self._cmp(other) <= 0 + return super().__le__(other) - if _HAS_FOLD: - kwargs["fold"] = self.fold + def __lt__(self, other): + if isinstance(other, DateTime): + return self._cmp(other) < 0 + return super().__lt__(other) - dt = datetime.datetime( - self.year, - self.month, - self.day, - self.hour, - self.minute, - self.second, - self.microsecond, - **kwargs - ) + def __ge__(self, other): + # Will default to the negative of its reflection + return NotImplemented + + def __gt__(self, other): + # Will default to the negative of its reflection + return NotImplemented - return 0 if dt == other else 1 if dt > other else -1 + def _cmp(self, other, **kwargs): + sts = self.timestamp() + ots = other.timestamp() + return 0 if sts == ots else 1 if sts > ots else -1 DateTime.min = DateTime(1, 1, 1, 0, 0, tzinfo=UTC) diff --git a/tests/datetime/test_comparison.py b/tests/datetime/test_comparison.py index 0819f801..d836931e 100644 --- a/tests/datetime/test_comparison.py +++ b/tests/datetime/test_comparison.py @@ -1,6 +1,7 @@ from datetime import datetime import pendulum +import pytest import pytz from ..conftest import assert_datetime @@ -192,6 +193,27 @@ def test_less_than_with_timezone_false(): assert not d1 < d3 +@pytest.mark.parametrize( + 'truth_fun', + ( + lambda earlier, later: earlier < later, + lambda earlier, later: earlier <= later, + lambda earlier, later: later > earlier, + lambda earlier, later: later >= earlier, + ) +) +def test_comparison_crossing_dst_transitioning_off(truth_fun): + # We only need to test turning off DST, since that's when the time + # component goes backwards. + # We start with 2019-11-03T01:30:00-0700 + earlier = pendulum.datetime(2019, 11, 3, 8, 30).in_tz("US/Pacific") + # Adding 55 minutes to it, we turn off DST, but the time component is + # slightly less than before, i.e. we get 2019-11-03T01:25:00-0800 + later = earlier.add(minutes=55) + # Run through all inequality-comparison functions + assert truth_fun(earlier, later) + + def test_less_than_or_equal_true(): d1 = pendulum.datetime(2000, 1, 1) d2 = pendulum.datetime(2000, 1, 2) From 8af1e2e8f859d5b0b8dc11e1aea6eb25b1e7aba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Mon, 13 Jul 2020 12:30:10 +0200 Subject: [PATCH 2/5] Fix now() behavior for DST (#483) * Fix now() behavior for DST * Upgrade pip before installing dependencies --- .github/workflows/tests.yml | 12 ++++++++ pendulum/__init__.py | 12 +++++++- poetry.lock | 47 ++++++++++++++++++++++++++------ pyproject.toml | 4 ++- tests/datetime/test_construct.py | 46 +++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 155b2227..af2bdf9e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -44,6 +44,10 @@ jobs: with: path: .venv key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + - name: Upgrade pip + run: | + source $HOME/.poetry/env + poetry run python -m pip install pip -U - name: Install dependencies run: | source $HOME/.poetry/env @@ -86,6 +90,10 @@ jobs: with: path: .venv key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-fix-${{ hashFiles('**/poetry.lock') }} + - name: Upgrade pip + run: | + source $HOME/.poetry/env + poetry run python -m pip install pip -U - name: Install dependencies run: | source $HOME/.poetry/env @@ -127,6 +135,10 @@ jobs: with: path: .venv key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + - name: Upgrade pip + run: | + $env:Path += ";$env:Userprofile\.poetry\bin" + poetry run python -m pip install pip -U - name: Install dependencies run: | $env:Path += ";$env:Userprofile\.poetry\bin" diff --git a/pendulum/__init__.py b/pendulum/__init__.py index 78524b2c..bb1e0ca7 100644 --- a/pendulum/__init__.py +++ b/pendulum/__init__.py @@ -216,7 +216,17 @@ def now(tz=None): # type: (Optional[Union[str, _Timezone]]) -> DateTime tz = _safe_timezone(tz) dt = tz.convert(dt) - return instance(dt, tz) + return DateTime( + dt.year, + dt.month, + dt.day, + dt.hour, + dt.minute, + dt.second, + dt.microsecond, + tzinfo=dt.tzinfo, + fold=dt.fold if _HAS_FOLD else 0, + ) def today(tz="local"): # type: (Union[str, _Timezone]) -> DateTime diff --git a/poetry.lock b/poetry.lock index 0c58f9fc..4b5211bb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -99,10 +99,10 @@ description = "Cleo allows you to create beautiful and testable command-line int name = "cleo" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.7.6" +version = "0.8.1" [package.dependencies] -clikit = ">=0.4.0,<0.5.0" +clikit = ">=0.6.0,<0.7.0" [[package]] category = "dev" @@ -118,14 +118,15 @@ description = "CliKit is a group of utilities to build beautiful and testable co name = "clikit" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.4.3" +version = "0.6.2" [package.dependencies] pastel = ">=0.2.0,<0.3.0" pylev = ">=1.3,<2.0" +crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} enum34 = {version = ">=1.1,<2.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\""} typing = {version = ">=3.6,<4.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"3.5\""} -typing-extensions = {version = ">=3.6,<4.0", markers = "python_version >= \"3.5.0\" and python_version < \"3.5.4\""} +typing-extensions = {version = ">=3.6,<4.0", markers = "python_version >= \"3.5\" and python_full_version < \"3.5.4\""} [[package]] category = "dev" @@ -166,6 +167,14 @@ version = "5.1" [package.extras] toml = ["toml"] +[[package]] +category = "dev" +description = "Manage Python errors with ease" +name = "crashtest" +optional = false +python-versions = ">=3.6,<4.0" +version = "0.3.0" + [[package]] category = "dev" description = "Distribution utilities" @@ -190,6 +199,18 @@ optional = false python-versions = "*" version = "3.0.12" +[[package]] +category = "dev" +description = "Let your Python tests travel through time" +name = "freezegun" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.3.15" + +[package.dependencies] +python-dateutil = ">=1.0,<2.0 || >2.0" +six = "*" + [[package]] category = "dev" description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" @@ -713,7 +734,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["pathlib2", "unittest2", "jaraco.itertools", "func-timeout"] [metadata] -content-hash = "0f423ffa1c413a3d812c9af00057382655b1f4527551359f5270d6c742ee83fb" +content-hash = "531d174befc626d82bcaabeccd06cfeff9f4e81201993bdd9b61cd5f50e54123" python-versions = "~2.7 || ^3.5" [metadata.files] @@ -750,16 +771,16 @@ cfgv = [ {file = "cfgv-2.0.1.tar.gz", hash = "sha256:edb387943b665bf9c434f717bf630fa78aecd53d5900d2e05da6ad6048553144"}, ] cleo = [ - {file = "cleo-0.7.6-py2.py3-none-any.whl", hash = "sha256:9443d67e5b2da79b32d820ae41758dd6a25618345cb10b9a022a695e26b291b9"}, - {file = "cleo-0.7.6.tar.gz", hash = "sha256:99cf342406f3499cec43270fcfaf93c126c5164092eca201dfef0f623360b409"}, + {file = "cleo-0.8.1-py2.py3-none-any.whl", hash = "sha256:141cda6dc94a92343be626bb87a0b6c86ae291dfc732a57bf04310d4b4201753"}, + {file = "cleo-0.8.1.tar.gz", hash = "sha256:3d0e22d30117851b45970b6c14aca4ab0b18b1b53c8af57bed13208147e4069f"}, ] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] clikit = [ - {file = "clikit-0.4.3-py2.py3-none-any.whl", hash = "sha256:71e321b7795a2a6c4888629f43365d52db071737e668ab16861121d7dd3ada09"}, - {file = "clikit-0.4.3.tar.gz", hash = "sha256:6e2d7e115e7c7b35bceb0209109935ab2f9ab50910e9ff2293f7fa0b7abf973e"}, + {file = "clikit-0.6.2-py2.py3-none-any.whl", hash = "sha256:71268e074e68082306e23d7369a7b99f824a0ef926e55ba2665e911f7208489e"}, + {file = "clikit-0.6.2.tar.gz", hash = "sha256:442ee5db9a14120635c5990bcdbfe7c03ada5898291f0c802f77be71569ded59"}, ] colorama = [ {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, @@ -806,6 +827,10 @@ coverage = [ {file = "coverage-5.1-cp39-cp39-win_amd64.whl", hash = "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e"}, {file = "coverage-5.1.tar.gz", hash = "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"}, ] +crashtest = [ + {file = "crashtest-0.3.0-py3-none-any.whl", hash = "sha256:06069a9267c54be31c42b03574b72407bf780e13c82cb0238f24ea69cf25b6dd"}, + {file = "crashtest-0.3.0.tar.gz", hash = "sha256:e9c06cc96400939ab5327123a3f699078eaad8a6283247d7b2ae0f6afffadf14"}, +] distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, @@ -819,6 +844,10 @@ filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, ] +freezegun = [ + {file = "freezegun-0.3.15-py2.py3-none-any.whl", hash = "sha256:82c757a05b7c7ca3e176bfebd7d6779fd9139c7cb4ef969c38a28d74deef89b2"}, + {file = "freezegun-0.3.15.tar.gz", hash = "sha256:e2062f2c7f95cc276a834c22f1a17179467176b624cc6f936e8bc3be5535ad1b"}, +] funcsigs = [ {file = "funcsigs-1.0.2-py2.py3-none-any.whl", hash = "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca"}, {file = "funcsigs-1.0.2.tar.gz", hash = "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"}, diff --git a/pyproject.toml b/pyproject.toml index 95082049..0aecf2d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ pytest = "^4.6" pytest-cov = "^2.5" pytz = ">=2018.3" babel = "^2.5" -cleo = "^0.7.5" +cleo = "^0.8.1" tox = "^3.0" black = { version = "^19.3b0", markers = "python_version >= '3.6' and python_version < '4.0' and implementation_name != 'pypy'" } isort = { version = "^4.3.21", markers = "python_version >= '3.6' and python_version < '4.0'" } @@ -41,6 +41,7 @@ mkdocs = { version = "^1.0", python = "^3.5" } pymdown-extensions = "^6.0" pygments = "^2.2" markdown-include = "^0.5.1" +freezegun = "^0.3.15" [tool.isort] @@ -62,6 +63,7 @@ known_third_party = [ "babel", "cleo", "dateutil", + "freezegun", "pytzdata", ] diff --git a/tests/datetime/test_construct.py b/tests/datetime/test_construct.py index b5db944c..2e45ead9 100644 --- a/tests/datetime/test_construct.py +++ b/tests/datetime/test_construct.py @@ -3,6 +3,7 @@ from datetime import datetime from dateutil import tz +from freezegun import freeze_time import pendulum import pytest @@ -10,6 +11,7 @@ from pendulum import DateTime from pendulum.tz import timezone +from pendulum.utils._compat import PY36 from ..conftest import assert_datetime @@ -102,6 +104,50 @@ def test_now(): assert now.hour != in_paris.hour +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +@freeze_time("2016-03-27 00:30:00") +def test_now_dst_off(): + utc = pendulum.now("UTC") + in_paris = pendulum.now("Europe/Paris") + in_paris_from_utc = utc.in_tz("Europe/Paris") + assert in_paris.hour == 1 + assert not in_paris.is_dst() + assert in_paris.isoformat() == in_paris_from_utc.isoformat() + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +@freeze_time("2016-03-27 01:30:00") +def test_now_dst_transitioning_on(): + utc = pendulum.now("UTC") + in_paris = pendulum.now("Europe/Paris") + in_paris_from_utc = utc.in_tz("Europe/Paris") + assert in_paris.hour == 3 + assert in_paris.is_dst() + assert in_paris.isoformat() == in_paris_from_utc.isoformat() + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +@freeze_time("2016-10-30 00:30:00") +def test_now_dst_on(): + utc = pendulum.now("UTC") + in_paris = pendulum.now("Europe/Paris") + in_paris_from_utc = utc.in_tz("Europe/Paris") + assert in_paris.hour == 2 + assert in_paris.is_dst() + assert in_paris.isoformat() == in_paris_from_utc.isoformat() + + +@pytest.mark.skipif(not PY36, reason="fold attribute only present in Python 3.6+") +@freeze_time("2016-10-30 01:30:00") +def test_now_dst_transitioning_off(): + utc = pendulum.now("UTC") + in_paris = pendulum.now("Europe/Paris") + in_paris_from_utc = utc.in_tz("Europe/Paris") + assert in_paris.hour == 2 + assert not in_paris.is_dst() + assert in_paris.isoformat() == in_paris_from_utc.isoformat() + + def test_now_with_fixed_offset(): now = pendulum.now(6) From 1b1784f69a6a5744621bb561c683903cf7e224d0 Mon Sep 17 00:00:00 2001 From: Rodrigo Dias Arruda Senra Date: Mon, 13 Jul 2020 06:54:20 -0400 Subject: [PATCH 3/5] Fix the typing annotation for the return of parse() (#452) --- pendulum/parser.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pendulum/parser.py b/pendulum/parser.py index 3d2cc841..c8f5fe04 100644 --- a/pendulum/parser.py +++ b/pendulum/parser.py @@ -5,8 +5,11 @@ import pendulum +from .date import Date from .parsing import _Interval from .parsing import parse as base_parse +from .time import Duration +from .time import Time from .tz import UTC @@ -16,7 +19,9 @@ CDuration = None -def parse(text, **options): # type: (str, **typing.Any) -> str +def parse( + text, **options +): # type: (str, **typing.Any) -> typing.Union[Date, Time, Duration] # Use the mock now value if it exists options["now"] = options.get("now", pendulum.get_test_now()) From 9d28c880e537ff2bade606460096c1d6b6a1096c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Eustace?= Date: Mon, 13 Jul 2020 12:56:11 +0200 Subject: [PATCH 4/5] Add DateTime to possible return types of parse() --- pendulum/parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pendulum/parser.py b/pendulum/parser.py index c8f5fe04..0df76161 100644 --- a/pendulum/parser.py +++ b/pendulum/parser.py @@ -6,6 +6,7 @@ import pendulum from .date import Date +from .datetime import DateTime from .parsing import _Interval from .parsing import parse as base_parse from .time import Duration @@ -21,7 +22,7 @@ def parse( text, **options -): # type: (str, **typing.Any) -> typing.Union[Date, Time, Duration] +): # type: (str, **typing.Any) -> typing.Union[Date, Time, DateTime, Duration] # Use the mock now value if it exists options["now"] = options.get("now", pendulum.get_test_now()) From d9b66d258b376a7669fdf094c19c4623ac5fb695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B3mas=20=C3=81rni=20J=C3=B3nasson?= Date: Mon, 13 Jul 2020 10:49:17 -0700 Subject: [PATCH 5/5] Make into private Jyve library --- README.rst | 9 +++++++++ pyproject.toml | 21 ++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 313d4c92..50dab05e 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,15 @@ Pendulum ######## +**NOTE: This is a fork off of pendulum (https://github.com/sdispater/pendulum)** +The fork includes a few miscellaneous fixes. We currently publish it as jyve-pendulum. + +To make a new version, do: + +1. poetry version prerelease +2. poetry build +3. poetry publish -r jyve + .. image:: https://img.shields.io/pypi/v/pendulum.svg :target: https://pypi.python.org/pypi/pendulum diff --git a/pyproject.toml b/pyproject.toml index 0aecf2d5..fd45097f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,10 @@ +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" + [tool.poetry] -name = "pendulum" -version = "2.1.0" +name = "jyve-pendulum" +version = "2.1.1-alpha.1" description = "Python datetimes made easy" authors = ["Sébastien Eustace "] license = "MIT" @@ -9,12 +13,12 @@ homepage = "https://pendulum.eustace.io" repository = "https://github.com/sdispater/pendulum" documentation = "https://pendulum.eustace.io/docs" keywords = ['datetime', 'date', 'time'] - -build = "build.py" +classifiers = [ + "Private :: Do Not Upload" +] packages = [ {include = "pendulum"}, - #{include = "tests", format = "sdist"}, ] include = ["pendulum/py.typed"] @@ -67,7 +71,6 @@ known_third_party = [ "pytzdata", ] - -[build-system] -requires = ["poetry>=1.0.0b1"] -build-backend = "poetry.masonry.api" +[[tool.poetry.source]] +name = "jyve" +url = "https://pypi.fury.io/jyve"