diff --git a/config.py b/config.py new file mode 100644 index 00000000..9686a4ac --- /dev/null +++ b/config.py @@ -0,0 +1,42 @@ +import ConfigParser +import os +import twitter + +class TweetRc(object): + """Get options from .tweetrc file.""" + def __init__(self): + self._config = None + + def GetConsumerKey(self): + return self._GetOption('consumer_key') + + def GetConsumerSecret(self): + return self._GetOption('consumer_secret') + + def GetAccessKey(self): + return self._GetOption('access_key') + + def GetAccessSecret(self): + return self._GetOption('access_secret') + + def _GetOption(self, option): + try: + return self._GetConfig().get('Tweet', option) + except: + return None + + def _GetConfig(self): + if not self._config: + self._config = ConfigParser.ConfigParser() + self._config.read(os.path.expanduser('~/.tweetrc')) + return self._config + +def get_api(): + conf = TweetRc()._GetConfig() + return twitter.Api(consumer_key=conf.get('Tweet', 'consumer_key'), + consumer_secret=conf.get('Tweet', 'consumer_secret'), + access_token_key=conf.get('Tweet', 'access_key'), + access_token_secret=conf.get('Tweet', 'access_secret') + ) + + diff --git a/requirements.txt b/requirements.txt index 1cc432e6..4d9649ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,15 @@ -requests -requests_oauthlib +appnope==0.1.0 +decorator==4.0.2 +gnureadline==6.3.3 +ipython==4.0.0 +ipython-genutils==0.1.0 +oauthlib==1.0.3 +path.py==7.7 +pexpect==3.3 +pickleshare==0.5 +py==1.4.30 +pytest==2.7.2 +requests==2.7.0 +requests-oauthlib==0.5.0 +simplegeneric==0.8.1 +traitlets==4.0.0 diff --git a/test_api/test_control_characters.py b/test_api/test_control_characters.py new file mode 100644 index 00000000..064ad1ed --- /dev/null +++ b/test_api/test_control_characters.py @@ -0,0 +1,60 @@ +# test_control_characters + +import pytest +import random +import struct + +import config +api = config.get_api() +import twitter + +def make_control_character(): + """Generate one random control character.""" + # Add one character made up of one codepoint each from + # (High Surrogates + High Private Use Surrogates) and Low Surrogates. + # We expect each such pair to behave as a single high-codepoint + # character. + controls = ('0000', '001F') + return [unicode_char(char) + for char in range(int(controls[0], 16), int(controls[1], 16)+1)] + +def unicode_char(n): + """Extend `unichr` for all possible Unicode values (n).""" + try: + return unichr(n) + except ValueError: + # Generate bytes object packed as int. + bytes_object = struct.pack('i', n) + # Return decoded w/ utf-32 codec. + return bytes_object.decode('utf-32') + +#################### +# Create parameters +argument_qty = 5 +tweet_prefix = 'Sample tweet with random control character ' +control_strings = make_control_character() + +params = {'argnames': 'tweet', + 'argvalues': [ + tweet_prefix + u'{} (here)'. + format(control_str.encode('utf-8')) + for control_str in control_strings + ] + } + +params['ids'] = ['\n ' + repr(argvalue) for argvalue in params['argvalues']] + +#################### + +@pytest.mark.xfail +@pytest.mark.parametrize(**params) +def test_controls(tweet): + # Post tweet. api.PostUpdate() + status = api.PostUpdate(tweet) + # Validate returned tweet: content and length. + assert status.text == tweet.decode('utf-8') + # Destroy tweet. api.DestroyStatus() +# status2 = api.DestroyStatus(status.id) + # Validate that tweet has been destroyed. +# with pytest.raises(twitter.TwitterError): +# assert api.DestroyStatus(status.id) diff --git a/test_api/test_emoji.py b/test_api/test_emoji.py new file mode 100644 index 00000000..8bbd8381 --- /dev/null +++ b/test_api/test_emoji.py @@ -0,0 +1,237 @@ +# test_emoji.py + +import pytest +import random +import struct + +import config +api = config.get_api() +import twitter + +# Emoiji Unicode numbers collected from +# http://unicode.org/emoji/charts/full-emoji-list.html +emoji = { + 'Basic Multilingual Plane': ['00A9', '00AE', '002A', '2B1B', + '2B1C', '2B05', '2B06', '2B07', '2B50', '2B55', '20E3', + '21A9', '21AA', '0023', '23CF', '23E9', '23EA', '23EB', + '23EC', '23ED', '23EE', '23EF', '23F0', '23F1', '23F2', + '23F3', '23F8', '23F9', '23FA', '24C2', '25AA', '25AB', + '25B6', '25C0', '25FB', '25FC', '25FD', '25FE', '26A0', + '26A1', '26AA', '26AB', '26B0', '26B1', '26BD', '26BE', + '26C4', '26C5', '26C8', '26CE', '26CF', '26D1', '26D3', + '26D4', '26E9', '26EA', '26F0', '26F1', '26F2', '26F3', + '26F4', '26F5', '26F7', '26F8', '26F9', '26FA', '26FD', + '27A1', '27B0', '27BF', '0030', '0031', '0032', '0033', + '0034', '0035', '0036', '0037', '0038', '0039', '203C', + '231A', '231B', '260E', '261D', '262A', '262E', '262F', + '263A', '264A', '264B', '264C', '264D', '264E', '264F', + '267B', '267F', '269B', '269C', '270A', '270B', '270C', + '270D', '270F', '271D', '274C', '274E', '303D', '2049', + '2122', '2139', '2194', '2195', '2196', '2197', '2198', + '2199', '2328', '2600', '2601', '2602', '2603', '2604', + '2611', '2614', '2615', '2618', '2620', '2622', '2623', + '2626', '2638', '2639', '2648', '2649', '2650', '2651', + '2652', '2653', '2660', '2663', '2665', '2666', '2668', + '2692', '2693', '2694', '2696', '2697', '2699', '2702', + '2705', '2708', '2709', '2712', '2714', '2716', '2721', + '2728', '2733', '2734', '2744', '2747', '2753', '2754', + '2755', '2757', '2763', '2764', '2795', '2796', '2797', + '2934', '2935', '3030', '3297', '3299' + ], + 'Supplementary Multilingual Plane': [ + '1F0CF', '1F1E6', '1F1E7', '1F1E8', '1F1E9', '1F1EA', + '1F1EB', '1F1EC', '1F1ED', '1F1EE', '1F1EF', '1F1F0', + '1F1F1', '1F1F2', '1F1F3', '1F1F4', '1F1F5', '1F1F6', + '1F1F7', '1F1F8', '1F1F9', '1F1FA', '1F1FB', '1F1FC', + '1F1FD', '1F1FE', '1F1FF', '1F3A0', '1F3A1', '1F3A2', + '1F3A3', '1F3A4', '1F3A5', '1F3A6', '1F3A7', '1F3A8', + '1F3A9', '1F3AA', '1F3AB', '1F3AC', '1F3AD', '1F3AE', + '1F3AF', '1F3B0', '1F3B1', '1F3B2', '1F3B3', '1F3B4', + '1F3B5', '1F3B6', '1F3B7', '1F3B8', '1F3B9', '1F3BA', + '1F3BB', '1F3BC', '1F3BD', '1F3BE', '1F3BF', '1F3C0', + '1F3C1', '1F3C2', '1F3C3', '1F3C4', '1F3C5', '1F3C6', + '1F3C7', '1F3C8', '1F3C9', '1F3CA', '1F3CB', '1F3CC', + '1F3CD', '1F3CE', '1F3CF', '1F3D0', '1F3D1', '1F3D2', + '1F3D3', '1F3D4', '1F3D5', '1F3D6', '1F3D7', '1F3D8', + '1F3D9', '1F3DA', '1F3DB', '1F3DC', '1F3DD', '1F3DE', + '1F3DF', '1F3E0', '1F3E1', '1F3E2', '1F3E3', '1F3E4', + '1F3E5', '1F3E6', '1F3E7', '1F3E8', '1F3E9', '1F3EA', + '1F3EB', '1F3EC', '1F3ED', '1F3EE', '1F3EF', '1F3F0', + '1F3F3', '1F3F4', '1F3F5', '1F3F7', '1F3F8', '1F3F9', + '1F3FA', '1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF', + '1F004', '1F4A0', '1F4A1', '1F4A2', '1F4A3', '1F4A4', + '1F4A5', '1F4A6', '1F4A7', '1F4A8', '1F4A9', '1F4AA', + '1F4AB', '1F4AC', '1F4AD', '1F4AE', '1F4AF', '1F4B0', + '1F4B1', '1F4B2', '1F4B3', '1F4B4', '1F4B5', '1F4B6', + '1F4B7', '1F4B8', '1F4B9', '1F4BA', '1F4BB', '1F4BC', + '1F4BD', '1F4BE', '1F4BF', '1F4C0', '1F4C1', '1F4C2', + '1F4C3', '1F4C4', '1F4C5', '1F4C6', '1F4C7', '1F4C8', + '1F4C9', '1F4CA', '1F4CB', '1F4CC', '1F4CD', '1F4CE', + '1F4CF', '1F4D0', '1F4D1', '1F4D2', '1F4D3', '1F4D4', + '1F4D5', '1F4D6', '1F4D7', '1F4D8', '1F4D9', '1F4DA', + '1F4DB', '1F4DC', '1F4DD', '1F4DE', '1F4DF', '1F4E0', + '1F4E1', '1F4E2', '1F4E3', '1F4E4', '1F4E5', '1F4E6', + '1F4E7', '1F4E8', '1F4E9', '1F4EA', '1F4EB', '1F4EC', + '1F4ED', '1F4EE', '1F4EF', '1F4F0', '1F4F1', '1F4F2', + '1F4F3', '1F4F4', '1F4F5', '1F4F6', '1F4F7', '1F4F8', + '1F4F9', '1F4FA', '1F4FB', '1F4FC', '1F4FD', '1F4FF', + '1F5A5', '1F5A8', '1F5B1', '1F5B2', '1F5BC', '1F5C2', + '1F5C3', '1F5C4', '1F5D1', '1F5D2', '1F5D3', '1F5DC', + '1F5DD', '1F5DE', '1F5E1', '1F5E3', '1F5EF', '1F5F3', + '1F5FA', '1F5FB', '1F5FC', '1F5FD', '1F5FE', '1F5FF', + '1F6A0', '1F6A1', '1F6A2', '1F6A3', '1F6A4', '1F6A5', + '1F6A6', '1F6A7', '1F6A8', '1F6A9', '1F6AA', '1F6AB', + '1F6AC', '1F6AD', '1F6AE', '1F6AF', '1F6B0', '1F6B1', + '1F6B2', '1F6B3', '1F6B4', '1F6B5', '1F6B6', '1F6B7', + '1F6B8', '1F6B9', '1F6BA', '1F6BB', '1F6BC', '1F6BD', + '1F6BE', '1F6BF', '1F6C0', '1F6C1', '1F6C2', '1F6C3', + '1F6C4', '1F6C5', '1F6CB', '1F6CC', '1F6CD', '1F6CE', + '1F6CF', '1F6D0', '1F6E0', '1F6E1', '1F6E2', '1F6E3', + '1F6E4', '1F6E5', '1F6E9', '1F6EB', '1F6EC', '1F6F0', + '1F6F3', '1F9C0', '1F17E', '1F17F', '1F18E', '1F19A', + '1F21A', '1F22F', '1F23A', '1F30A', '1F30B', '1F30C', + '1F30D', '1F30E', '1F30F', '1F31A', '1F31B', '1F31C', + '1F31D', '1F31E', '1F31F', '1F32A', '1F32B', '1F32C', + '1F32D', '1F32E', '1F32F', '1F33A', '1F33B', '1F33C', + '1F33D', '1F33E', '1F33F', '1F34A', '1F34B', '1F34C', + '1F34D', '1F34E', '1F34F', '1F35A', '1F35B', '1F35C', + '1F35D', '1F35E', '1F35F', '1F36A', '1F36B', '1F36C', + '1F36D', '1F36E', '1F36F', '1F37A', '1F37B', '1F37C', + '1F37D', '1F37E', '1F37F', '1F38A', '1F38B', '1F38C', + '1F38D', '1F38E', '1F38F', '1F39A', '1F39B', '1F39E', + '1F39F', '1F40A', '1F40B', '1F40C', '1F40D', '1F40E', + '1F40F', '1F41A', '1F41B', '1F41C', '1F41D', '1F41E', + '1F41F', '1F42A', '1F42B', '1F42C', '1F42D', '1F42E', + '1F42F', '1F43A', '1F43B', '1F43C', '1F43D', '1F43E', + '1F43F', '1F44A', '1F44B', '1F44C', '1F44D', '1F44E', + '1F44F', '1F45A', '1F45B', '1F45C', '1F45D', '1F45E', + '1F45F', '1F46A', '1F46B', '1F46C', '1F46D', '1F46E', + '1F46F', '1F47A', '1F47B', '1F47C', '1F47D', '1F47E', + '1F47F', '1F48A', '1F48B', '1F48C', '1F48D', '1F48E', + '1F48F', '1F49A', '1F49B', '1F49C', '1F49D', '1F49E', + '1F49F', '1F50A', '1F50B', '1F50C', '1F50D', '1F50E', + '1F50F', '1F51A', '1F51B', '1F51C', '1F51D', '1F51E', + '1F51F', '1F52A', '1F52B', '1F52C', '1F52D', '1F52E', + '1F52F', '1F53A', '1F53B', '1F53C', '1F53D', '1F54A', + '1F54B', '1F54C', '1F54D', '1F54E', '1F55A', '1F55B', + '1F55C', '1F55D', '1F55E', '1F55F', '1F56F', '1F58A', + '1F58B', '1F58C', '1F58D', '1F60A', '1F60B', '1F60C', + '1F60D', '1F60E', '1F60F', '1F61A', '1F61B', '1F61C', + '1F61D', '1F61E', '1F61F', '1F62A', '1F62B', '1F62C', + '1F62D', '1F62E', '1F62F', '1F63A', '1F63B', '1F63C', + '1F63D', '1F63E', '1F63F', '1F64A', '1F64B', '1F64C', + '1F64D', '1F64E', '1F64F', '1F68A', '1F68B', '1F68C', + '1F68D', '1F68E', '1F68F', '1F69A', '1F69B', '1F69C', + '1F69D', '1F69E', '1F69F', '1F170', '1F171', '1F191', + '1F192', '1F193', '1F194', '1F195', '1F196', '1F197', + '1F198', '1F199', '1F201', '1F202', '1F232', '1F233', + '1F234', '1F235', '1F236', '1F237', '1F238', '1F239', + '1F250', '1F251', '1F300', '1F301', '1F302', '1F303', + '1F304', '1F305', '1F306', '1F307', '1F308', '1F309', + '1F310', '1F311', '1F312', '1F313', '1F314', '1F315', + '1F316', '1F317', '1F318', '1F319', '1F320', '1F321', + '1F324', '1F325', '1F326', '1F327', '1F328', '1F329', + '1F330', '1F331', '1F332', '1F333', '1F334', '1F335', + '1F336', '1F337', '1F338', '1F339', '1F340', '1F341', + '1F342', '1F343', '1F344', '1F345', '1F346', '1F347', + '1F348', '1F349', '1F350', '1F351', '1F352', '1F353', + '1F354', '1F355', '1F356', '1F357', '1F358', '1F359', + '1F360', '1F361', '1F362', '1F363', '1F364', '1F365', + '1F366', '1F367', '1F368', '1F369', '1F370', '1F371', + '1F372', '1F373', '1F374', '1F375', '1F376', '1F377', + '1F378', '1F379', '1F380', '1F381', '1F382', '1F383', + '1F384', '1F385', '1F386', '1F387', '1F388', '1F389', + '1F390', '1F391', '1F392', '1F393', '1F396', '1F397', + '1F399', '1F400', '1F401', '1F402', '1F403', '1F404', + '1F405', '1F406', '1F407', '1F408', '1F409', '1F410', + '1F411', '1F412', '1F413', '1F414', '1F415', '1F416', + '1F417', '1F418', '1F419', '1F420', '1F421', '1F422', + '1F423', '1F424', '1F425', '1F426', '1F427', '1F428', + '1F429', '1F430', '1F431', '1F432', '1F433', '1F434', + '1F435', '1F436', '1F437', '1F438', '1F439', '1F440', + '1F441', '1F442', '1F443', '1F444', '1F445', '1F446', + '1F447', '1F448', '1F449', '1F450', '1F451', '1F452', + '1F453', '1F454', '1F455', '1F456', '1F457', '1F458', + '1F459', '1F460', '1F461', '1F462', '1F463', '1F464', + '1F465', '1F466', '1F467', '1F468', '1F469', '1F470', + '1F471', '1F472', '1F473', '1F474', '1F475', '1F476', + '1F477', '1F478', '1F479', '1F480', '1F481', '1F482', + '1F483', '1F484', '1F485', '1F486', '1F487', '1F488', + '1F489', '1F490', '1F491', '1F492', '1F493', '1F494', + '1F495', '1F496', '1F497', '1F498', '1F499', '1F500', + '1F501', '1F502', '1F503', '1F504', '1F505', '1F506', + '1F507', '1F508', '1F509', '1F510', '1F511', '1F512', + '1F513', '1F514', '1F515', '1F516', '1F517', '1F518', + '1F519', '1F520', '1F521', '1F522', '1F523', '1F524', + '1F525', '1F526', '1F527', '1F528', '1F529', '1F530', + '1F531', '1F532', '1F533', '1F534', '1F535', '1F536', + '1F537', '1F538', '1F539', '1F549', '1F550', '1F551', + '1F552', '1F553', '1F554', '1F555', '1F556', '1F557', + '1F558', '1F559', '1F560', '1F561', '1F562', '1F563', + '1F564', '1F565', '1F566', '1F567', '1F570', '1F573', + '1F574', '1F575', '1F576', '1F577', '1F578', '1F579', + '1F587', '1F590', '1F595', '1F596', '1F600', '1F601', + '1F602', '1F603', '1F604', '1F605', '1F606', '1F607', + '1F608', '1F609', '1F610', '1F611', '1F612', '1F613', + '1F614', '1F615', '1F616', '1F617', '1F618', '1F619', + '1F620', '1F621', '1F622', '1F623', '1F624', '1F625', + '1F626', '1F627', '1F628', '1F629', '1F630', '1F631', + '1F632', '1F633', '1F634', '1F635', '1F636', '1F637', + '1F638', '1F639', '1F640', '1F641', '1F642', '1F643', + '1F644', '1F645', '1F646', '1F647', '1F648', '1F649', + '1F680', '1F681', '1F682', '1F683', '1F684', '1F685', + '1F686', '1F687', '1F688', '1F689', '1F690', '1F691', + '1F692', '1F693', '1F694', '1F695', '1F696', '1F697', + '1F698', '1F699', '1F910', '1F911', '1F912', '1F913', + '1F914', '1F915', '1F916', '1F917', '1F918', '1F980', + '1F981', '1F982', '1F983', '1F984', + ] + } + +def unicode_char(n): + """Extend `unichr` for all possible Unicode values (n).""" + try: + return unichr(n) + except ValueError: + # Generate bytes object packed as int. + bytes_object = struct.pack('i', n) + # Return decoded w/ utf-32 codec. + return bytes_object.decode('utf-32') + +#################### +# Create parameters +argument_qty = 1 +tweet_prefix = 'Sample tweet with random emoji ' +emoji_qty = 20 +emoji_strings = [] +for plane in emoji.keys(): + for _ in range(argument_qty): + # Choose random codepoints and convert to characters. + emoji_strings.append( + 'from plane {} '.format(plane) + + ' '.join([unicode_char(int(random.choice(emoji[plane]), 16)) + for _ in range(emoji_qty) + ])) + +params = {'argnames': 'tweet', + 'argvalues': [ + tweet_prefix + u'{}'.format(emoji_str).encode('utf-8') + for emoji_str in emoji_strings + ] + } + +params['ids'] = ['\n ' + argvalue for argvalue in params['argvalues']] + +#################### + +@pytest.mark.parametrize(**params) +def test_emoji(tweet): + # Post tweet. api.PostUpdate() + status = api.PostUpdate(tweet) + # Validate returned tweet: content and length. + assert status.text == tweet.decode('utf-8') + # Destroy tweet. api.DestroyStatus() +# status2 = api.DestroyStatus(status.id) + # Validate that tweet has been destroyed. +# with pytest.raises(twitter.TwitterError): +# assert api.DestroyStatus(status.id) diff --git a/test_api/test_surrogate_pairs.py b/test_api/test_surrogate_pairs.py new file mode 100644 index 00000000..7cf5621d --- /dev/null +++ b/test_api/test_surrogate_pairs.py @@ -0,0 +1,104 @@ +# test_surrogate_pairs + +import pytest +import random +import struct +import unicodedata + +import config +api = config.get_api() +import twitter + +high_surrogates = ('D800', 'DBFF') # including High Private Use Surrogates +high_surrogates_cjk = ('D840', 'D87E') # This covers mostly CJK. +low_surrogates = ('DC00', 'DFFF') + +def make_surrogate_pair(): + """Generate one random surrogate pair.""" + # Add one character made up of one codepoint each from + # (High Surrogates + High Private Use Surrogates) and Low Surrogates. + # We expect each such pair to behave as a single high-codepoint + # character. + return (unicode_char(random.randint( + int(high_surrogates_cjk[0], 16), + int(high_surrogates_cjk[1], 16))) + + unicode_char(random.randint( + int(low_surrogates[0], 16), int(low_surrogates[1], 16))) + ) + +def unicode_char(n): + """Extend `unichr` for all possible Unicode values (n).""" + try: + return unichr(n) + except ValueError: + # Generate bytes object packed as int. + bytes_object = struct.pack('i', n) + # Return decoded w/ utf-32 codec. + return bytes_object.decode('utf-32') + +#################### +# Create parameters +argument_qty = 1 +tweet_prefix = 'Sample tweet with random surrogate-pair characters ' +surrogate_pair_qty = 20 +surrogate_pair_strings = [] +for _ in range(argument_qty): + # Choose random codepoints and convert to characters. + surrogate_pair_strings.append( + 'from high and low surrogate-pair blocks, combined ' + + ' '.join([make_surrogate_pair() + for _ in range(surrogate_pair_qty)]) + ) + +params = { + 'argnames': 'tweet', + 'argvalues': [ + tweet_prefix + u'{}'.format(surrogate_pair_str).encode('utf-8') + for surrogate_pair_str in surrogate_pair_strings + ] + } +params['ids'] = ['\n ' + argvalue for argvalue in params['argvalues']] + +unmatched_chars = set() +# Choose high or low surrogate block at random, then choose one random char. +while len(unmatched_chars) < argument_qty: + collection = random.choice([high_surrogates, low_surrogates]) + char = random.randint( int(collection[0], 16), int(collection[1], 16)) + unmatched_chars.add(unicode_char(char)) +half_pair_params = { + 'argnames': 'tweet, unmatched_char', + 'argvalues': [ + ('''Sample tweet with unmatched surrogate pair component: {} ''' + '''({} was posted).'''. + format(char.encode('utf-8'), repr(char)), char) + for char in unmatched_chars] + } +half_pair_params['ids'] = ['\n ' + argvalue[0] + for argvalue in half_pair_params['argvalues']] + +#################### + +@pytest.mark.parametrize(**params) +def test_surrogate_pairs(tweet): + # Post tweet. api.PostUpdate() + status = api.PostUpdate(tweet) + # Validate returned tweet: content and length. + assert status.text == tweet.decode('utf-8') + # Destroy tweet. api.DestroyStatus() + status2 = api.DestroyStatus(status.id) + # Validate that tweet has been destroyed. + with pytest.raises(twitter.TwitterError): + assert api.DestroyStatus(status.id) + +@pytest.mark.parametrize(**half_pair_params) +def test_half_surrogate_pairs(tweet, unmatched_char): + # Post tweet. api.PostUpdate() + status = api.PostUpdate(tweet) + # Validate returned tweet: content and length. + assert unmatched_char not in status.text + assert unichr(int('FFFD', 16)) in status.text + # Destroy tweet. api.DestroyStatus() +# status2 = api.DestroyStatus(status.id) + # Validate that tweet has been destroyed. +# with pytest.raises(twitter.TwitterError): +# assert api.DestroyStatus(status.id)