bpo-15873: Implement [date][time].fromisoformat (#4699) · python/cpython@09dc2f5 (original) (raw)

`@@ -173,6 +173,24 @@ def _format_time(hh, mm, ss, us, timespec='auto'):

`

173

173

`else:

`

174

174

`return fmt.format(hh, mm, ss, us)

`

175

175

``

``

176

`+

def _format_offset(off):

`

``

177

`+

s = ''

`

``

178

`+

if off is not None:

`

``

179

`+

if off.days < 0:

`

``

180

`+

sign = "-"

`

``

181

`+

off = -off

`

``

182

`+

else:

`

``

183

`+

sign = "+"

`

``

184

`+

hh, mm = divmod(off, timedelta(hours=1))

`

``

185

`+

mm, ss = divmod(mm, timedelta(minutes=1))

`

``

186

`+

s += "%s%02d:%02d" % (sign, hh, mm)

`

``

187

`+

if ss or ss.microseconds:

`

``

188

`+

s += ":%02d" % ss.seconds

`

``

189

+

``

190

`+

if ss.microseconds:

`

``

191

`+

s += '.%06d' % ss.microseconds

`

``

192

`+

return s

`

``

193

+

176

194

`# Correctly substitute for %z and %Z escapes in strftime formats.

`

177

195

`def _wrap_strftime(object, format, timetuple):

`

178

196

`# Don't call utcoffset() or tzname() unless actually needed.

`

`@@ -237,6 +255,102 @@ def _wrap_strftime(object, format, timetuple):

`

237

255

`newformat = "".join(newformat)

`

238

256

`return _time.strftime(newformat, timetuple)

`

239

257

``

``

258

`+

Helpers for parsing the result of isoformat()

`

``

259

`+

def _parse_isoformat_date(dtstr):

`

``

260

`+

It is assumed that this function will only be called with a

`

``

261

`+

string of length exactly 10, and (though this is not used) ASCII-only

`

``

262

`+

year = int(dtstr[0:4])

`

``

263

`+

if dtstr[4] != '-':

`

``

264

`+

raise ValueError('Invalid date separator: %s' % dtstr[4])

`

``

265

+

``

266

`+

month = int(dtstr[5:7])

`

``

267

+

``

268

`+

if dtstr[7] != '-':

`

``

269

`+

raise ValueError('Invalid date separator')

`

``

270

+

``

271

`+

day = int(dtstr[8:10])

`

``

272

+

``

273

`+

return [year, month, day]

`

``

274

+

``

275

`+

def _parse_hh_mm_ss_ff(tstr):

`

``

276

`+

Parses things of the form HH[:MM[:SS[.fff[fff]]]]

`

``

277

`+

len_str = len(tstr)

`

``

278

+

``

279

`+

time_comps = [0, 0, 0, 0]

`

``

280

`+

pos = 0

`

``

281

`+

for comp in range(0, 3):

`

``

282

`+

if (len_str - pos) < 2:

`

``

283

`+

raise ValueError('Incomplete time component')

`

``

284

+

``

285

`+

time_comps[comp] = int(tstr[pos:pos+2])

`

``

286

+

``

287

`+

pos += 2

`

``

288

`+

next_char = tstr[pos:pos+1]

`

``

289

+

``

290

`+

if not next_char or comp >= 2:

`

``

291

`+

break

`

``

292

+

``

293

`+

if next_char != ':':

`

``

294

`+

raise ValueError('Invalid time separator: %c' % next_char)

`

``

295

+

``

296

`+

pos += 1

`

``

297

+

``

298

`+

if pos < len_str:

`

``

299

`+

if tstr[pos] != '.':

`

``

300

`+

raise ValueError('Invalid microsecond component')

`

``

301

`+

else:

`

``

302

`+

pos += 1

`

``

303

+

``

304

`+

len_remainder = len_str - pos

`

``

305

`+

if len_remainder not in (3, 6):

`

``

306

`+

raise ValueError('Invalid microsecond component')

`

``

307

+

``

308

`+

time_comps[3] = int(tstr[pos:])

`

``

309

`+

if len_remainder == 3:

`

``

310

`+

time_comps[3] *= 1000

`

``

311

+

``

312

`+

return time_comps

`

``

313

+

``

314

`+

def _parse_isoformat_time(tstr):

`

``

315

`+

Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]

`

``

316

`+

len_str = len(tstr)

`

``

317

`+

if len_str < 2:

`

``

318

`+

raise ValueError('Isoformat time too short')

`

``

319

+

``

320

`+

This is equivalent to re.search('[+-]', tstr), but faster

`

``

321

`+

tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1)

`

``

322

`+

timestr = tstr[:tz_pos-1] if tz_pos > 0 else tstr

`

``

323

+

``

324

`+

time_comps = _parse_hh_mm_ss_ff(timestr)

`

``

325

+

``

326

`+

tzi = None

`

``

327

`+

if tz_pos > 0:

`

``

328

`+

tzstr = tstr[tz_pos:]

`

``

329

+

``

330

`+

Valid time zone strings are:

`

``

331

`+

HH:MM len: 5

`

``

332

`+

HH:MM:SS len: 8

`

``

333

`+

HH:MM:SS.ffffff len: 15

`

``

334

+

``

335

`+

if len(tzstr) not in (5, 8, 15):

`

``

336

`+

raise ValueError('Malformed time zone string')

`

``

337

+

``

338

`+

tz_comps = _parse_hh_mm_ss_ff(tzstr)

`

``

339

`+

if all(x == 0 for x in tz_comps):

`

``

340

`+

tzi = timezone.utc

`

``

341

`+

else:

`

``

342

`+

tzsign = -1 if tstr[tz_pos - 1] == '-' else 1

`

``

343

+

``

344

`+

td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],

`

``

345

`+

seconds=tz_comps[2], microseconds=tz_comps[3])

`

``

346

+

``

347

`+

tzi = timezone(tzsign * td)

`

``

348

+

``

349

`+

time_comps.append(tzi)

`

``

350

+

``

351

`+

return time_comps

`

``

352

+

``

353

+

240

354

`# Just raise TypeError if the arg isn't None or a string.

`

241

355

`def _check_tzname(name):

`

242

356

`if name is not None and not isinstance(name, str):

`

`@@ -732,6 +846,19 @@ def fromordinal(cls, n):

`

732

846

`y, m, d = _ord2ymd(n)

`

733

847

`return cls(y, m, d)

`

734

848

``

``

849

`+

@classmethod

`

``

850

`+

def fromisoformat(cls, date_string):

`

``

851

`+

"""Construct a date from the output of date.isoformat()."""

`

``

852

`+

if not isinstance(date_string, str):

`

``

853

`+

raise TypeError('fromisoformat: argument must be str')

`

``

854

+

``

855

`+

try:

`

``

856

`+

assert len(date_string) == 10

`

``

857

`+

return cls(*_parse_isoformat_date(date_string))

`

``

858

`+

except Exception:

`

``

859

`+

raise ValueError('Invalid isoformat string: %s' % date_string)

`

``

860

+

``

861

+

735

862

`# Conversions to string

`

736

863

``

737

864

`def repr(self):

`

`@@ -1190,22 +1317,10 @@ def hash(self):

`

1190

1317

``

1191

1318

`# Conversion to string

`

1192

1319

``

1193

``

`-

def _tzstr(self, sep=":"):

`

1194

``

`-

"""Return formatted timezone offset (+xx:xx) or None."""

`

``

1320

`+

def _tzstr(self):

`

``

1321

`+

"""Return formatted timezone offset (+xx:xx) or an empty string."""

`

1195

1322

`off = self.utcoffset()

`

1196

``

`-

if off is not None:

`

1197

``

`-

if off.days < 0:

`

1198

``

`-

sign = "-"

`

1199

``

`-

off = -off

`

1200

``

`-

else:

`

1201

``

`-

sign = "+"

`

1202

``

`-

hh, mm = divmod(off, timedelta(hours=1))

`

1203

``

`-

mm, ss = divmod(mm, timedelta(minutes=1))

`

1204

``

`-

assert 0 <= hh < 24

`

1205

``

`-

off = "%s%02d%s%02d" % (sign, hh, sep, mm)

`

1206

``

`-

if ss:

`

1207

``

`-

off += ':%02d' % ss.seconds

`

1208

``

`-

return off

`

``

1323

`+

return _format_offset(off)

`

1209

1324

``

1210

1325

`def repr(self):

`

1211

1326

`"""Convert to formal string, for repr()."""

`

`@@ -1244,6 +1359,18 @@ def isoformat(self, timespec='auto'):

`

1244

1359

``

1245

1360

`str = isoformat

`

1246

1361

``

``

1362

`+

@classmethod

`

``

1363

`+

def fromisoformat(cls, time_string):

`

``

1364

`+

"""Construct a time from the output of isoformat()."""

`

``

1365

`+

if not isinstance(time_string, str):

`

``

1366

`+

raise TypeError('fromisoformat: argument must be str')

`

``

1367

+

``

1368

`+

try:

`

``

1369

`+

return cls(*_parse_isoformat_time(time_string))

`

``

1370

`+

except Exception:

`

``

1371

`+

raise ValueError('Invalid isoformat string: %s' % time_string)

`

``

1372

+

``

1373

+

1247

1374

`def strftime(self, fmt):

`

1248

1375

`"""Format using strftime(). The date part of the timestamp passed

`

1249

1376

` to underlying strftime should not be used.

`

`@@ -1497,6 +1624,31 @@ def combine(cls, date, time, tzinfo=True):

`

1497

1624

`time.hour, time.minute, time.second, time.microsecond,

`

1498

1625

`tzinfo, fold=time.fold)

`

1499

1626

``

``

1627

`+

@classmethod

`

``

1628

`+

def fromisoformat(cls, date_string):

`

``

1629

`+

"""Construct a datetime from the output of datetime.isoformat()."""

`

``

1630

`+

if not isinstance(date_string, str):

`

``

1631

`+

raise TypeError('fromisoformat: argument must be str')

`

``

1632

+

``

1633

`+

Split this at the separator

`

``

1634

`+

dstr = date_string[0:10]

`

``

1635

`+

tstr = date_string[11:]

`

``

1636

+

``

1637

`+

try:

`

``

1638

`+

date_components = _parse_isoformat_date(dstr)

`

``

1639

`+

except ValueError:

`

``

1640

`+

raise ValueError('Invalid isoformat string: %s' % date_string)

`

``

1641

+

``

1642

`+

if tstr:

`

``

1643

`+

try:

`

``

1644

`+

time_components = _parse_isoformat_time(tstr)

`

``

1645

`+

except ValueError:

`

``

1646

`+

raise ValueError('Invalid isoformat string: %s' % date_string)

`

``

1647

`+

else:

`

``

1648

`+

time_components = [0, 0, 0, 0, None]

`

``

1649

+

``

1650

`+

return cls(*(date_components + time_components))

`

``

1651

+

1500

1652

`def timetuple(self):

`

1501

1653

`"Return local time tuple compatible with time.localtime()."

`

1502

1654

`dst = self.dst()

`

`@@ -1673,18 +1825,10 @@ def isoformat(self, sep='T', timespec='auto'):

`

1673

1825

`self._microsecond, timespec))

`

1674

1826

``

1675

1827

`off = self.utcoffset()

`

1676

``

`-

if off is not None:

`

1677

``

`-

if off.days < 0:

`

1678

``

`-

sign = "-"

`

1679

``

`-

off = -off

`

1680

``

`-

else:

`

1681

``

`-

sign = "+"

`

1682

``

`-

hh, mm = divmod(off, timedelta(hours=1))

`

1683

``

`-

mm, ss = divmod(mm, timedelta(minutes=1))

`

1684

``

`-

s += "%s%02d:%02d" % (sign, hh, mm)

`

1685

``

`-

if ss:

`

1686

``

`-

assert not ss.microseconds

`

1687

``

`-

s += ":%02d" % ss.seconds

`

``

1828

`+

tz = _format_offset(off)

`

``

1829

`+

if tz:

`

``

1830

`+

s += tz

`

``

1831

+

1688

1832

`return s

`

1689

1833

``

1690

1834

`def repr(self):

`

`@@ -2275,9 +2419,10 @@ def _name_from_offset(delta):

`

2275

2419

`_check_date_fields, _check_int_field, _check_time_fields,

`

2276

2420

`_check_tzinfo_arg, _check_tzname, _check_utc_offset, _cmp, _cmperror,

`

2277

2421

`_date_class, _days_before_month, _days_before_year, _days_in_month,

`

2278

``

`-

_format_time, _is_leap, _isoweek1monday, _math, _ord2ymd,

`

2279

``

`-

_time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord,

`

2280

``

`-

_divide_and_round)

`

``

2422

`+

_format_time, _format_offset, _is_leap, _isoweek1monday, _math,

`

``

2423

`+

_ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord,

`

``

2424

`+

_divide_and_round, _parse_isoformat_date, _parse_isoformat_time,

`

``

2425

`+

_parse_hh_mm_ss_ff)

`

2281

2426

`# XXX Since import * above excludes names that start with _,

`

2282

2427

`# docstring does not get overwritten. In the future, it may be

`

2283

2428

`# appropriate to maintain a single module level docstring and

`