Skip to content

Commit 0615db0

Browse files
committed
Merge branch 'develop'
2 parents e23d439 + a8378e0 commit 0615db0

File tree

15 files changed

+425
-32
lines changed

15 files changed

+425
-32
lines changed

.travis.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@ matrix:
66
env: PENDULUM_EXTENSIONS=1
77
- python: 2.7
88
env: PENDULUM_EXTENSIONS=0
9-
- python: 3.2
10-
env: PENDULUM_EXTENSIONS=1
11-
- python: 3.2
12-
env: PENDULUM_EXTENSIONS=0
13-
- python: 3.3
14-
env: PENDULUM_EXTENSIONS=1
15-
- python: 3.3
16-
env: PENDULUM_EXTENSIONS=0
179
- python: 3.4
1810
env: PENDULUM_EXTENSIONS=1
1911
- python: 3.4

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# Change Log
22

3+
## [Unreleased]
4+
5+
### Added
6+
7+
- Added support for the alternative formatter's tokens in `from_format()`.
8+
- Added a `timezones` module attribute to expose available timezones.
9+
- Added the `exact` keyword to `parse()` which behaves exactly like `strict`.
10+
11+
### Changed
12+
13+
- Dropped support for Python 3.2 and 3.3.
14+
- The `classic` formatter in `from_format()` is now deprecated.
15+
16+
### Fixed
17+
18+
- Fixed `th` locale. (Thanks to [idxn](https://github.com/idxn))
19+
20+
321
## [1.2.5] - 2017-09-04
422

523
### Fixed
@@ -394,7 +412,7 @@ Initial release
394412

395413

396414

397-
[Unreleased]: https://github.com/sdispater/pendulum/compare/1.2.5...master
415+
[Unreleased]: https://github.com/sdispater/pendulum/compare/master...develop
398416
[1.2.5]: https://github.com/sdispater/pendulum/releases/tag/1.2.5
399417
[1.2.4]: https://github.com/sdispater/pendulum/releases/tag/1.2.4
400418
[1.2.3]: https://github.com/sdispater/pendulum/releases/tag/1.2.3

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Pendulum
1616

1717
Python datetimes made easy.
1818

19-
Supports Python **2.7+**, **3.2+** and **PyPy**.
19+
Supports Python **2.7+**, **3.4+** and **PyPy**.
2020

2121

2222
.. code-block:: python

appveyor.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ environment:
44
matrix:
55
- PYTHON: "C:/Python27"
66
- PYTHON: "C:/Python27-x64"
7-
- PYTHON: "C:/Python33"
8-
- PYTHON: "C:/Python33-x64"
97
- PYTHON: "C:/Python34"
108
- PYTHON: "C:/Python34-x64"
119
- PYTHON: "C:/Python35"

docs/_docs/instantiation.rst

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ and the timezone will be created for you.
2424
Supported strings for timezones are the one provided by the `IANA time zone database <https://www.iana.org/time-zones>`_.
2525
The special ``local`` string is also supported and will return your current timezone.
2626

27+
As of release 1.3.0, available timezones are exposed via the ``timezones`` attribute.
28+
29+
.. code-block:: python
30+
31+
import pendulum
32+
33+
pendulum.timezones
34+
('CET',
35+
'CST6CDT',
36+
'Cuba',
37+
'EET',
38+
'Egypt',
39+
'Eire',
40+
...,
41+
'US/Michigan',
42+
'US/Mountain',
43+
'US/Pacific',
44+
'US/Pacific-New',
45+
'US/Samoa')
46+
2747
This is again shown in the next example which also introduces the ``now()`` function.
2848

2949
.. code-block:: python
@@ -90,12 +110,25 @@ The difference being the addition the ``tz`` argument that can be a ``tzinfo`` i
90110
91111
pendulum.from_format('1975-05-21 22', '%Y-%m-%d %H').to_datetime_string()
92112
'1975-05-21 22:00:00'
93-
pendulum.from_format('1975-05-21 22', '%Y-%m-%d %H', 'Europe/London').isoformat()
113+
pendulum.from_format('1975-05-21 22', '%Y-%m-%d %H', tz='Europe/London').isoformat()
94114
'1975-05-21T22:00:00+01:00'
95115
96116
# Using strptime is also possible (the timezone will be UTC)
97117
pendulum.strptime('1975-05-21 22', '%Y-%m-%d %H').isoformat()
98118
119+
.. note::
120+
121+
``from_format()`` also accepts a ``formatter`` keyword argument to use the
122+
`Alternative Formatter`_'s tokens.
123+
124+
.. code-block:: python
125+
126+
import pendulum
127+
128+
pendulum.from_format('1975-05-21 22', 'YYYY-MM-DD HH', formatter='alternative')
129+
130+
Note that it will be the only one supported in the next major version.
131+
99132
The final ``create`` function is for working with unix timestamps.
100133
``from_timestamp()`` will create a ``Pendulum`` instance equal to the given timestamp
101134
and will set the timezone as well or default it to ``UTC``.
@@ -241,15 +274,15 @@ When passing only time information the date will default to today.
241274

242275
.. note::
243276

244-
You can pass the ``strict`` keyword argument to ``parse()`` to get the exact type
277+
You can pass the ``exact`` keyword argument to ``parse()`` to get the exact type
245278
that the string represents:
246279

247280
.. code-block:: python
248281
249282
import pendulum
250283
251-
pendulum.parse('2012-05-03', strict=True)
284+
pendulum.parse('2012-05-03', exact=True)
252285
# <Date [2012-05-03]>
253286
254-
pendulum.parse('12:04:23', strict=True)
287+
pendulum.parse('12:04:23', exact=True)
255288
# <Time [12:04:23]>

pendulum/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,4 @@
7777
period = Period
7878

7979
# Timezones
80-
from .tz import timezone, local_timezone, UTC
80+
from .tz import timezone, timezones, local_timezone, UTC

pendulum/formatting/alternative_formatter.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@
66
from .formatter import Formatter
77

88

9+
_MATCH_1 = re.compile('\d')
10+
_MATCH_2 = re.compile('\d\d')
11+
_MATCH_3 = re.compile('\d{3}')
12+
_MATCH_4 = re.compile('\d{4}')
13+
_MATCH_6 = re.compile('[+-]?\d{6}')
14+
_MATCH_1_TO_2 = re.compile('\d\d?')
15+
_MATCH_1_TO_3 = re.compile('\d{1,3}')
16+
_MATCH_1_TO_4 = re.compile('\d{1,4}')
17+
_MATCH_1_TO_6 = re.compile('[+-]?\d{1,6}')
18+
_MATCH_3_TO_4 = re.compile('\d{3}\d?')
19+
_MATCH_5_TO_6 = re.compile('\d{5}\d?')
20+
_MATCH_UNSIGNED = re.compile('\d+')
21+
_MATCH_SIGNED = re.compile('[+-]?\d+')
22+
_MATCH_OFFSET = re.compile('(?i)Z|[+-]\d\d:?\d\d')
23+
_MATCH_SHORT_OFFSET = re.compile('(?i)Z|[+-]\d\d(?::?\d\d)?')
24+
_MATCH_TIMESTAMP = re.compile('[+-]?\d+(\.\d{1,3})?')
25+
_MATCH_WORD = re.compile("[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}")
26+
27+
28+
929
class AlternativeFormatter(Formatter):
1030

1131
_TOKENS = '\[([^\[]*)\]|\\\(.)|' \
@@ -99,6 +119,71 @@ class AlternativeFormatter(Formatter):
99119
'LLLL': 'dddd, MMMM D, YYYY h:mm A',
100120
}
101121

122+
_REGEX_TOKENS = {
123+
'Y': _MATCH_SIGNED,
124+
'YY': (_MATCH_1_TO_2, _MATCH_2),
125+
'YYYY': (_MATCH_1_TO_4, _MATCH_4),
126+
'Q': _MATCH_1,
127+
'Qo': None,
128+
'M': _MATCH_1_TO_2,
129+
'MM': (_MATCH_1_TO_2, _MATCH_2),
130+
'MMM': None,
131+
'MMMM': None,
132+
'D': _MATCH_1_TO_2,
133+
'DD': (_MATCH_1_TO_2, _MATCH_2),
134+
'DDD': _MATCH_1_TO_3,
135+
'DDDD': _MATCH_3,
136+
'Do': None,
137+
'H': _MATCH_1_TO_2,
138+
'HH': (_MATCH_1_TO_2, _MATCH_2),
139+
'h': _MATCH_1_TO_2,
140+
'hh': (_MATCH_1_TO_2, _MATCH_2),
141+
'm': _MATCH_1_TO_2,
142+
'mm': (_MATCH_1_TO_2, _MATCH_2),
143+
's': _MATCH_1_TO_2,
144+
'ss': (_MATCH_1_TO_2, _MATCH_2),
145+
'S': (_MATCH_1_TO_3, _MATCH_1),
146+
'SS': (_MATCH_1_TO_3, _MATCH_2),
147+
'SSS': (_MATCH_1_TO_3, _MATCH_3),
148+
'SSSS': _MATCH_UNSIGNED,
149+
'SSSSS': _MATCH_UNSIGNED,
150+
'SSSSSS': _MATCH_UNSIGNED,
151+
'a': None,
152+
'x': _MATCH_SIGNED,
153+
'X': re.compile('[+-]?\d+(\.\d{1,3})?')
154+
}
155+
156+
_PARSE_TOKENS = {
157+
'YYYY': lambda year: int(year),
158+
'YY': lambda year: 1900 + int(year),
159+
'Q': lambda quarter: int(quarter),
160+
'MMMM': lambda month: None,
161+
'MMM': lambda month: None,
162+
'MM': lambda month: int(month),
163+
'M': lambda month: int(month),
164+
'DDDD': lambda day: int(day),
165+
'DDD': lambda day: int(day),
166+
'DD': lambda day: int(day),
167+
'D': lambda day: int(day),
168+
'HH': lambda hour: int(hour),
169+
'H': lambda hour: int(hour),
170+
'hh': lambda hour: int(hour),
171+
'h': lambda hour: int(hour),
172+
'mm': lambda minute: int(minute),
173+
'm': lambda minute: int(minute),
174+
'ss': lambda second: int(second),
175+
's': lambda second: int(second),
176+
'S': lambda us: int(us) * 100000,
177+
'SS': lambda us: int(us) * 10000,
178+
'SSS': lambda us: int(us) * 1000,
179+
'SSSS': lambda us: int(us) * 100,
180+
'SSSSS': lambda us: int(us) * 10,
181+
'SSSSSS': lambda us: int(us),
182+
'a': lambda meridiem: None,
183+
'X': lambda ts: float(ts),
184+
'x': lambda ts: float(ts) / 1e3,
185+
}
186+
102187
def format(self, dt, fmt, locale=None):
103188
"""
104189
Formats a Pendulum instance with a given format and locale.
@@ -232,3 +317,97 @@ def _format_localizable_token(self, dt, token, locale):
232317
return self._format_localizable_token(dt, token, 'en')
233318

234319
return trans
320+
321+
def parse(self, time, fmt):
322+
"""
323+
Parses a time string matching a given format as a tuple.
324+
325+
:param time: The timestring
326+
:type time: str
327+
328+
:param fmt: The format
329+
:type fmt: str
330+
331+
:rtype: tuple
332+
"""
333+
tokens = self._FORMAT_RE.findall(fmt)
334+
if not tokens:
335+
return time
336+
337+
parsed = {
338+
'year': None,
339+
'month': None,
340+
'day': None,
341+
'hour': None,
342+
'minute': None,
343+
'second': None,
344+
'microsecond': None,
345+
'tz': None,
346+
'quarter': None,
347+
'day_of_week': None,
348+
'day_of_year': None,
349+
'meridiem': None,
350+
'timestamp': None
351+
}
352+
353+
pattern = self._FORMAT_RE.sub(lambda m: self._replace_tokens(m.group(0)), fmt)
354+
355+
if not re.match(pattern, time):
356+
raise ValueError('String does not match format {}'.format(fmt))
357+
358+
re.sub(pattern, lambda m: self._get_parsed_values(m, parsed), time)
359+
360+
return parsed
361+
362+
def _get_parsed_values(self, m, parsed):
363+
for token, index in m.re.groupindex.items():
364+
self._get_parsed_value(token, m.group(index), parsed)
365+
366+
def _get_parsed_value(self, token, value, parsed):
367+
parsed_token = self._PARSE_TOKENS[token](value)
368+
369+
if 'Y' in token:
370+
parsed['year'] = parsed_token
371+
elif 'Q' == token:
372+
parsed['quarter'] = parsed_token
373+
elif 'M' in token:
374+
parsed['month'] = parsed_token
375+
elif token in ['DDDD', 'DDD']:
376+
parsed['day_of_year'] = parsed_token
377+
elif 'D' in token:
378+
parsed['day'] = parsed_token
379+
elif 'H' in token:
380+
parsed['hour'] = parsed_token
381+
elif token in ['hh', 'h']:
382+
parsed['hour'] = parsed_token
383+
elif 'm' in token:
384+
parsed['minute'] = parsed_token
385+
elif 's' in token:
386+
parsed['second'] = parsed_token
387+
elif 'S' in token:
388+
parsed['microsecond'] = parsed_token
389+
elif token in ['MMM', 'MMMM']:
390+
parsed['day_of_week'] = parsed_token
391+
elif token == 'a':
392+
pass
393+
elif token in ['X', 'x']:
394+
parsed['timestamp'] = parsed_token
395+
396+
def _replace_tokens(self, token):
397+
if token.startswith('[') and token.endswith(']'):
398+
return token[1:-1]
399+
elif token.startswith('\\'):
400+
return token[1:]
401+
elif token not in self._REGEX_TOKENS:
402+
raise ValueError('Unsupported token: {}'.format(token))
403+
404+
candidates = self._REGEX_TOKENS[token]
405+
if not candidates:
406+
raise ValueError('Unsupported token: {}'.format(token))
407+
408+
if not isinstance(candidates, tuple):
409+
candidates = (candidates,)
410+
411+
pattern = '(?P<{}>{})'.format(token, '|'.join([p.pattern for p in candidates]))
412+
413+
return pattern

pendulum/parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def parse(self, text):
3030

3131
parsed = super(Parser, self).parse(text)
3232

33-
if not self.is_strict():
33+
if not self.is_exact():
3434
return self._create_pendulum_object(parsed)
3535

3636
# Checking for date

pendulum/parsing/parser.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import re
44
import copy
5+
import warnings
56

67
from datetime import datetime, date, time
78
from dateutil import parser
@@ -58,16 +59,35 @@ class Parser(object):
5859
DEFAULT_OPTIONS = {
5960
'day_first': False,
6061
'year_first': True,
61-
'strict': False,
62+
'exact': False,
6263
'now': None
6364
}
6465

6566
def __init__(self, **options):
67+
if 'strict' in options:
68+
warnings.warn(
69+
'The "strict" keyword when parsing will have '
70+
'another meaning in version 2.0. Use "exact" instead.',
71+
DeprecationWarning,
72+
stacklevel=2
73+
)
74+
75+
options['exact'] = options['strict']
76+
6677
self._options = copy.copy(self.DEFAULT_OPTIONS)
6778
self._options.update(options)
6879

6980
def is_strict(self):
70-
return self._options['strict']
81+
warnings.warn(
82+
'is_strict() is deprecated. Use is_exact() instead.',
83+
DeprecationWarning,
84+
stacklevel=2
85+
)
86+
87+
return self.is_exact()
88+
89+
def is_exact(self):
90+
return self._options['exact']
7191

7292
def now(self):
7393
return self._options['now'] or datetime.now()

0 commit comments

Comments
 (0)