From 4ce9fb2ecf92b06bd4499bc8db72e716f7ab0e9c Mon Sep 17 00:00:00 2001 From: Sean Stewart Date: Sun, 22 Nov 2020 20:56:03 -0500 Subject: [PATCH 1/5] Add ISO 8601 compliant isoformat() method to Duration. --- docs/docs/duration.md | 19 +++++++++++++++++++ pendulum/duration.py | 23 +++++++++++++++++++++++ tests/duration/test_isoformat.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 tests/duration/test_isoformat.py diff --git a/docs/docs/duration.md b/docs/docs/duration.md index 0801d9ea..83403e75 100644 --- a/docs/docs/duration.md +++ b/docs/docs/duration.md @@ -175,3 +175,22 @@ It also has a handy `in_words()` method, which determines the duration represent >>> it.in_words(locale='de') '168 Wochen 1 Tag 2 Stunden 1 Minute 24 Sekunden' ``` + +Finally, it has an +[ISO-8601-compliant](https://en.wikipedia.org/wiki/ISO_8601#Durations) `isformat()` +method for cross-platform representation. + +```python + +>>> import pendulum + +>>> dur = pendulum.duration(years=1, months=3, days=6, seconds=3) + +>>> iso = dur.isoformat() + +>>> print(iso) +'P1Y3M6DT0H0M3S' + +>>> pendulum.parse(iso) == dur +True +``` diff --git a/pendulum/duration.py b/pendulum/duration.py index 45df13da..0225010f 100644 --- a/pendulum/duration.py +++ b/pendulum/duration.py @@ -414,6 +414,29 @@ def __divmod__(self, other): return NotImplemented + def isoformat(self) -> str: + """Represent this duration as a ISO-8601-compliant string.""" + periods = [ + ("Y", self.years), + ("M", self.months), + ("D", self.remaining_days), + ] + period = "P" + for sym, val in periods: + period += f"{val}{sym}" + times = [ + ("H", self.hours), + ("M", self.minutes), + ("S", self.remaining_seconds), + ] + time = "T" + for sym, val in times: + time += f"{val}{sym}" + if self.microseconds: + time = time[:-1] + time += f".{self.microseconds:06}S" + return period + time + Duration.min = Duration(days=-999999999) Duration.max = Duration( diff --git a/tests/duration/test_isoformat.py b/tests/duration/test_isoformat.py new file mode 100644 index 00000000..fb7bef7e --- /dev/null +++ b/tests/duration/test_isoformat.py @@ -0,0 +1,32 @@ +import pytest + +from pendulum import Duration, parse + + +@pytest.mark.parametrize( + "dur, expected_iso", + [ + ( + Duration( + years=1, + months=3, + days=6, + minutes=50, + seconds=3, + milliseconds=10, + microseconds=10, + ), + "P1Y3M6DT0H50M3.010010S", + ), + + (Duration(days=4, hours=12, minutes=30, seconds=5), "P0Y0M4DT12H30M5S"), + (Duration(days=4, hours=12, minutes=30, seconds=5), "P0Y0M4DT12H30M5S"), + (Duration(microseconds=10), "P0Y0M0DT0H0M0.000010S"), + (Duration(milliseconds=1), "P0Y0M0DT0H0M0.001000S"), + (Duration(minutes=1), "P0Y0M0DT0H1M0S"), + ], +) +def test_isoformat(dur, expected_iso): + fmt = dur.isoformat() + assert fmt == expected_iso + assert parse(fmt) == dur From 2dec70fb74247aaef1c49edd90d87e97b3180f59 Mon Sep 17 00:00:00 2001 From: Sean Stewart Date: Sun, 22 Nov 2020 20:57:32 -0500 Subject: [PATCH 2/5] Add .venv to gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index bb25f8b3..30b9fddd 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ setup.py # editor .vscode + +# dev + +.venv From 364388d853acf61131de30dcf60ea794b8edb9db Mon Sep 17 00:00:00 2001 From: Sean Stewart Date: Mon, 23 Nov 2020 18:47:08 -0500 Subject: [PATCH 3/5] Run pre-commit linters on source. --- pendulum/__init__.py | 5 ++++- tests/datetime/test_from_format.py | 3 ++- tests/duration/test_isoformat.py | 24 ++++++++++++------------ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/pendulum/__init__.py b/pendulum/__init__.py index bb1e0ca7..b19f87d5 100644 --- a/pendulum/__init__.py +++ b/pendulum/__init__.py @@ -251,7 +251,10 @@ def yesterday(tz="local"): # type: (Union[str, _Timezone]) -> DateTime def from_format( - string, fmt, tz=UTC, locale=None, # noqa + string, + fmt, + tz=UTC, + locale=None, # noqa ): # type: (str, str, Union[str, _Timezone], Optional[str]) -> DateTime """ Creates a DateTime instance from a specific format. diff --git a/tests/datetime/test_from_format.py b/tests/datetime/test_from_format.py index 0949332c..398b68da 100644 --- a/tests/datetime/test_from_format.py +++ b/tests/datetime/test_from_format.py @@ -39,7 +39,8 @@ def test_from_format_with_timezone(): def test_from_format_with_square_bracket_in_timezone(): with pytest.raises(ValueError, match="^String does not match format"): pendulum.from_format( - "1975-05-21 22:32:11 Eu[rope/London", "YYYY-MM-DD HH:mm:ss z", + "1975-05-21 22:32:11 Eu[rope/London", + "YYYY-MM-DD HH:mm:ss z", ) diff --git a/tests/duration/test_isoformat.py b/tests/duration/test_isoformat.py index fb7bef7e..2d280d15 100644 --- a/tests/duration/test_isoformat.py +++ b/tests/duration/test_isoformat.py @@ -1,24 +1,24 @@ import pytest -from pendulum import Duration, parse +from pendulum import Duration +from pendulum import parse @pytest.mark.parametrize( "dur, expected_iso", [ ( - Duration( - years=1, - months=3, - days=6, - minutes=50, - seconds=3, - milliseconds=10, - microseconds=10, - ), - "P1Y3M6DT0H50M3.010010S", + Duration( + years=1, + months=3, + days=6, + minutes=50, + seconds=3, + milliseconds=10, + microseconds=10, + ), + "P1Y3M6DT0H50M3.010010S", ), - (Duration(days=4, hours=12, minutes=30, seconds=5), "P0Y0M4DT12H30M5S"), (Duration(days=4, hours=12, minutes=30, seconds=5), "P0Y0M4DT12H30M5S"), (Duration(microseconds=10), "P0Y0M0DT0H0M0.000010S"), From da9ab9d346cef41d13fac0de61e880a39c9dd8ff Mon Sep 17 00:00:00 2001 From: Sean Stewart Date: Sat, 28 Nov 2020 09:31:33 -0500 Subject: [PATCH 4/5] Fix for python 3.5 compatibility --- pendulum/duration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pendulum/duration.py b/pendulum/duration.py index 0225010f..9b352bd3 100644 --- a/pendulum/duration.py +++ b/pendulum/duration.py @@ -423,7 +423,7 @@ def isoformat(self) -> str: ] period = "P" for sym, val in periods: - period += f"{val}{sym}" + period += "{val}{sym}".format(val=val, sym=sym) times = [ ("H", self.hours), ("M", self.minutes), @@ -431,10 +431,10 @@ def isoformat(self) -> str: ] time = "T" for sym, val in times: - time += f"{val}{sym}" + time += "{val}{sym}".format(val=val, sym=sym) if self.microseconds: time = time[:-1] - time += f".{self.microseconds:06}S" + time += ".{ms:06}S".format(ms=self.microseconds) return period + time From d75d2ff3a451ed9d5a2b03f7a5dcb1cf69694d27 Mon Sep 17 00:00:00 2001 From: Sean Stewart Date: Sat, 28 Nov 2020 09:40:34 -0500 Subject: [PATCH 5/5] Turn off fast-fail in testing. --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2e8b58e7..26fc743b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,7 @@ jobs: strategy: matrix: python-version: [2.7, 3.5, 3.6, 3.7, 3.8, pypy3] + fail-fast: false steps: - uses: actions/checkout@v2 @@ -68,6 +69,7 @@ jobs: strategy: matrix: python-version: [2.7, 3.5, 3.6, 3.7, 3.8, pypy3] + fail-fast: false steps: - uses: actions/checkout@v2 @@ -112,6 +114,7 @@ jobs: strategy: matrix: python-version: [2.7, 3.5, 3.6, 3.7, 3.8] + fail-fast: false steps: - uses: actions/checkout@v2