Skip to content

Commit 9d5057f

Browse files
committed
Improves the formatting part by isolating it in its own module.
1 parent 3cbe6ce commit 9d5057f

File tree

6 files changed

+187
-85
lines changed

6 files changed

+187
-85
lines changed

pendulum/formatting/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from .classic_formatter import ClassicFormatter
4+
5+
6+
FORMATTERS = {
7+
'classic': ClassicFormatter()
8+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import re
4+
import datetime
5+
6+
from .formatter import Formatter
7+
8+
9+
class ClassicFormatter(Formatter):
10+
11+
_CUSTOM_FORMATTERS = ['_z', '_t']
12+
_FORMATTERS_REGEX = re.compile('%%(%s)' % '|'.join(_CUSTOM_FORMATTERS))
13+
14+
def format(self, dt, fmt, locale=None):
15+
"""
16+
Formats a Pendulum instance with a given format and locale.
17+
18+
:param dt: The instance to format
19+
:type dt: Pendulum
20+
21+
:param fmt: The format to use
22+
:type fmt: str
23+
24+
:param locale: The locale to use
25+
:type locale: str or None
26+
27+
:rtype: str
28+
"""
29+
if not locale:
30+
locale = dt.get_locale()
31+
32+
# Checking for custom formatters
33+
fmt = self._FORMATTERS_REGEX.sub(lambda m: self._strftime(dt, m, locale), fmt)
34+
35+
# Checking for localizable directives
36+
fmt = re.sub('%(a|A|b|B|p)', lambda m: self._localize_directive(dt, m.group(1), locale), fmt)
37+
38+
return dt._datetime.strftime(fmt)
39+
40+
def _localize_directive(self, dt, directive, locale):
41+
"""
42+
Localize a native strftime directive.
43+
44+
:param dt: The instance to format
45+
:type dt: Pendulum
46+
47+
:param directive: The directive to localize
48+
:type directive: str
49+
50+
:param locale: The locale to use for localization
51+
:type locale: str
52+
53+
:rtype: str
54+
"""
55+
if directive == 'a':
56+
id = 'days_abbrev'
57+
number = dt.day_of_week
58+
elif directive == 'A':
59+
id = 'days'
60+
number = dt.day_of_week
61+
elif directive == 'b':
62+
id = 'months_abbrev'
63+
number = dt.month
64+
elif directive == 'B':
65+
id = 'months'
66+
number = dt.month
67+
elif directive == 'p':
68+
id = 'meridian'
69+
number = dt.hour
70+
else:
71+
raise ValueError('Unlocalizable directive [{}]'.format(directive))
72+
73+
translation = dt.translator().transchoice(id, number, locale=locale)
74+
if translation == id:
75+
return ''
76+
77+
return translation
78+
79+
def _strftime(self, dt, m, locale):
80+
"""
81+
Handles custom formatters in format string.
82+
83+
:param dt: The instance to format
84+
:type dt: Pendulum
85+
86+
:return: str
87+
"""
88+
fmt = m.group(1)
89+
90+
if fmt == '_z':
91+
offset = dt.utcoffset() or datetime.timedelta()
92+
minutes = offset.total_seconds() / 60
93+
94+
if minutes >= 0:
95+
sign = '+'
96+
else:
97+
sign = '-'
98+
99+
hour, minute = divmod(abs(int(minutes)), 60)
100+
101+
return '{0}{1:02d}:{2:02d}'.format(sign, hour, minute)
102+
elif fmt == '_t':
103+
translation = dt.translator().transchoice('ordinal', dt.day, locale=locale)
104+
if translation == 'ordinal':
105+
translation = ''
106+
107+
return translation
108+
109+
raise ValueError('Unknown formatter %%{}'.format(fmt))

pendulum/formatting/formatter.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# -*- coding: utf-8 -*-
2+
3+
4+
class Formatter(object):
5+
"""
6+
Base class for all formatters.
7+
"""
8+
9+
def format(self, dt, fmt, locale=None):
10+
"""
11+
Formats a Pendulum instance with a given format and locale.
12+
13+
:param dt: The instance to format
14+
:type dt: Pendulum
15+
16+
:param fmt: The format to use
17+
:type fmt: str
18+
19+
:param locale: The locale to use
20+
:type locale: str or None
21+
22+
:rtype: str
23+
"""
24+
raise NotImplementedError()

pendulum/pendulum.py

Lines changed: 15 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
from __future__ import division
44

5-
import re
65
import time as _time
76
import math
87
import calendar
98
import datetime
9+
import warnings
1010
import locale as _locale
1111

1212
from contextlib import contextmanager
@@ -18,6 +18,7 @@
1818
from .mixins.default import TranslatableMixin
1919
from .tz import Timezone, UTC, FixedTimezone, local_timezone
2020
from .tz.timezone_info import TimezoneInfo
21+
from .formatting import FORMATTERS
2122
from ._compat import PY33, basestring
2223
from .constants import (
2324
SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
@@ -78,9 +79,6 @@ class Pendulum(datetime.datetime, TranslatableMixin):
7879

7980
_EPOCH = datetime.datetime(1970, 1, 1, tzinfo=UTC)
8081

81-
_CUSTOM_FORMATTERS = ['_z', '_t']
82-
_FORMATTERS_REGEX = re.compile('%%(%s)' % '|'.join(_CUSTOM_FORMATTERS))
83-
8482
_MODIFIERS_VALID_UNITS = ['day', 'week', 'month', 'year', 'decade', 'century']
8583

8684
_TRANSITION_RULE = Timezone.POST_TRANSITION
@@ -885,25 +883,28 @@ def set_to_string_format(cls, fmt):
885883
"""
886884
cls._to_string_format = fmt
887885

888-
def format(self, fmt, locale=None):
886+
def format(self, fmt, locale=None, formatter=None):
889887
"""
890888
Formats the Pendulum instance using the given format.
891889
892890
:param fmt: The format to use
893891
:type fmt: str
894892
893+
:param locale: The locale to use
894+
:type locale: str or None
895+
896+
:param formatter: The formatter to use
897+
:type formatter: str or None
898+
895899
:rtype: str
896900
"""
897-
if not locale:
898-
locale = self.get_locale()
899-
900-
# Checking for custom formatters
901-
fmt = self._FORMATTERS_REGEX.sub(lambda m: self._strftime(m, locale), fmt)
901+
if formatter is None:
902+
formatter = 'classic'
902903

903-
# Checking for localizable directives
904-
fmt = re.sub('%(a|A|b|B|p)', lambda m: self._localize_directive(m.group(1), locale), fmt)
904+
if formatter not in FORMATTERS:
905+
raise ValueError('Invalid formatter [{}]'.format(formatter))
905906

906-
return self._datetime.strftime(fmt)
907+
return FORMATTERS[formatter].format(self, fmt, locale)
907908

908909
def strftime(self, fmt):
909910
"""
@@ -914,78 +915,7 @@ def strftime(self, fmt):
914915
915916
:rtype: str
916917
"""
917-
# Checking for custom formatters
918-
fmt = self._FORMATTERS_REGEX.sub(self._strftime, fmt)
919-
920-
return self._datetime.strftime(fmt)
921-
922-
def _localize_directive(self, directive, locale):
923-
"""
924-
Localize a native strftime directive.
925-
926-
:param directive: The directive to localize
927-
:type directive: str
928-
929-
:param locale: The locale to use for localization
930-
:type locale: str
931-
932-
:rtype: str
933-
"""
934-
if directive == 'a':
935-
id = 'days_abbrev'
936-
number = self.day_of_week
937-
elif directive == 'A':
938-
id = 'days'
939-
number = self.day_of_week
940-
elif directive == 'b':
941-
id = 'months_abbrev'
942-
number = self.month
943-
elif directive == 'B':
944-
id = 'months'
945-
number = self.month
946-
elif directive == 'p':
947-
id = 'meridian'
948-
number = self.hour
949-
else:
950-
raise ValueError('Unlocalizable directive [{}]'.format(directive))
951-
952-
translation = self.translator().transchoice(id, number, locale=locale)
953-
if translation == id:
954-
return ''
955-
956-
return translation
957-
958-
def _strftime(self, m, locale=None):
959-
"""
960-
Handles custom formatters in format string.
961-
962-
:return: str
963-
"""
964-
if not locale:
965-
locale = _locale.getlocale()[0]
966-
967-
fmt = m.group(1)
968-
969-
if fmt == '_z':
970-
offset = self._datetime.utcoffset() or datetime.timedelta()
971-
minutes = offset.total_seconds() / 60
972-
973-
if minutes >= 0:
974-
sign = '+'
975-
else:
976-
sign = '-'
977-
978-
hour, minute = divmod(abs(int(minutes)), 60)
979-
980-
return '{0}{1:02d}:{2:02d}'.format(sign, hour, minute)
981-
elif fmt == '_t':
982-
translation = self.translator().transchoice('ordinal', self.day, locale=locale)
983-
if translation == 'ordinal':
984-
translation = ''
985-
986-
return translation
987-
988-
raise ValueError('Unknown formatter %%{}'.format(fmt))
918+
return self.format(fmt, _locale.getlocale()[0], 'classic')
989919

990920
def __str__(self):
991921
if self._to_string_format is None:

tests/formatting_tests/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# -*- coding: utf-8 -*-
2+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from pendulum import Pendulum
4+
from pendulum.formatting.classic_formatter import ClassicFormatter
5+
from .. import AbstractTestCase
6+
7+
8+
class ClassicFormatterTest(AbstractTestCase):
9+
10+
def test_custom_formatters(self):
11+
d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local')
12+
f = ClassicFormatter()
13+
self.assertEqual(
14+
'Thursday 25th of December 1975 02:15:16 PM -05:00',
15+
f.format(d, '%A %-d%_t of %B %Y %I:%M:%S %p %_z')
16+
)
17+
18+
def test_format_with_locale(self):
19+
d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local')
20+
f = ClassicFormatter()
21+
self.assertEqual(
22+
'jeudi 25e jour de décembre 1975 02:15:16 -05:00',
23+
f.format(d, '%A %-d%_t jour de %B %Y %I:%M:%S %p %_z', locale='fr')
24+
)
25+
26+
def test_unlocalizable_directive(self):
27+
d = Pendulum(1975, 12, 25, 14, 15, 16, tzinfo='local')
28+
f = ClassicFormatter()
29+
self.assertRaises(ValueError, f._localize_directive, d, '%8', 'en')

0 commit comments

Comments
 (0)