Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pendulum/parsing/__init__.py: 90%

103 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-30 06:11 +0000

1from __future__ import annotations 

2 

3import contextlib 

4import copy 

5import os 

6import re 

7import struct 

8 

9from datetime import date 

10from datetime import datetime 

11from datetime import time 

12from typing import Any 

13from typing import Optional 

14from typing import cast 

15 

16from dateutil import parser 

17 

18from pendulum.parsing.exceptions import ParserError 

19 

20 

21with_extensions = os.getenv("PENDULUM_EXTENSIONS", "1") == "1" 

22 

23try: 

24 if not with_extensions or struct.calcsize("P") == 4: 

25 raise ImportError() 

26 

27 from _pendulum import Duration 

28 from _pendulum import parse_iso8601 

29except ImportError: 

30 from pendulum.duration import Duration # type: ignore[assignment] 

31 from pendulum.parsing.iso8601 import parse_iso8601 # type: ignore[assignment] 

32 

33 

34COMMON = re.compile( 

35 # Date (optional) # noqa: ERA001 

36 "^" 

37 "(?P<date>" 

38 " (?P<classic>" # Classic date (YYYY-MM-DD) 

39 r" (?P<year>\d{4})" # Year 

40 " (?P<monthday>" 

41 r" (?P<monthsep>[/:])?(?P<month>\d{2})" # Month (optional) 

42 r" ((?P<daysep>[/:])?(?P<day>\d{2}))" # Day (optional) 

43 " )?" 

44 " )" 

45 ")?" 

46 # Time (optional) # noqa: ERA001 

47 "(?P<time>" r" (?P<timesep>\ )?" # Separator (space) 

48 # HH:mm:ss (optional mm and ss) 

49 r" (?P<hour>\d{1,2}):(?P<minute>\d{1,2})?(?::(?P<second>\d{1,2}))?" 

50 # Subsecond part (optional) 

51 " (?P<subsecondsection>" 

52 " (?:[.|,])" # Subsecond separator (optional) 

53 r" (?P<subsecond>\d{1,9})" # Subsecond 

54 " )?" 

55 ")?" 

56 "$", 

57 re.VERBOSE, 

58) 

59 

60DEFAULT_OPTIONS = { 

61 "day_first": False, 

62 "year_first": True, 

63 "strict": True, 

64 "exact": False, 

65 "now": None, 

66} 

67 

68 

69def parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration: 

70 """ 

71 Parses a string with the given options. 

72 

73 :param text: The string to parse. 

74 """ 

75 _options: dict[str, Any] = copy.copy(DEFAULT_OPTIONS) 

76 _options.update(options) 

77 

78 return _normalize(_parse(text, **_options), **_options) 

79 

80 

81def _normalize( 

82 parsed: datetime | date | time | _Interval | Duration, **options: Any 

83) -> datetime | date | time | _Interval | Duration: 

84 """ 

85 Normalizes the parsed element. 

86 

87 :param parsed: The parsed elements. 

88 """ 

89 if options.get("exact"): 

90 return parsed 

91 

92 if isinstance(parsed, time): 

93 now = cast(Optional[datetime], options["now"]) or datetime.now() 

94 

95 return datetime( 

96 now.year, 

97 now.month, 

98 now.day, 

99 parsed.hour, 

100 parsed.minute, 

101 parsed.second, 

102 parsed.microsecond, 

103 ) 

104 elif isinstance(parsed, date) and not isinstance(parsed, datetime): 

105 return datetime(parsed.year, parsed.month, parsed.day) 

106 

107 return parsed 

108 

109 

110def _parse(text: str, **options: Any) -> datetime | date | time | _Interval | Duration: 

111 # Trying to parse ISO8601 

112 with contextlib.suppress(ValueError): 

113 return parse_iso8601(text) 

114 

115 with contextlib.suppress(ValueError): 

116 return _parse_iso8601_interval(text) 

117 

118 with contextlib.suppress(ParserError): 

119 return _parse_common(text, **options) 

120 

121 # We couldn't parse the string 

122 # so we fallback on the dateutil parser 

123 # If not strict 

124 if options.get("strict", True): 

125 raise ParserError(f"Unable to parse string [{text}]") 

126 

127 try: 

128 dt = parser.parse( 

129 text, dayfirst=options["day_first"], yearfirst=options["year_first"] 

130 ) 

131 except ValueError: 

132 raise ParserError(f"Invalid date string: {text}") 

133 

134 return dt 

135 

136 

137def _parse_common(text: str, **options: Any) -> datetime | date | time: 

138 """ 

139 Tries to parse the string as a common datetime format. 

140 

141 :param text: The string to parse. 

142 """ 

143 m = COMMON.match(text) 

144 has_date = False 

145 year = 0 

146 month = 1 

147 day = 1 

148 

149 if not m: 

150 raise ParserError("Invalid datetime string") 

151 

152 if m.group("date"): 

153 # A date has been specified 

154 has_date = True 

155 

156 year = int(m.group("year")) 

157 

158 if not m.group("monthday"): 

159 # No month and day 

160 month = 1 

161 day = 1 

162 else: 

163 if options["day_first"]: 

164 month = int(m.group("day")) 

165 day = int(m.group("month")) 

166 else: 

167 month = int(m.group("month")) 

168 day = int(m.group("day")) 

169 

170 if not m.group("time"): 

171 return date(year, month, day) 

172 

173 # Grabbing hh:mm:ss 

174 hour = int(m.group("hour")) 

175 

176 minute = int(m.group("minute")) 

177 

178 second = int(m.group("second")) if m.group("second") else 0 

179 

180 # Grabbing subseconds, if any 

181 microsecond = 0 

182 if m.group("subsecondsection"): 

183 # Limiting to 6 chars 

184 subsecond = m.group("subsecond")[:6] 

185 

186 microsecond = int(f"{subsecond:0<6}") 

187 

188 if has_date: 

189 return datetime(year, month, day, hour, minute, second, microsecond) 

190 

191 return time(hour, minute, second, microsecond) 

192 

193 

194class _Interval: 

195 """ 

196 Special class to handle ISO 8601 intervals 

197 """ 

198 

199 def __init__( 

200 self, 

201 start: datetime | None = None, 

202 end: datetime | None = None, 

203 duration: Duration | None = None, 

204 ) -> None: 

205 self.start = start 

206 self.end = end 

207 self.duration = duration 

208 

209 

210def _parse_iso8601_interval(text: str) -> _Interval: 

211 if "/" not in text: 

212 raise ParserError("Invalid interval") 

213 

214 first, last = text.split("/") 

215 start = end = duration = None 

216 

217 if first[0] == "P": 

218 # duration/end 

219 duration = parse_iso8601(first) 

220 end = parse_iso8601(last) 

221 elif last[0] == "P": 

222 # start/duration 

223 start = parse_iso8601(first) 

224 duration = parse_iso8601(last) 

225 else: 

226 # start/end 

227 start = parse_iso8601(first) 

228 end = parse_iso8601(last) 

229 

230 return _Interval( 

231 cast(datetime, start), cast(datetime, end), cast(Duration, duration) 

232 ) 

233 

234 

235__all__ = ["parse", "parse_iso8601"]