Kaydet (Commit) 6c7a4182 authored tarafından Alexander Belopolsky's avatar Alexander Belopolsky

Closes issue #20858: Enhancements/fixes to pure-python datetime module

This patch brings the pure-python datetime more in-line with the C
module.  Patch contributed by Brian Kearns, a PyPy developer.  PyPy
project has been running these modifications in PyPy2 stdlib.

This commit includes:

- General PEP8/cleanups;
- Better testing of argument types passed to constructors;
- Removal of duplicate operations;
- Optimization of timedelta creation;
- Caching the result of __hash__ like the C accelerator;
- Enhancements/bug fixes in tests.
üst a2f93885
This diff is collapsed.
...@@ -50,6 +50,17 @@ class TestModule(unittest.TestCase): ...@@ -50,6 +50,17 @@ class TestModule(unittest.TestCase):
self.assertEqual(datetime.MINYEAR, 1) self.assertEqual(datetime.MINYEAR, 1)
self.assertEqual(datetime.MAXYEAR, 9999) self.assertEqual(datetime.MAXYEAR, 9999)
def test_name_cleanup(self):
if '_Fast' not in str(self):
return
datetime = datetime_module
names = set(name for name in dir(datetime)
if not name.startswith('__') and not name.endswith('__'))
allowed = set(['MAXYEAR', 'MINYEAR', 'date', 'datetime',
'datetime_CAPI', 'time', 'timedelta', 'timezone',
'tzinfo'])
self.assertEqual(names - allowed, set([]))
############################################################################# #############################################################################
# tzinfo tests # tzinfo tests
...@@ -616,8 +627,12 @@ class TestTimeDelta(HarmlessMixedComparison, unittest.TestCase): ...@@ -616,8 +627,12 @@ class TestTimeDelta(HarmlessMixedComparison, unittest.TestCase):
# Single-field rounding. # Single-field rounding.
eq(td(milliseconds=0.4/1000), td(0)) # rounds to 0 eq(td(milliseconds=0.4/1000), td(0)) # rounds to 0
eq(td(milliseconds=-0.4/1000), td(0)) # rounds to 0 eq(td(milliseconds=-0.4/1000), td(0)) # rounds to 0
eq(td(milliseconds=0.5/1000), td(microseconds=0))
eq(td(milliseconds=-0.5/1000), td(microseconds=0))
eq(td(milliseconds=0.6/1000), td(microseconds=1)) eq(td(milliseconds=0.6/1000), td(microseconds=1))
eq(td(milliseconds=-0.6/1000), td(microseconds=-1)) eq(td(milliseconds=-0.6/1000), td(microseconds=-1))
eq(td(seconds=0.5/10**6), td(microseconds=0))
eq(td(seconds=-0.5/10**6), td(microseconds=0))
# Rounding due to contributions from more than one field. # Rounding due to contributions from more than one field.
us_per_hour = 3600e6 us_per_hour = 3600e6
...@@ -1131,11 +1146,13 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase): ...@@ -1131,11 +1146,13 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
#check that this standard extension works #check that this standard extension works
t.strftime("%f") t.strftime("%f")
def test_format(self): def test_format(self):
dt = self.theclass(2007, 9, 10) dt = self.theclass(2007, 9, 10)
self.assertEqual(dt.__format__(''), str(dt)) self.assertEqual(dt.__format__(''), str(dt))
with self.assertRaisesRegex(TypeError, '^must be str, not int$'):
dt.__format__(123)
# check that a derived class's __str__() gets called # check that a derived class's __str__() gets called
class A(self.theclass): class A(self.theclass):
def __str__(self): def __str__(self):
...@@ -1391,9 +1408,10 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase): ...@@ -1391,9 +1408,10 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
for month_byte in b'9', b'\0', b'\r', b'\xff': for month_byte in b'9', b'\0', b'\r', b'\xff':
self.assertRaises(TypeError, self.theclass, self.assertRaises(TypeError, self.theclass,
base[:2] + month_byte + base[3:]) base[:2] + month_byte + base[3:])
# Good bytes, but bad tzinfo: if issubclass(self.theclass, datetime):
self.assertRaises(TypeError, self.theclass, # Good bytes, but bad tzinfo:
bytes([1] * len(base)), 'EST') with self.assertRaisesRegex(TypeError, '^bad tzinfo state arg$'):
self.theclass(bytes([1] * len(base)), 'EST')
for ord_byte in range(1, 13): for ord_byte in range(1, 13):
# This shouldn't blow up because of the month byte alone. If # This shouldn't blow up because of the month byte alone. If
...@@ -1469,6 +1487,9 @@ class TestDateTime(TestDate): ...@@ -1469,6 +1487,9 @@ class TestDateTime(TestDate):
dt = self.theclass(2007, 9, 10, 4, 5, 1, 123) dt = self.theclass(2007, 9, 10, 4, 5, 1, 123)
self.assertEqual(dt.__format__(''), str(dt)) self.assertEqual(dt.__format__(''), str(dt))
with self.assertRaisesRegex(TypeError, '^must be str, not int$'):
dt.__format__(123)
# check that a derived class's __str__() gets called # check that a derived class's __str__() gets called
class A(self.theclass): class A(self.theclass):
def __str__(self): def __str__(self):
...@@ -1789,6 +1810,7 @@ class TestDateTime(TestDate): ...@@ -1789,6 +1810,7 @@ class TestDateTime(TestDate):
tzinfo=timezone(timedelta(hours=-5), 'EST')) tzinfo=timezone(timedelta(hours=-5), 'EST'))
self.assertEqual(t.timestamp(), self.assertEqual(t.timestamp(),
18000 + 3600 + 2*60 + 3 + 4*1e-6) 18000 + 3600 + 2*60 + 3 + 4*1e-6)
def test_microsecond_rounding(self): def test_microsecond_rounding(self):
for fts in [self.theclass.fromtimestamp, for fts in [self.theclass.fromtimestamp,
self.theclass.utcfromtimestamp]: self.theclass.utcfromtimestamp]:
...@@ -1839,6 +1861,7 @@ class TestDateTime(TestDate): ...@@ -1839,6 +1861,7 @@ class TestDateTime(TestDate):
for insane in -1e200, 1e200: for insane in -1e200, 1e200:
self.assertRaises(OverflowError, self.theclass.utcfromtimestamp, self.assertRaises(OverflowError, self.theclass.utcfromtimestamp,
insane) insane)
@unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps") @unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps")
def test_negative_float_fromtimestamp(self): def test_negative_float_fromtimestamp(self):
# The result is tz-dependent; at least test that this doesn't # The result is tz-dependent; at least test that this doesn't
...@@ -2218,6 +2241,9 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase): ...@@ -2218,6 +2241,9 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase):
t = self.theclass(1, 2, 3, 4) t = self.theclass(1, 2, 3, 4)
self.assertEqual(t.__format__(''), str(t)) self.assertEqual(t.__format__(''), str(t))
with self.assertRaisesRegex(TypeError, '^must be str, not int$'):
t.__format__(123)
# check that a derived class's __str__() gets called # check that a derived class's __str__() gets called
class A(self.theclass): class A(self.theclass):
def __str__(self): def __str__(self):
...@@ -2347,6 +2373,9 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase): ...@@ -2347,6 +2373,9 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase):
for hour_byte in ' ', '9', chr(24), '\xff': for hour_byte in ' ', '9', chr(24), '\xff':
self.assertRaises(TypeError, self.theclass, self.assertRaises(TypeError, self.theclass,
hour_byte + base[1:]) hour_byte + base[1:])
# Good bytes, but bad tzinfo:
with self.assertRaisesRegex(TypeError, '^bad tzinfo state arg$'):
self.theclass(bytes([1] * len(base)), 'EST')
# A mixin for classes with a tzinfo= argument. Subclasses must define # A mixin for classes with a tzinfo= argument. Subclasses must define
# theclass as a class atribute, and theclass(1, 1, 1, tzinfo=whatever) # theclass as a class atribute, and theclass(1, 1, 1, tzinfo=whatever)
...@@ -2606,7 +2635,7 @@ class TestTimeTZ(TestTime, TZInfoBase, unittest.TestCase): ...@@ -2606,7 +2635,7 @@ class TestTimeTZ(TestTime, TZInfoBase, unittest.TestCase):
self.assertRaises(TypeError, t.strftime, "%Z") self.assertRaises(TypeError, t.strftime, "%Z")
# Issue #6697: # Issue #6697:
if '_Fast' in str(type(self)): if '_Fast' in str(self):
Badtzname.tz = '\ud800' Badtzname.tz = '\ud800'
self.assertRaises(ValueError, t.strftime, "%Z") self.assertRaises(ValueError, t.strftime, "%Z")
...@@ -3768,6 +3797,61 @@ class Oddballs(unittest.TestCase): ...@@ -3768,6 +3797,61 @@ class Oddballs(unittest.TestCase):
self.assertEqual(as_datetime, datetime_sc) self.assertEqual(as_datetime, datetime_sc)
self.assertEqual(datetime_sc, as_datetime) self.assertEqual(datetime_sc, as_datetime)
def test_extra_attributes(self):
for x in [date.today(),
time(),
datetime.utcnow(),
timedelta(),
tzinfo(),
timezone(timedelta())]:
with self.assertRaises(AttributeError):
x.abc = 1
def test_check_arg_types(self):
import decimal
class Number:
def __init__(self, value):
self.value = value
def __int__(self):
return self.value
for xx in [decimal.Decimal(10),
decimal.Decimal('10.9'),
Number(10)]:
self.assertEqual(datetime(10, 10, 10, 10, 10, 10, 10),
datetime(xx, xx, xx, xx, xx, xx, xx))
with self.assertRaisesRegex(TypeError, '^an integer is required '
'\(got type str\)$'):
datetime(10, 10, '10')
f10 = Number(10.9)
with self.assertRaisesRegex(TypeError, '^__int__ returned non-int '
'\(type float\)$'):
datetime(10, 10, f10)
class Float(float):
pass
s10 = Float(10.9)
with self.assertRaisesRegex(TypeError, '^integer argument expected, '
'got float$'):
datetime(10, 10, s10)
with self.assertRaises(TypeError):
datetime(10., 10, 10)
with self.assertRaises(TypeError):
datetime(10, 10., 10)
with self.assertRaises(TypeError):
datetime(10, 10, 10.)
with self.assertRaises(TypeError):
datetime(10, 10, 10, 10.)
with self.assertRaises(TypeError):
datetime(10, 10, 10, 10, 10.)
with self.assertRaises(TypeError):
datetime(10, 10, 10, 10, 10, 10.)
with self.assertRaises(TypeError):
datetime(10, 10, 10, 10, 10, 10, 10.)
def test_main(): def test_main():
support.run_unittest(__name__) support.run_unittest(__name__)
......
...@@ -689,6 +689,7 @@ Per Øyvind Karlsen ...@@ -689,6 +689,7 @@ Per Øyvind Karlsen
Anton Kasyanov Anton Kasyanov
Lou Kates Lou Kates
Hiroaki Kawai Hiroaki Kawai
Brian Kearns
Sebastien Keim Sebastien Keim
Ryan Kelly Ryan Kelly
Dan Kenigsberg Dan Kenigsberg
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment