Ticket #2651: 2651.patch

File 2651.patch, 9.3 KB (added by thomasvs, 12 years ago)

reworked patch, integrating into amp.py, with unit tests

  • twisted/test/test_amp.py

     
    66Tests for L{twisted.protocols.amp}.
    77"""
    88
     9import datetime
     10import decimal
     11
    912from zope.interface.verify import verifyObject
    1013
    1114from twisted.python.util import setIDFunction
     
    25952598        "single": [u"foo"],
    25962599        "multiple": [u"\N{SNOWMAN}", u"Hello", u"world"]}
    25972600
     2601class ListOfDecimalTests(unittest.TestCase, ListOfTestsMixin):
     2602    """
     2603    Tests for L{ListOf} combined with L{decimal.Decimal}.
     2604    """
     2605    elementType = amp.Decimal()
    25982606
     2607    strings = {
     2608        "empty": "",
     2609        "single": "\x00\x031.1",
     2610        "extreme": "\x00\x08Infinity\x00\x09-Infinity",
     2611        "scientist": "\x00\x083.141E+5\x00\x0a0.00003141\x00\x083.141E-7"
     2612                     "\x00\x09-3.141E+5\x00\x0b-0.00003141\x00\x09-3.141E-7",
     2613        "engineer": "\x00\x04%s\x00\x06%s" % (
     2614            decimal.Decimal("0e6").to_eng_string(),
     2615            decimal.Decimal("1.5E-9").to_eng_string()),
     2616    }
    25992617
     2618    objects = {
     2619        "empty": [],
     2620        "single": [decimal.Decimal("1.1")],
     2621        "extreme": [
     2622            decimal.Decimal("Infinity"),
     2623            decimal.Decimal("-Infinity"),
     2624        ],
     2625        # exarkun objected to AMP supporting engineering notation because
     2626        # it was redundant, until we realised that 1E6 has less precision
     2627        # than 1000000 and is represented differently.  But they compare
     2628        # and even hash equally.  There were tears.
     2629        "scientist": [
     2630            decimal.Decimal("3.141E5"),
     2631            decimal.Decimal("3.141e-5"),
     2632            decimal.Decimal("3.141E-7"),
     2633            decimal.Decimal("-3.141e5"),
     2634            decimal.Decimal("-3.141E-5"),
     2635            decimal.Decimal("-3.141e-7"),
     2636        ],
     2637        "engineer": [
     2638            decimal.Decimal("0e6"),
     2639            decimal.Decimal("1.5E-9"),
     2640        ],
     2641     }
     2642
     2643# NaN is never equal to itself, so we cannot use the Mixin
     2644class DecimalNanTests(unittest.TestCase, ListOfTestsMixin):
     2645    elementType = amp.Decimal()
     2646
     2647    strings = {
     2648        "nan": "\x00\x03NaN\x00\x04-NaN\x00\x04sNaN\x00\x05-sNaN",
     2649    }
     2650
     2651    objects = {
     2652        "nan": [
     2653            decimal.Decimal("NaN"),
     2654            decimal.Decimal("-NaN"),
     2655            decimal.Decimal("sNaN"),
     2656            decimal.Decimal("-sNaN"),
     2657        ]
     2658    }
     2659
     2660    def test_fromBox(self):
     2661        # since this compares objects and one NaN is not another NaN, we
     2662        # override this method
     2663        """
     2664        L{ListOf.fromBox} reverses the operation performed by L{ListOf.toBox}.
     2665        """
     2666        stringList = amp.ListOf(self.elementType)
     2667        objects = {}
     2668        for key in self.strings:
     2669            stringList.fromBox(key, self.strings.copy(), objects, None)
     2670        n = objects["nan"]
     2671        self.failUnless(n[0].is_qnan() and not n[0].is_signed())
     2672        self.failUnless(n[1].is_qnan() and n[1].is_signed())
     2673        self.failUnless(n[2].is_snan() and not n[2].is_signed())
     2674        self.failUnless(n[3].is_snan() and n[3].is_signed())
     2675
     2676class DateTimeTests(unittest.TestCase, ListOfTestsMixin):
     2677    elementType = amp.DateTime()
     2678
     2679    strings = {
     2680        "christmas":
     2681            "\x00\x202010-12-25T00:00:00.000000-00:00"
     2682            "\x00\x202010-12-25T00:00:00.000000-00:00",
     2683        "christmas in eu": "\x00\x202010-12-25T00:00:00.000000+01:00",
     2684        "christmas in iran": "\x00\x202010-12-25T00:00:00.000000+03:30",
     2685        "christmas in nyc": "\x00\x202010-12-25T00:00:00.000000-05:00",
     2686        "previous tests": "\x00\x202010-12-25T00:00:00.000000+03:19"
     2687                          "\x00\x202010-12-25T00:00:00.000000-06:59",
     2688    }
     2689
     2690    objects = {
     2691        "christmas": [
     2692            datetime.datetime(2010, 12, 25, 0, 0, 0, tzinfo=amp.UTCtzinfo),
     2693            datetime.datetime(2010, 12, 25, 0, 0, 0,
     2694                tzinfo=amp._FixedOffsetTZInfo('+', 0, 0)),
     2695        ],
     2696        "christmas in eu": [
     2697            datetime.datetime(2010, 12, 25, 0, 0, 0,
     2698                tzinfo=amp._FixedOffsetTZInfo('+', 1, 0)),
     2699        ],
     2700        "christmas in iran": [
     2701            datetime.datetime(2010, 12, 25, 0, 0, 0,
     2702                tzinfo=amp._FixedOffsetTZInfo('+', 3, 30)),
     2703        ],
     2704        "christmas in nyc": [
     2705            datetime.datetime(2010, 12, 25, 0, 0, 0,
     2706                tzinfo=amp._FixedOffsetTZInfo('-', 5, 0)),
     2707        ],
     2708        "previous tests": [
     2709            datetime.datetime(2010, 12, 25, 0, 0, 0,
     2710                tzinfo=amp._FixedOffsetTZInfo('+', 3, 19)),
     2711            datetime.datetime(2010, 12, 25, 0, 0, 0,
     2712                tzinfo=amp._FixedOffsetTZInfo('-', 6, 59)),
     2713        ],
     2714    }
     2715
     2716class BadDateTest(unittest.TestCase):
     2717    def test_wrongLength(self):
     2718        d = amp.DateTime()
     2719        self.assertRaises(ValueError, d.fromString, 'abc')
     2720
     2721        d = amp.DateTime()
     2722        self.assertRaises(ValueError, d.toString,
     2723            datetime.datetime(2010, 12, 25, 0, 0, 0))
     2724
     2725
     2726class FixedOffsetTest(unittest.TestCase):
     2727    def test_tzName(self):
     2728        utc = amp.UTCtzinfo
     2729        self.assertEquals(utc.tzname(None), '+00:00')
     2730
     2731    def test_badSign(self):
     2732        self.assertRaises(ValueError, amp._FixedOffsetTZInfo, '?', 0, 0)
     2733
    26002734if not interfaces.IReactorSSL.providedBy(reactor):
    26012735    skipMsg = 'This test case requires SSL support in the reactor'
    26022736    TLSTest.skip = skipMsg
  • twisted/protocols/amp.py

     
    164164
    165165from cStringIO import StringIO
    166166from struct import pack
     167import decimal, datetime, re
    167168
    168169from zope.interface import Interface, implements
    169170
     
    23912392    for argname, argparser in arglist:
    23922393        argparser.toBox(argname, strings, myObjects, proto)
    23932394    return strings
     2395
     2396class _FixedOffsetTZInfo(datetime.tzinfo):
     2397    '''"Timezones? What timezones?" -- Python standard library'''
     2398
     2399    def __init__(self, sign, hours, minutes):
     2400        self.name = '%s%02i:%02i' % (sign, hours, minutes)
     2401        if sign == '-':
     2402            hours = -hours
     2403            minutes = -minutes
     2404        elif sign != '+':
     2405            raise ValueError, 'invalid sign for timezone %r' % (sign,)
     2406        self.offset = datetime.timedelta(hours=hours, minutes=minutes)
     2407
     2408    def utcoffset(self, dt):
     2409        return self.offset
     2410
     2411    def dst(self, dt):
     2412        return datetime.timedelta(0)
     2413
     2414    def tzname(self, dt):
     2415        return self.name
     2416
     2417
     2418UTCtzinfo = _FixedOffsetTZInfo('+', 0, 0)
     2419
     2420
     2421class Decimal(Argument):
     2422    '''Encodes decimal.Decimal.
     2423   
     2424    Encoding on the wire is done with str() and Decimal(), which works great
     2425    for python, but may not be the greatest considering interoperability with
     2426    other decimal types. Someone should look at Decimal.__str__ and see if it
     2427    is specified sufficiently well to be implemented elsewhere.
     2428    '''
     2429    fromString = decimal.Decimal
     2430    def toString(self, inObject):
     2431        return str(decimal.Decimal(inObject))
     2432
     2433
     2434class DateTime(Argument):
     2435    '''Encodes datetime.DateTime.
     2436   
     2437    Wire format: '%04i-%02i-%02iT%02i:%02i:%02i.%06i%s%02i:%02i'. Fields in
     2438    order are: year, month, day, hour, minute, second, microsecond, timezone
     2439    direction (+ or -), timezone hour, timezone minute. Encoded string is
     2440    always exactly 32 characters long. This format is compatible with ISO 8601,
     2441    but that does not mean all ISO 8601 dates can be accepted.
     2442
     2443    Also, note that the datetime module's notion of a "timezone" can be
     2444    complex, but the wire format includes only a fixed offset, so the
     2445    conversion is not lossless. A lossless transmission of a DateTime instance
     2446    is not feasible since the receiving end would require a python interpreter.
     2447    '''
     2448    def fromString(self, s):
     2449        if len(s) != 32:
     2450            raise ValueError, 'invalid date format %r' % (s,)
     2451
     2452        # strptime doesn't have any way to parse the microseconds or timezone
     2453
     2454        date, timeAndZone = s.split('T', 1)
     2455        time, zoneSign, zone = re.split(r'(\+|-)', timeAndZone, 2)
     2456        year, month, day = map(int, date.split('-'))
     2457        hour, minute, secondAndMicrosecond = time.split(':', 2)
     2458        hour = int(hour)
     2459        minute = int(minute)
     2460        second, microsecond = map(int, secondAndMicrosecond.split('.', 1))
     2461        zoneHour, zoneMinute = map(int, zone.split(':', 2))
     2462
     2463        return datetime.datetime(year, month, day, hour, minute, second, microsecond, _FixedOffsetTZInfo(zoneSign, zoneHour, zoneMinute))
     2464
     2465    def toString(self, i):
     2466        offset = i.utcoffset()
     2467        if offset is None:
     2468            raise ValueError, 'datetimes going through AMP must have an associated timezone. You may find amp.UTCtzinfo useful.'
     2469
     2470        minutesOffset = (offset.days * 86400 + offset.seconds) // 60
     2471
     2472        if minutesOffset > 0:
     2473            sign = '+'
     2474        else:
     2475            sign = '-'
     2476
     2477        # strftime has no way to format the microseconds, or put a ':' in the
     2478        # timezone. Suprise!
     2479
     2480        return '%04i-%02i-%02iT%02i:%02i:%02i.%06i%s%02i:%02i' % (
     2481            i.year,
     2482            i.month,
     2483            i.day,
     2484            i.hour,
     2485            i.minute,
     2486            i.second,
     2487            i.microsecond,
     2488            sign,
     2489            abs(minutesOffset) // 60,
     2490            abs(minutesOffset) % 60)