From 8817bb2e053406c493509aff02fc4f91db3143b9 Mon Sep 17 00:00:00 2001 From: stephendenham Date: Thu, 11 Nov 2010 15:02:53 +0000 Subject: [PATCH] Initial checkin. git-svn-id: svn://svn.code.sf.net/p/xbmc-groove/code@1 2dec19e3-eb1d-4749-8193-008c8bba0994 --- default.py | 375 +++ default.tbn | Bin 0 -> 3996 bytes resources/img/album.png | Bin 0 -> 13215 bytes resources/img/artist.png | Bin 0 -> 8040 bytes resources/img/favorites.png | Bin 0 -> 8673 bytes resources/img/playlist.png | Bin 0 -> 11647 bytes resources/img/popular.png | Bin 0 -> 11240 bytes resources/img/song.png | Bin 0 -> 11401 bytes resources/lib/GrooveAPI.py | 575 ++++ resources/lib/simplejson/__init__.py | 318 +++ resources/lib/simplejson/_speedups.c | 2329 +++++++++++++++++ resources/lib/simplejson/decoder.py | 354 +++ resources/lib/simplejson/encoder.py | 440 ++++ resources/lib/simplejson/scanner.py | 65 + resources/lib/simplejson/tests/__init__.py | 23 + .../simplejson/tests/test_check_circular.py | 30 + resources/lib/simplejson/tests/test_decode.py | 22 + .../lib/simplejson/tests/test_default.py | 9 + resources/lib/simplejson/tests/test_dump.py | 21 + .../tests/test_encode_basestring_ascii.py | 38 + resources/lib/simplejson/tests/test_fail.py | 76 + resources/lib/simplejson/tests/test_float.py | 15 + resources/lib/simplejson/tests/test_indent.py | 41 + resources/lib/simplejson/tests/test_pass1.py | 76 + resources/lib/simplejson/tests/test_pass2.py | 14 + resources/lib/simplejson/tests/test_pass3.py | 20 + .../lib/simplejson/tests/test_recursion.py | 67 + .../lib/simplejson/tests/test_scanstring.py | 111 + .../lib/simplejson/tests/test_separators.py | 42 + .../lib/simplejson/tests/test_unicode.py | 64 + resources/lib/simplejson/tool.py | 37 + resources/settings.xml | 7 + 32 files changed, 5169 insertions(+) create mode 100644 default.py create mode 100644 default.tbn create mode 100644 resources/img/album.png create mode 100644 resources/img/artist.png create mode 100644 resources/img/favorites.png create mode 100644 resources/img/playlist.png create mode 100644 resources/img/popular.png create mode 100644 resources/img/song.png create mode 100644 resources/lib/GrooveAPI.py create mode 100644 resources/lib/simplejson/__init__.py create mode 100644 resources/lib/simplejson/_speedups.c create mode 100644 resources/lib/simplejson/decoder.py create mode 100644 resources/lib/simplejson/encoder.py create mode 100644 resources/lib/simplejson/scanner.py create mode 100644 resources/lib/simplejson/tests/__init__.py create mode 100644 resources/lib/simplejson/tests/test_check_circular.py create mode 100644 resources/lib/simplejson/tests/test_decode.py create mode 100644 resources/lib/simplejson/tests/test_default.py create mode 100644 resources/lib/simplejson/tests/test_dump.py create mode 100644 resources/lib/simplejson/tests/test_encode_basestring_ascii.py create mode 100644 resources/lib/simplejson/tests/test_fail.py create mode 100644 resources/lib/simplejson/tests/test_float.py create mode 100644 resources/lib/simplejson/tests/test_indent.py create mode 100644 resources/lib/simplejson/tests/test_pass1.py create mode 100644 resources/lib/simplejson/tests/test_pass2.py create mode 100644 resources/lib/simplejson/tests/test_pass3.py create mode 100644 resources/lib/simplejson/tests/test_recursion.py create mode 100644 resources/lib/simplejson/tests/test_scanstring.py create mode 100644 resources/lib/simplejson/tests/test_separators.py create mode 100644 resources/lib/simplejson/tests/test_unicode.py create mode 100644 resources/lib/simplejson/tool.py create mode 100644 resources/settings.xml diff --git a/default.py b/default.py new file mode 100644 index 0000000..49c0e85 --- /dev/null +++ b/default.py @@ -0,0 +1,375 @@ +import urllib, urllib2, re, xbmcplugin, xbmcgui, xbmc, sys, os + +# plugin constants +__plugin__ = "Grooveshark" +__author__ = "Stephen Denham" +__url__ = "" +__svn_url__ = "" +__version__ = "0.0.1" +__svn_revision__ = "" +__XBMC_Revision__ = "" + +MODE_SEARCH_SONGS = 1 +MODE_SEARCH_ALBUMS = 2 +MODE_SEARCH_ARTISTS = 3 +MODE_POPULAR = 4 +MODE_FAVORITES = 5 +MODE_PLAYLISTS = 6 +MODE_ALBUM = 7 +MODE_ARTIST = 8 +MODE_PLAYLIST = 9 +MODE_SONG = 10 +MODE_FAVORITE = 11 +MODE_UNFAVORITE = 12 + +songsearchlimit = 25 +albumsearchlimit = 15 +artistsearchlimit = 15 + +lastID = 0 + +rootDir = os.getcwd() + +resDir = xbmc.translatePath(os.path.join(rootDir, 'resources')) +libDir = xbmc.translatePath(os.path.join(resDir, 'lib')) +imgDir = xbmc.translatePath(os.path.join(resDir, 'img')) + +sys.path.append (libDir) +from GrooveAPI import * +groovesharkApi = GrooveAPI() + +class _Info: + def __init__( self, *args, **kwargs ): + self.__dict__.update( kwargs ) + +class Groveshark: + + albumImg = xbmc.translatePath(os.path.join(imgDir, 'album.png')) + artistImg = xbmc.translatePath(os.path.join(imgDir, 'artist.png')) + favoritesImg = xbmc.translatePath(os.path.join(imgDir, 'favorites.png')) + playlistImg = xbmc.translatePath(os.path.join(imgDir, 'playlist.png')) + popularImg = xbmc.translatePath(os.path.join(imgDir, 'popular.png')) + songImg = xbmc.translatePath(os.path.join(imgDir, 'song.png')) + + songsearchlimit = xbmcplugin.getSetting('songsearchlimit') + albumsearchlimit = xbmcplugin.getSetting('albumsearchlimit') + artistsearchlimit = xbmcplugin.getSetting('artistsearchlimit') + + def __init__( self ): + self._handle = int(sys.argv[1]) + + def categories(self): + userid = self._get_login() + self._addDir('Search songs', '', MODE_SEARCH_SONGS, self.songImg, 0) + self._addDir('Search albums', '', MODE_SEARCH_ALBUMS, self.albumImg, 0) + self._addDir('Search artists', '', MODE_SEARCH_ARTISTS, self.artistImg, 0) + self._addDir('Popular', '', MODE_POPULAR, self.popularImg, 0) + if (userid != 0): + self._addDir('Favorites', '', MODE_FAVORITES, self.favoritesImg, 0) + self._addDir('Playlists', '', MODE_PLAYLISTS, self.playlistImg, 0) + + def searchSongs(self): + query = self._get_keyboard(default="", heading="Search songs") + if (query): + songs = groovesharkApi.searchSongs(query, limit = songsearchlimit) + if (len(songs) > 0): + self._get_songs(songs) + else: + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'No matching songs.') + self.categories() + + def searchAlbums(self): + query = self._get_keyboard(default="", heading="Search albums") + if (query): + albums = groovesharkApi.searchAlbums(query, limit = albumsearchlimit) + if (len(albums) > 0): + self._get_albums(albums) + else: + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'No matching albums.') + self.categories() + + def searchArtists(self): + query = self._get_keyboard(default="", heading="Search artists") + if (query): + artists = groovesharkApi.searchArtists(query, limit = artistsearchlimit) + if (len(artists) > 0): + self._get_artists(artists) + else: + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'No matching artists.') + self.categories() + + def favorites(self): + userid = self._get_login() + if (userid != 0): + favorites = groovesharkApi.userGetFavoriteSongs(userid) + if (len(favorites) > 0): + self._get_songs(favorites) + else: + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'You have no favorites.') + self.categories() + + def popular(self): + popular = groovesharkApi.popularGetSongs(limit = songsearchlimit) + if (len(popular) > 0): + self._get_songs(popular) + else: + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'No popular songs.') + self.categories() + + def playlists(self): + userid = self._get_login() + if (userid != 0): + playlists = groovesharkApi.userGetPlaylists() + if (len(playlists) > 0): + self._get_playlists(playlists) + else: + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'You have no playlists.') + self.categories() + + def favorite(self, songid): + userid = self._get_login() + if (userid != 0): + xbmc.log("Favorite song: " + str(songid)) + groovesharkApi.favoriteSong(songID = songid) + else: + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'You must be logged in', 'to add favorites.') + + def unfavorite(self, songid): + userid = self._get_login() + if (userid != 0): + xbmc.log("Unfavorite song: " + str(songid)) + groovesharkApi.unfavoriteSong(songID = songid) + else: + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'You must be logged in', 'to remove favorites.') + + def album(self,albumid): + album = groovesharkApi.albumGetSongs(albumId = albumid, limit = songsearchlimit) + self._get_songs(album) + + def artist(self, artistid): + albums = groovesharkApi.artistGetAlbums(artistId = artistid, limit = albumsearchlimit) + self._get_albums(albums) + + def playlist(self, playlistid): + userid = self._get_login() + if (userid != 0): + songs = groovesharkApi.playlistGetSongs(playlistId = playlistid, limit = songsearchlimit) + self._get_songs(songs) + else: + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'You must be logged in', 'to get playlists.') + + def song(self, url, name, album, artist, duration): + xbmc.log("Playing: " + url) + songItem = xbmcgui.ListItem(name + " - " + artist, path=url) + songItem.setInfo( type="Music", infoLabels={ "Title": name, "Duration": duration, "Album": album, "Artist": artist} ) + xbmc.Player().stop() + xbmc.Player(xbmc.PLAYER_CORE_PAPLAYER).play(url, songItem, False) + + def _get_keyboard(self, default="", heading="", hidden=False): + kb = xbmc.Keyboard(default, heading, hidden) + kb.doModal() + if (kb.isConfirmed()): + return unicode(kb.getText(), "utf-8") + return '' + + def _get_login(self): + username = xbmcplugin.getSetting('username') + password = xbmcplugin.getSetting('password') + if (username == "" or password == ""): + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'Unable to login.', 'Check username and password in settings.') + return 0 + else: + if groovesharkApi.loggedInStatus() == 1: + groovesharkApi.logout() + try: + userid = groovesharkApi.loginExt(username, password) + except (LoginUnknownError): + userid = 0 + if (userid != 0): + xbmc.log("Logged in") + return userid + else: + dialog = xbmcgui.Dialog() + dialog.ok('Grooveshark', 'Unable to login.', 'Check username and password in settings.') + return 0 + + def _get_songs(self, songs): + xbmc.log("Found " + str(len(songs)) + " songs...") + i = 0 + while i < len(songs): + song = songs[i] + songName = song[0] + songID = song[1] + songDuration = song[2] + songAlbum = song[3] + songArtist = song[6] + songImage = song[9] + xbmc.log(songName) + self._addSong(songID, songName, groovesharkApi.getStreamURL(songID), songDuration, songAlbum, songArtist, songImage) + i = i + 1 + xbmcplugin.setContent(self._handle, 'songs') + xbmcplugin.addSortMethod(self._handle, xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE) + + def _get_albums(self, albums): + xbmc.log("Found " + str(len(albums)) + " albums...") + i = 0 + while i < len(albums): + album = albums[i] + albumArtistName = album[0] + albumName = album[2] + albumID = album[3] + albumImage = album[4] + xbmc.log(albumName) + self._addDir(albumName + " - " + albumArtistName, '', MODE_ALBUM, albumImage, albumID) + i = i + 1 + xbmcplugin.setContent(self._handle, 'albums') + xbmcplugin.addSortMethod(self._handle, xbmcplugin.SORT_METHOD_ALBUM_IGNORE_THE) + + def _get_artists(self, artists): + xbmc.log("Found " + str(len(artists)) + " artists...") + i = 0 + while i < len(artists): + artist = artists[i] + artistName = artist[0] + artistID = artist[1] + xbmc.log(artistName) + self._addDir(artistName, '', MODE_ARTIST, self.artistImg, artistID) + i = i + 1 + xbmcplugin.setContent(self._handle, 'artists') + xbmcplugin.addSortMethod(self._handle, xbmcplugin.SORT_METHOD_ARTIST_IGNORE_THE) + + def _get_playlists(self, playlists): + xbmc.log("Found " + str(len(playlists)) + " playlists...") + i = 0 + while i < len(playlists): + playlist = playlists[i] + playlistName = playlist[0] + playlistID = playlist[1] + xbmc.log(playlistName) + self._addDir(playlistName, '', MODE_PLAYLIST, self.playlistImg, playlistID, ) + i = i + 1 + xbmcplugin.setContent(self._handle, 'files') + xbmcplugin.addSortMethod(self._handle, xbmcplugin.SORT_METHOD_PLAYLIST_ORDER) + + def _addSong(self, songid, songname, songurl, songduration, songalbum, songartist, songimage): + u=sys.argv[0]+"?url="+urllib.quote_plus(songurl)+"&mode="+str(MODE_SONG)+"&name="+urllib.quote_plus(songname)+"&id="+str(songid) \ + +"&album="+urllib.quote_plus(songalbum) \ + +"&artist="+urllib.quote_plus(songartist) \ + +"&duration="+str(songduration) + songItem = xbmcgui.ListItem(songname + " - " + songartist, iconImage=songimage, thumbnailImage=songimage, path=songurl) + songItem.setInfo( type="Music", infoLabels={ "Title": songname, "Duration": songduration, "Album": songalbum, "Artist": songartist} ) + fav=sys.argv[0]+"?url="+urllib.quote_plus(songurl)+"&mode="+str(MODE_FAVORITE)+"&name="+urllib.quote_plus(songname)+"&id="+str(songid) + unfav=sys.argv[0]+"?url="+urllib.quote_plus(songurl)+"&mode="+str(MODE_UNFAVORITE)+"&name="+urllib.quote_plus(songname)+"&id="+str(songid) + menuItems = [] + menuItems.append(("Grooveshark Favorite", "XBMC.RunPlugin("+fav+")")) + menuItems.append(("Not Grooveshark Favorite", "XBMC.RunPlugin("+unfav+")")) + songItem.addContextMenuItems(menuItems, replaceItems=False) + return xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=u,listitem=songItem,isFolder=False) + + def _addDir(self, name, url, mode, iconimage, id): + u=sys.argv[0]+"?url="+urllib.quote_plus(url)+"&mode="+str(mode)+"&name="+urllib.quote_plus(name)+"&id="+str(id) + dir=xbmcgui.ListItem(name, iconImage=iconimage, thumbnailImage=iconimage) + dir.setInfo( type="Music", infoLabels={ "Title": name } ) + # Codes from http://xbmc-scripting.googlecode.com/svn/trunk/Script%20Templates/common/gui/codes.py + menuItems = [] + menuItems.append(("Select", "XBMC.executebuiltin(Action(7))")) + dir.addContextMenuItems(menuItems, replaceItems=True) + return xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=u,listitem=dir,isFolder=True) + +def get_params(): + param=[] + paramstring=sys.argv[2] + if len(paramstring)>=2: + params=sys.argv[2] + cleanedparams=params.replace('?','') + if (params[len(params)-1]=='/'): + params=params[0:len(params)-2] + pairsofparams=cleanedparams.split('&') + param={} + for i in range(len(pairsofparams)): + splitparams={} + splitparams=pairsofparams[i].split('=') + if (len(splitparams))==2: + param[splitparams[0]]=splitparams[1] + print param + return param + +grooveshark = Groveshark(); + +params=get_params() +url=None +name=None +mode=None +id=None + +try: url=urllib.unquote_plus(params["url"]) +except: pass +try: name=urllib.unquote_plus(params["name"]) +except: pass +try: mode=int(params["mode"]) +except: pass +try: id=int(params["id"]) +except: pass + +if (id > 0): + lastID = id + +if mode==None: + grooveshark.categories() + +elif mode==MODE_SEARCH_SONGS: + grooveshark.searchSongs() + +elif mode==MODE_SEARCH_ALBUMS: + grooveshark.searchAlbums() + +elif mode==MODE_SEARCH_ARTISTS: + grooveshark.searchArtists() + +elif mode==MODE_POPULAR: + grooveshark.popular() + +elif mode==MODE_PLAYLISTS: + grooveshark.playlists() + +elif mode==MODE_FAVORITES: + grooveshark.favorites() + +elif mode==MODE_SONG: + try: name=urllib.unquote_plus(params["name"]) + except: pass + try: album=urllib.unquote_plus(params["album"]) + except: pass + try: artist=urllib.unquote_plus(params["artist"]) + except: pass + try: duration=int(params["duration"]) + except: pass + grooveshark.song(url, name, album, artist, duration) + +elif mode==MODE_ARTIST: + grooveshark.artist(lastID) + +elif mode==MODE_ALBUM: + grooveshark.album(lastID) + +elif mode==MODE_PLAYLIST: + grooveshark.playlist(lastID) + +elif mode==MODE_FAVORITE: + grooveshark.favorite(lastID) + +elif mode==MODE_UNFAVORITE: + grooveshark.unfavorite(lastID) + +if (mode < MODE_SONG): + xbmcplugin.endOfDirectory(int(sys.argv[1])) diff --git a/default.tbn b/default.tbn new file mode 100644 index 0000000000000000000000000000000000000000..0d4c2478cfc75ba40faa10793d00192ff724c6e8 GIT binary patch literal 3996 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 EA8WO6?*IS* literal 0 HcmV?d00001 diff --git a/resources/img/artist.png b/resources/img/artist.png new file mode 100644 index 0000000000000000000000000000000000000000..43838cb7d31eeb98ee60723540bb81111014ab02 GIT binary patch literal 8040 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 literal 0 HcmV?d00001 diff --git a/resources/img/favorites.png b/resources/img/favorites.png new file mode 100644 index 0000000000000000000000000000000000000000..114cc404b1666a8de7083b9268a9bf1afc3332b9 GIT binary patch literal 8673 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 literal 0 HcmV?d00001 diff --git a/resources/img/popular.png b/resources/img/popular.png new file mode 100644 index 0000000000000000000000000000000000000000..81134a8f4f1d91502779b3b46c403d2507564c34 GIT binary patch literal 11240 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 literal 0 HcmV?d00001 diff --git a/resources/lib/GrooveAPI.py b/resources/lib/GrooveAPI.py new file mode 100644 index 0000000..f4eb865 --- /dev/null +++ b/resources/lib/GrooveAPI.py @@ -0,0 +1,575 @@ +import urllib2, md5, unicodedata, re, os, traceback, sys, pickle, socket +from operator import itemgetter, attrgetter + +class LoginTokensExceededError(Exception): + def __init__(self): + self.value = 'You have created to many tokens. Only 12 are allowed' + def __str__(self): + return repr(self.value) + +class LoginUnknownError(Exception): + def __init__(self): + self.value = 'Unable to get a new session ID. Wait a few minutes and try again' + def __str__(self): + return repr(self.value) + +class SessionIDTryAgainError(Exception): + def __init__(self): + self.value = 'Unable to get a new session ID. Wait a few minutes and try again' + def __str__(self): + return repr(self.value) + +class GrooveAPI: + def __init__(self, enableDebug = False, isXbox = False): + if isXbox == True: + import simplejson_xbox + self.simplejson = simplejson_xbox + print 'GrooveShark: Initialized as XBOX script' + else: + import simplejson + self.simplejson = simplejson + timeout = 40 + socket.setdefaulttimeout(timeout) + self.enableDebug = enableDebug + self.loggedIn = 0 + self.radioEnabled = 0 + self.userId = 0 + self.seedArtists = [] + self.frowns = [] + self.songIDsAlreadySeen = [] + self.recentArtists = [] + self.rootDir = os.getcwd() + self.sessionID = self.getSavedSession() + self.debug('Saved sessionID: ' + self.sessionID) + self.sessionID = self.getSessionFromAPI() + self.debug('API sessionID: ' + self.sessionID) + if self.sessionID == '': + self.sessionID = self.startSession() + self.debug('Start() sessionID: ' + self.sessionID) + if self.sessionID == '': + self.debug('Could not get a sessionID. Try again in a few minutes') + raise SessionIDTryAgainError() + else: + self.saveSession() + + self.debug('sessionID: ' + self.sessionID) + + def __del__(self): + try: + if self.loggedIn == 1: + self.logout() + except: + pass + + def debug(self, msg): + if self.enableDebug == True: + print msg + + def getSavedSession(self): + sessionID = '' + path = os.path.join(self.rootDir, 'data', 'session.txt') + + try: + f = open(path, 'rb') + sessionID = pickle.load(f) + f.close() + except: + sessionID = '' + pass + + return sessionID + + def saveSession(self): + try: + dir = os.path.join(self.rootDir, 'data') + # Create the 'data' directory if it doesn't exist. + if not os.path.exists(dir): + os.mkdir(dir) + path = os.path.join(dir, 'session.txt') + f = open(path, 'wb') + pickle.dump(self.sessionID, f, protocol=pickle.HIGHEST_PROTOCOL) + f.close() + except IOError, e: + print 'There was an error while saving the session pickle (%s)' % e + pass + except: + print "An unknown error occured during save session: " + str(sys.exc_info()[0]) + pass + + def saveSettings(self): + try: + dir = os.path.join(self.rootDir, 'data') + # Create the 'data' directory if it doesn't exist. + if not os.path.exists(dir): + os.mkdir(dir) + path = os.path.join(dir, 'settings1.txt') + f = open(path, 'wb') + pickle.dump(self.settings, f, protocol=pickle.HIGHEST_PROTOCOL) + f.close() + except IOError, e: + print 'There was an error while saving the settings pickle (%s)' % e + pass + except: + print "An unknown error occured during save settings\n" + pass + + def callRemote(self, method, params={}): + data = {'header': {'sessionID': self.sessionID}, 'method': method, 'parameters': params} + #data = {'header': {'sessionID': None}, 'method': method, 'parameters': params} + data = self.simplejson.dumps(data) + #proxy_support = urllib2.ProxyHandler({"http" : "http://wwwproxy.kom.aau.dk:3128"}) + ## build a new opener with proxy details + #opener = urllib2.build_opener(proxy_support, urllib2.HTTPHandler) + ## install it + #urllib2.install_opener(opener) + #print data + req = urllib2.Request("http://api.grooveshark.com/ws/1.0/?json") + req.add_header('Host', 'api.grooveshark.com') + req.add_header('Content-type', 'text/json') + req.add_header('Content-length', str(len(data))) + req.add_data(data) + response = urllib2.urlopen(req) + result = response.read() + response.close() + try: + result = self.simplejson.loads(result) + if 'fault' in result: + self.debug(result) + return result + except: + return [] + + def startSession(self): + response = urllib2.urlopen("http://www.moovida.com/services/grooveshark/session_start") + result = response.read() + result = self.simplejson.loads(result) + response.close() + if 'fault' in result: + return '' + else: + return result['header']['sessionID'] + + def sessionDestroy(self): + return self.callRemote("session.destroy") + + def getSessionFromAPI(self): + result = self.callRemote("session.get") + if 'fault' in result: + return '' + else: + return result['header']['sessionID'] + + def getStreamURL(self, songID): + result = self.callRemote("song.getStreamUrlEx", {"songID": songID}) + if 'result' in result: + return result['result']['url'] + else: + return '' + + def createUserAuthToken(self, username, password): + hashpass = md5.new(password).hexdigest() + hashpass = username + hashpass + hashpass = md5.new(hashpass).hexdigest() + result = self.callRemote("session.createUserAuthToken", {"username": username, "hashpass": hashpass}) + if 'result' in result: + return result['result']['token'], result['result']['userID'] + elif 'fault' in result: + if result['fault']['code'] == 256: + return -1 # Exceeded the number of allowed tokens. Should not happen + else: + return -2 # Unknown error + else: + return -2 # Unknown error + + def destroyUserAuthToken(self, token): + self.callRemote("session.destroyAuthToken", {"token": token}) + + def loginViaAuthToken(self, token): + result = self.callRemote("session.loginViaAuthToken", {"token": token}) + self.destroyUserAuthToken(token) + if 'result' in result: + self.userID = result['result']['userID'] + return result['result']['userID'] + else: + return 0 + + def login(self, username, password): + if self.loggedIn == 1: + return self.userId + result = self.createUserAuthToken(username, password) + if result == -1: + raise LoginTokensExceededError() + elif result == -2: + raise LoginUnknownError() + else: + self.token = result[0] + self.debug('Token:' + self.token) + self.userId = self.loginViaAuthToken(self.token) + if self.userId == 0: + raise LoginUnknownError() + else: + self.loggedIn = 1 + return self.userId + + def loginExt(self, username, password): + if self.loggedIn == 1: + return self.userId + token = md5.new(username.lower() + md5.new(password).hexdigest()).hexdigest() + result = self.callRemote("session.loginExt", {"username": username, "token": token}) + if 'result' in result: + if 'userID' in result['result']: + self.loggedIn = 1 + self.userId = result['result']['userID'] + return result['result']['userID'] + else: + return 0 + + + def loginBasic(self, username, password): + if self.loggedIn == 1: + return self.userId + result = self.callRemote("session.login", {"username": username, "password": password}) + if 'result' in result: + if 'userID' in result['result']: + self.loggedIn = 1 + self.userId = result['result']['userID'] + return result['result']['userID'] + else: + return 0 + + def loggedInStatus(self): + return self.loggedIn + + def logout(self): + self.callRemote("session.logout", {}) + self.loggedIn = 0 + + def getSongInfo(self, songID): + return self.callRemote("song.about", {"songID": songID})['result']['song'] + + def userGetFavoriteSongs(self, userID): + result = self.callRemote("user.getFavoriteSongs", {"userID": userID}) + list = self.parseSongs(result) + return list + + def userGetPlaylists(self, limit=25): + if self.loggedIn == 1: + result = self.callRemote("user.getPlaylists", {"userID": self.userId, "limit": limit}) + if 'result' in result: + playlists = result['result']['playlists'] + else: + return [] + i = 0 + list = [] + while(i < len(playlists)): + p = playlists[i] + list.append([p['playlistName'].encode('ascii', 'ignore'), p['playlistID']]) + i = i + 1 + return sorted(list, key=itemgetter(0)) + else: + return [] + + def playlistCreate(self, name, about): + if self.loggedIn == 1: + result = self.callRemote("playlist.create", {"name": name, "about": about}) + if 'result' in result: + return result['result']['playlistID'] + else: + return 0 + else: + return 0 + + def playlistGetSongs(self, playlistId, limit=25): + result = self.callRemote("playlist.getSongs", {"playlistID": playlistId}) + list = self.parseSongs(result) + return list + + def playlistDelete(self, playlistId): + if self.loggedIn == 1: + return self.callRemote("playlist.delete", {"playlistID": playlistId}) + + def playlistRename(self, playlistId, name): + if self.loggedIn == 1: + result = self.callRemote("playlist.rename", {"playlistID": playlistId, "name": name}) + if 'fault' in result: + return 0 + else: + return 1 + else: + return 0 + + def playlistClearSongs(self, playlistId): + if self.loggedIn == 1: + return self.callRemote("playlist.clearSongs", {"playlistID": playlistId}) + + def playlistAddSong(self, playlistId, songId, position): + if self.loggedIn == 1: + result = self.callRemote("playlist.addSong", {"playlistID": playlistId, "songID": songId, "position": position}) + if 'fault' in result: + return 0 + else: + return 1 + else: + return 0 + + def playlistReplace(self, playlistId, songIds): + if self.loggedIn == 1: + result = self.callRemote("playlist.replace", {"playlistID": playlistId, "songIDs": songIds}) + if 'fault' in result: + return 0 + else: + return 1 + else: + return 0 + + def autoplayStartWithArtistIDs(self, artistIds): + result = self.callRemote("autoplay.startWithArtistIDs", {"artistIDs": artistIds}) + if 'fault' in result: + self.radioEnabled = 0 + return 0 + else: + self.radioEnabled = 1 + return 1 + + def autoplayStart(self, songIds): + result = self.callRemote("autoplay.start", {"songIDs": songIds}) + if 'fault' in result: + self.radioEnabled = 0 + return 0 + else: + self.radioEnabled = 1 + return 1 + + def autoplayStop(self): + result = self.callRemote("autoplay.stop", {}) + if 'fault' in result: + self.radioEnabled = 1 + return 0 + else: + self.radioEnabled = 0 + return 1 + + def autoplayGetNextSongEx(self, seedArtists = [], frowns = [], songIDsAlreadySeen = [], recentArtists = []): + result = self.callRemote("autoplay.getNextSongEx", {"seedArtists": seedArtists, "frowns": frowns, "songIDsAlreadySeen": songIDsAlreadySeen, "recentArtists": recentArtists}) + if 'fault' in result: + return [] + else: + return result + + def radioGetNextSong(self): + if self.seedArtists == []: + return [] + else: + result = self.autoplayGetNextSongEx(self.seedArtists, self.frowns, self.songIDsAlreadySeen, self.recentArtists) +# print result + if 'fault' in result: + return [] + else: + song = self.parseSongs(result) + self.radioAlreadySeen(song[0][1]) + return song + + def radioFrown(self, songId): + self.frown.append(songId) + + def radioAlreadySeen(self, songId): + self.songIDsAlreadySeen.append(songId) + + def radioAddArtist(self, artistId): + self.seedArtists.append(artistId) + + def radioStart(self, artists = [], frowns = []): + for artist in artists: + self.seedArtists.append(artist) + for artist in frowns: + self.frowns.append(artist) + if self.autoplayStartWithArtistIDs(self.seedArtists) == 1: + self.radioEnabled = 1 + return 1 + else: + self.radioEnabled = 0 + return 0 + + def radioStop(self): + self.seedArtists = [] + self.frowns = [] + self.songIDsAlreadySeen = [] + self.recentArtists = [] + self.radioEnabled = 0 + + def radioTurnedOn(self): + return self.radioEnabled + + def favoriteSong(self, songID): + return self.callRemote("song.favorite", {"songID": songID}) + + def unfavoriteSong(self, songID): + return self.callRemote("song.unfavorite", {"songID": songID}) + + def getMethods(self): + return self.callRemote("service.getMethods") + + def searchSongsExactMatch(self, songName, artistName, albumName): + result = self.callRemote("search.songExactMatch", {"songName": songName, "artistName": artistName, "albumName": albumName}) + list = self.parseSongs(result) + return list + + def searchSongs(self, query, limit, page=0, sortKey=6): + result = self.callRemote("search.songs", {"query": query, "limit": limit, "page:": page, "streamableOnly": 1}) + list = self.parseSongs(result) + return list + #return sorted(list, key=itemgetter(sortKey)) + + def searchArtists(self, query, limit, sortKey=0): + result = self.callRemote("search.artists", {"query": query, "limit": limit, "streamableOnly": 1}) + list = self.parseArtists(result) + return list + #return sorted(list, key=itemgetter(sortKey)) + + def searchAlbums(self, query, limit, sortKey=2): + result = self.callRemote("search.albums", {"query": query, "limit": limit, "streamableOnly": 1}) + list = self.parseAlbums(result) + return list + #return sorted(list, key=itemgetter(sortKey)) + + def searchPlaylists(self, query, limit): + result = self.callRemote("search.playlists", {"query": query, "limit": limit, "streamableOnly": 1}) + list = self.parsePlaylists(result) + return list + + def popularGetSongs(self, limit): + result = self.callRemote("popular.getSongs", {"limit": limit}) + list = self.parseSongs(result) + return list + + def popularGetArtists(self, limit): + result = self.callRemote("popular.getArtists", {"limit": limit}) + list = self.parseArtists(result) + return list + + def popularGetAlbums(self, limit): + result = self.callRemote("popular.getAlbums", {"limit": limit}) + list = self.parseAlbums(result) + return list + + def artistGetAlbums(self, artistId, limit, sortKey=2): + result = self.callRemote("artist.getAlbums", {"artistID": artistId, "limit": limit}) + list = self.parseAlbums(result) + return list + #return sorted(list, key=itemgetter(sortKey)) + + def artistGetVerifiedAlbums(self, artistId, limit): + result = self.callRemote("artist.getVerifiedAlbums", {"artistID": artistId, "limit": limit}) + list = self.parseSongs(result) + return list + + def albumGetSongs(self, albumId, limit): + result = self.callRemote("album.getSongs", {"albumID": albumId, "limit": limit}) + list = self.parseSongs(result) + return list + + def songGetSimilar(self, songId, limit): + result = self.callRemote("song.getSimilar", {"songID": songId, "limit": limit}) + list = self.parseSongs(result) + return list + + def parseSongs(self, items): + try: + if 'result' in items: + i = 0 + list = [] + if 'songs' in items['result']: + l = len(items['result']['songs']) + index = 'songs' + elif 'song' in items['result']: + l = 1 + index = 'song' + else: + l = 0 + index = '' + while(i < l): + if index == 'songs': + s = items['result'][index][i] + else: + s = items['result'][index] + if 'estDurationSecs' in s: + dur = s['estDurationSecs'] + else: + dur = 0 + try: + notIn = True + for entry in list: + songName = s['songName'].encode('ascii', 'ignore') + albumName = s['albumName'].encode('ascii', 'ignore') + artistName = s['artistName'].encode('ascii', 'ignore') + if (entry[0].lower() == songName.lower()) and (entry[3].lower() == albumName.lower()) and (entry[6].lower() == artistName.lower()): + notIn = False + if notIn == True: + list.append([s['songName'].encode('ascii', 'ignore'),\ + s['songID'],\ + dur,\ + s['albumName'].encode('ascii', 'ignore'),\ + s['albumID'],\ + s['image']['tiny'].encode('ascii', 'ignore'),\ + s['artistName'].encode('ascii', 'ignore'),\ + s['artistID'],\ + s['image']['small'].encode('ascii', 'ignore'),\ + s['image']['medium'].encode('ascii', 'ignore')]) + except: + print 'GrooveShark: Could not parse song number: ' + str(i) + traceback.print_exc() + i = i + 1 + return list + else: + return [] + pass + except: + print 'GrooveShark: Could not parse songs. Got this:' + traceback.print_exc() + return [] + + def parseArtists(self, items): + if 'result' in items: + i = 0 + list = [] + artists = items['result']['artists'] + while(i < len(artists)): + s = artists[i] + list.append([s['artistName'].encode('ascii', 'ignore'),\ + s['artistID']]) + i = i + 1 + return list + else: + return [] + + def parseAlbums(self, items): + if 'result' in items: + i = 0 + list = [] + albums = items['result']['albums'] + while(i < len(albums)): + s = albums[i] + list.append([s['artistName'].encode('ascii', 'ignore'),\ + s['artistID'],\ + s['albumName'].encode('ascii', 'ignore'),\ + s['albumID'],\ + s['image']['tiny'].encode('ascii', 'ignore')]) + i = i + 1 + return list + else: + return [] + + def parsePlaylists(self, items): + if 'result' in items: + i = 0 + list = [] + playlists = items['result']['playlists'] + while(i < len(playlists)): + s = playlists[i] + list.append([s['playlistID'],\ + s['playlistName'].encode('ascii', 'ignore'),\ + s['username'].encode('ascii', 'ignore')]) + i = i + 1 + return list + else: + return [] diff --git a/resources/lib/simplejson/__init__.py b/resources/lib/simplejson/__init__.py new file mode 100644 index 0000000..d5b4d39 --- /dev/null +++ b/resources/lib/simplejson/__init__.py @@ -0,0 +1,318 @@ +r"""JSON (JavaScript Object Notation) is a subset of +JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data +interchange format. + +:mod:`simplejson` exposes an API familiar to users of the standard library +:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained +version of the :mod:`json` library contained in Python 2.6, but maintains +compatibility with Python 2.4 and Python 2.5 and (currently) has +significant performance advantages, even without using the optional C +extension for speedups. + +Encoding basic Python object hierarchies:: + + >>> import simplejson as json + >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) + '["foo", {"bar": ["baz", null, 1.0, 2]}]' + >>> print json.dumps("\"foo\bar") + "\"foo\bar" + >>> print json.dumps(u'\u1234') + "\u1234" + >>> print json.dumps('\\') + "\\" + >>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) + {"a": 0, "b": 0, "c": 0} + >>> from StringIO import StringIO + >>> io = StringIO() + >>> json.dump(['streaming API'], io) + >>> io.getvalue() + '["streaming API"]' + +Compact encoding:: + + >>> import simplejson as json + >>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) + '[1,2,3,{"4":5,"6":7}]' + +Pretty printing:: + + >>> import simplejson as json + >>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=4) + >>> print '\n'.join([l.rstrip() for l in s.splitlines()]) + { + "4": 5, + "6": 7 + } + +Decoding JSON:: + + >>> import simplejson as json + >>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] + >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj + True + >>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar' + True + >>> from StringIO import StringIO + >>> io = StringIO('["streaming API"]') + >>> json.load(io)[0] == 'streaming API' + True + +Specializing JSON object decoding:: + + >>> import simplejson as json + >>> def as_complex(dct): + ... if '__complex__' in dct: + ... return complex(dct['real'], dct['imag']) + ... return dct + ... + >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', + ... object_hook=as_complex) + (1+2j) + >>> import decimal + >>> json.loads('1.1', parse_float=decimal.Decimal) == decimal.Decimal('1.1') + True + +Specializing JSON object encoding:: + + >>> import simplejson as json + >>> def encode_complex(obj): + ... if isinstance(obj, complex): + ... return [obj.real, obj.imag] + ... raise TypeError(repr(o) + " is not JSON serializable") + ... + >>> json.dumps(2 + 1j, default=encode_complex) + '[2.0, 1.0]' + >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j) + '[2.0, 1.0]' + >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j)) + '[2.0, 1.0]' + + +Using simplejson.tool from the shell to validate and pretty-print:: + + $ echo '{"json":"obj"}' | python -m simplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -m simplejson.tool + Expecting property name: line 1 column 2 (char 2) +""" +__version__ = '2.0.9' +__all__ = [ + 'dump', 'dumps', 'load', 'loads', + 'JSONDecoder', 'JSONEncoder', +] + +__author__ = 'Bob Ippolito ' + +from decoder import JSONDecoder +from encoder import JSONEncoder + +_default_encoder = JSONEncoder( + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=True, + indent=None, + separators=None, + encoding='utf-8', + default=None, +) + +def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, **kw): + """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a + ``.write()``-supporting file-like object). + + If ``skipkeys`` is true then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the some chunks written to ``fp`` + may be ``unicode`` instances, subject to normal Python ``str`` to + ``unicode`` coercion rules. Unless ``fp.write()`` explicitly + understands ``unicode`` (as in ``codecs.getwriter()``) this is likely + to cause an error. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) + in strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and object + members will be pretty-printed with that indent level. An indent level + of 0 will only insert newlines. ``None`` is the most compact representation. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + iterable = _default_encoder.iterencode(obj) + else: + if cls is None: + cls = JSONEncoder + iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, + default=default, **kw).iterencode(obj) + # could accelerate with writelines in some versions of Python, at + # a debuggability cost + for chunk in iterable: + fp.write(chunk) + + +def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, + allow_nan=True, cls=None, indent=None, separators=None, + encoding='utf-8', default=None, **kw): + """Serialize ``obj`` to a JSON formatted ``str``. + + If ``skipkeys`` is false then ``dict`` keys that are not basic types + (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) + will be skipped instead of raising a ``TypeError``. + + If ``ensure_ascii`` is false, then the return value will be a + ``unicode`` instance subject to normal Python ``str`` to ``unicode`` + coercion rules instead of being escaped to an ASCII ``str``. + + If ``check_circular`` is false, then the circular reference check + for container types will be skipped and a circular reference will + result in an ``OverflowError`` (or worse). + + If ``allow_nan`` is false, then it will be a ``ValueError`` to + serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in + strict compliance of the JSON specification, instead of using the + JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). + + If ``indent`` is a non-negative integer, then JSON array elements and + object members will be pretty-printed with that indent level. An indent + level of 0 will only insert newlines. ``None`` is the most compact + representation. + + If ``separators`` is an ``(item_separator, dict_separator)`` tuple + then it will be used instead of the default ``(', ', ': ')`` separators. + ``(',', ':')`` is the most compact JSON representation. + + ``encoding`` is the character encoding for str instances, default is UTF-8. + + ``default(obj)`` is a function that should return a serializable version + of obj or raise TypeError. The default simply raises TypeError. + + To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the + ``.default()`` method to serialize additional types), specify it with + the ``cls`` kwarg. + + """ + # cached encoder + if (not skipkeys and ensure_ascii and + check_circular and allow_nan and + cls is None and indent is None and separators is None and + encoding == 'utf-8' and default is None and not kw): + return _default_encoder.encode(obj) + if cls is None: + cls = JSONEncoder + return cls( + skipkeys=skipkeys, ensure_ascii=ensure_ascii, + check_circular=check_circular, allow_nan=allow_nan, indent=indent, + separators=separators, encoding=encoding, default=default, + **kw).encode(obj) + + +_default_decoder = JSONDecoder(encoding=None, object_hook=None) + + +def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, **kw): + """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing + a JSON document) to a Python object. + + If the contents of ``fp`` is encoded with an ASCII based encoding other + than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must + be specified. Encodings that are not ASCII based (such as UCS-2) are + not allowed, and should be wrapped with + ``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode`` + object and passed to ``loads()`` + + ``object_hook`` is an optional function that will be called with the + result of any object literal decode (a ``dict``). The return value of + ``object_hook`` will be used instead of the ``dict``. This feature + can be used to implement custom decoders (e.g. JSON-RPC class hinting). + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + return loads(fp.read(), + encoding=encoding, cls=cls, object_hook=object_hook, + parse_float=parse_float, parse_int=parse_int, + parse_constant=parse_constant, **kw) + + +def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, **kw): + """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON + document) to a Python object. + + If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding + other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name + must be specified. Encodings that are not ASCII based (such as UCS-2) + are not allowed and should be decoded to ``unicode`` first. + + ``object_hook`` is an optional function that will be called with the + result of any object literal decode (a ``dict``). The return value of + ``object_hook`` will be used instead of the ``dict``. This feature + can be used to implement custom decoders (e.g. JSON-RPC class hinting). + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN, null, true, false. + This can be used to raise an exception if invalid JSON numbers + are encountered. + + To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` + kwarg. + + """ + if (cls is None and encoding is None and object_hook is None and + parse_int is None and parse_float is None and + parse_constant is None and not kw): + return _default_decoder.decode(s) + if cls is None: + cls = JSONDecoder + if object_hook is not None: + kw['object_hook'] = object_hook + if parse_float is not None: + kw['parse_float'] = parse_float + if parse_int is not None: + kw['parse_int'] = parse_int + if parse_constant is not None: + kw['parse_constant'] = parse_constant + return cls(encoding=encoding, **kw).decode(s) diff --git a/resources/lib/simplejson/_speedups.c b/resources/lib/simplejson/_speedups.c new file mode 100644 index 0000000..23b5f4a --- /dev/null +++ b/resources/lib/simplejson/_speedups.c @@ -0,0 +1,2329 @@ +#include "Python.h" +#include "structmember.h" +#if PY_VERSION_HEX < 0x02060000 && !defined(Py_TYPE) +#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) +#endif +#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) +typedef int Py_ssize_t; +#define PY_SSIZE_T_MAX INT_MAX +#define PY_SSIZE_T_MIN INT_MIN +#define PyInt_FromSsize_t PyInt_FromLong +#define PyInt_AsSsize_t PyInt_AsLong +#endif +#ifndef Py_IS_FINITE +#define Py_IS_FINITE(X) (!Py_IS_INFINITY(X) && !Py_IS_NAN(X)) +#endif + +#ifdef __GNUC__ +#define UNUSED __attribute__((__unused__)) +#else +#define UNUSED +#endif + +#define DEFAULT_ENCODING "utf-8" + +#define PyScanner_Check(op) PyObject_TypeCheck(op, &PyScannerType) +#define PyScanner_CheckExact(op) (Py_TYPE(op) == &PyScannerType) +#define PyEncoder_Check(op) PyObject_TypeCheck(op, &PyEncoderType) +#define PyEncoder_CheckExact(op) (Py_TYPE(op) == &PyEncoderType) + +static PyTypeObject PyScannerType; +static PyTypeObject PyEncoderType; + +typedef struct _PyScannerObject { + PyObject_HEAD + PyObject *encoding; + PyObject *strict; + PyObject *object_hook; + PyObject *parse_float; + PyObject *parse_int; + PyObject *parse_constant; +} PyScannerObject; + +static PyMemberDef scanner_members[] = { + {"encoding", T_OBJECT, offsetof(PyScannerObject, encoding), READONLY, "encoding"}, + {"strict", T_OBJECT, offsetof(PyScannerObject, strict), READONLY, "strict"}, + {"object_hook", T_OBJECT, offsetof(PyScannerObject, object_hook), READONLY, "object_hook"}, + {"parse_float", T_OBJECT, offsetof(PyScannerObject, parse_float), READONLY, "parse_float"}, + {"parse_int", T_OBJECT, offsetof(PyScannerObject, parse_int), READONLY, "parse_int"}, + {"parse_constant", T_OBJECT, offsetof(PyScannerObject, parse_constant), READONLY, "parse_constant"}, + {NULL} +}; + +typedef struct _PyEncoderObject { + PyObject_HEAD + PyObject *markers; + PyObject *defaultfn; + PyObject *encoder; + PyObject *indent; + PyObject *key_separator; + PyObject *item_separator; + PyObject *sort_keys; + PyObject *skipkeys; + int fast_encode; + int allow_nan; +} PyEncoderObject; + +static PyMemberDef encoder_members[] = { + {"markers", T_OBJECT, offsetof(PyEncoderObject, markers), READONLY, "markers"}, + {"default", T_OBJECT, offsetof(PyEncoderObject, defaultfn), READONLY, "default"}, + {"encoder", T_OBJECT, offsetof(PyEncoderObject, encoder), READONLY, "encoder"}, + {"indent", T_OBJECT, offsetof(PyEncoderObject, indent), READONLY, "indent"}, + {"key_separator", T_OBJECT, offsetof(PyEncoderObject, key_separator), READONLY, "key_separator"}, + {"item_separator", T_OBJECT, offsetof(PyEncoderObject, item_separator), READONLY, "item_separator"}, + {"sort_keys", T_OBJECT, offsetof(PyEncoderObject, sort_keys), READONLY, "sort_keys"}, + {"skipkeys", T_OBJECT, offsetof(PyEncoderObject, skipkeys), READONLY, "skipkeys"}, + {NULL} +}; + +static Py_ssize_t +ascii_escape_char(Py_UNICODE c, char *output, Py_ssize_t chars); +static PyObject * +ascii_escape_unicode(PyObject *pystr); +static PyObject * +ascii_escape_str(PyObject *pystr); +static PyObject * +py_encode_basestring_ascii(PyObject* self UNUSED, PyObject *pystr); +void init_speedups(void); +static PyObject * +scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr); +static PyObject * +scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr); +static PyObject * +_build_rval_index_tuple(PyObject *rval, Py_ssize_t idx); +static PyObject * +scanner_new(PyTypeObject *type, PyObject *args, PyObject *kwds); +static int +scanner_init(PyObject *self, PyObject *args, PyObject *kwds); +static void +scanner_dealloc(PyObject *self); +static int +scanner_clear(PyObject *self); +static PyObject * +encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds); +static int +encoder_init(PyObject *self, PyObject *args, PyObject *kwds); +static void +encoder_dealloc(PyObject *self); +static int +encoder_clear(PyObject *self); +static int +encoder_listencode_list(PyEncoderObject *s, PyObject *rval, PyObject *seq, Py_ssize_t indent_level); +static int +encoder_listencode_obj(PyEncoderObject *s, PyObject *rval, PyObject *obj, Py_ssize_t indent_level); +static int +encoder_listencode_dict(PyEncoderObject *s, PyObject *rval, PyObject *dct, Py_ssize_t indent_level); +static PyObject * +_encoded_const(PyObject *const); +static void +raise_errmsg(char *msg, PyObject *s, Py_ssize_t end); +static PyObject * +encoder_encode_string(PyEncoderObject *s, PyObject *obj); +static int +_convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr); +static PyObject * +_convertPyInt_FromSsize_t(Py_ssize_t *size_ptr); +static PyObject * +encoder_encode_float(PyEncoderObject *s, PyObject *obj); + +#define S_CHAR(c) (c >= ' ' && c <= '~' && c != '\\' && c != '"') +#define IS_WHITESPACE(c) (((c) == ' ') || ((c) == '\t') || ((c) == '\n') || ((c) == '\r')) + +#define MIN_EXPANSION 6 +#ifdef Py_UNICODE_WIDE +#define MAX_EXPANSION (2 * MIN_EXPANSION) +#else +#define MAX_EXPANSION MIN_EXPANSION +#endif + +static int +_convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr) +{ + /* PyObject to Py_ssize_t converter */ + *size_ptr = PyInt_AsSsize_t(o); + if (*size_ptr == -1 && PyErr_Occurred()); + return 1; + return 0; +} + +static PyObject * +_convertPyInt_FromSsize_t(Py_ssize_t *size_ptr) +{ + /* Py_ssize_t to PyObject converter */ + return PyInt_FromSsize_t(*size_ptr); +} + +static Py_ssize_t +ascii_escape_char(Py_UNICODE c, char *output, Py_ssize_t chars) +{ + /* Escape unicode code point c to ASCII escape sequences + in char *output. output must have at least 12 bytes unused to + accommodate an escaped surrogate pair "\uXXXX\uXXXX" */ + output[chars++] = '\\'; + switch (c) { + case '\\': output[chars++] = (char)c; break; + case '"': output[chars++] = (char)c; break; + case '\b': output[chars++] = 'b'; break; + case '\f': output[chars++] = 'f'; break; + case '\n': output[chars++] = 'n'; break; + case '\r': output[chars++] = 'r'; break; + case '\t': output[chars++] = 't'; break; + default: +#ifdef Py_UNICODE_WIDE + if (c >= 0x10000) { + /* UTF-16 surrogate pair */ + Py_UNICODE v = c - 0x10000; + c = 0xd800 | ((v >> 10) & 0x3ff); + output[chars++] = 'u'; + output[chars++] = "0123456789abcdef"[(c >> 12) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 8) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 4) & 0xf]; + output[chars++] = "0123456789abcdef"[(c ) & 0xf]; + c = 0xdc00 | (v & 0x3ff); + output[chars++] = '\\'; + } +#endif + output[chars++] = 'u'; + output[chars++] = "0123456789abcdef"[(c >> 12) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 8) & 0xf]; + output[chars++] = "0123456789abcdef"[(c >> 4) & 0xf]; + output[chars++] = "0123456789abcdef"[(c ) & 0xf]; + } + return chars; +} + +static PyObject * +ascii_escape_unicode(PyObject *pystr) +{ + /* Take a PyUnicode pystr and return a new ASCII-only escaped PyString */ + Py_ssize_t i; + Py_ssize_t input_chars; + Py_ssize_t output_size; + Py_ssize_t max_output_size; + Py_ssize_t chars; + PyObject *rval; + char *output; + Py_UNICODE *input_unicode; + + input_chars = PyUnicode_GET_SIZE(pystr); + input_unicode = PyUnicode_AS_UNICODE(pystr); + + /* One char input can be up to 6 chars output, estimate 4 of these */ + output_size = 2 + (MIN_EXPANSION * 4) + input_chars; + max_output_size = 2 + (input_chars * MAX_EXPANSION); + rval = PyString_FromStringAndSize(NULL, output_size); + if (rval == NULL) { + return NULL; + } + output = PyString_AS_STRING(rval); + chars = 0; + output[chars++] = '"'; + for (i = 0; i < input_chars; i++) { + Py_UNICODE c = input_unicode[i]; + if (S_CHAR(c)) { + output[chars++] = (char)c; + } + else { + chars = ascii_escape_char(c, output, chars); + } + if (output_size - chars < (1 + MAX_EXPANSION)) { + /* There's more than four, so let's resize by a lot */ + Py_ssize_t new_output_size = output_size * 2; + /* This is an upper bound */ + if (new_output_size > max_output_size) { + new_output_size = max_output_size; + } + /* Make sure that the output size changed before resizing */ + if (new_output_size != output_size) { + output_size = new_output_size; + if (_PyString_Resize(&rval, output_size) == -1) { + return NULL; + } + output = PyString_AS_STRING(rval); + } + } + } + output[chars++] = '"'; + if (_PyString_Resize(&rval, chars) == -1) { + return NULL; + } + return rval; +} + +static PyObject * +ascii_escape_str(PyObject *pystr) +{ + /* Take a PyString pystr and return a new ASCII-only escaped PyString */ + Py_ssize_t i; + Py_ssize_t input_chars; + Py_ssize_t output_size; + Py_ssize_t chars; + PyObject *rval; + char *output; + char *input_str; + + input_chars = PyString_GET_SIZE(pystr); + input_str = PyString_AS_STRING(pystr); + + /* Fast path for a string that's already ASCII */ + for (i = 0; i < input_chars; i++) { + Py_UNICODE c = (Py_UNICODE)(unsigned char)input_str[i]; + if (!S_CHAR(c)) { + /* If we have to escape something, scan the string for unicode */ + Py_ssize_t j; + for (j = i; j < input_chars; j++) { + c = (Py_UNICODE)(unsigned char)input_str[j]; + if (c > 0x7f) { + /* We hit a non-ASCII character, bail to unicode mode */ + PyObject *uni; + uni = PyUnicode_DecodeUTF8(input_str, input_chars, "strict"); + if (uni == NULL) { + return NULL; + } + rval = ascii_escape_unicode(uni); + Py_DECREF(uni); + return rval; + } + } + break; + } + } + + if (i == input_chars) { + /* Input is already ASCII */ + output_size = 2 + input_chars; + } + else { + /* One char input can be up to 6 chars output, estimate 4 of these */ + output_size = 2 + (MIN_EXPANSION * 4) + input_chars; + } + rval = PyString_FromStringAndSize(NULL, output_size); + if (rval == NULL) { + return NULL; + } + output = PyString_AS_STRING(rval); + output[0] = '"'; + + /* We know that everything up to i is ASCII already */ + chars = i + 1; + memcpy(&output[1], input_str, i); + + for (; i < input_chars; i++) { + Py_UNICODE c = (Py_UNICODE)(unsigned char)input_str[i]; + if (S_CHAR(c)) { + output[chars++] = (char)c; + } + else { + chars = ascii_escape_char(c, output, chars); + } + /* An ASCII char can't possibly expand to a surrogate! */ + if (output_size - chars < (1 + MIN_EXPANSION)) { + /* There's more than four, so let's resize by a lot */ + output_size *= 2; + if (output_size > 2 + (input_chars * MIN_EXPANSION)) { + output_size = 2 + (input_chars * MIN_EXPANSION); + } + if (_PyString_Resize(&rval, output_size) == -1) { + return NULL; + } + output = PyString_AS_STRING(rval); + } + } + output[chars++] = '"'; + if (_PyString_Resize(&rval, chars) == -1) { + return NULL; + } + return rval; +} + +static void +raise_errmsg(char *msg, PyObject *s, Py_ssize_t end) +{ + /* Use the Python function simplejson.decoder.errmsg to raise a nice + looking ValueError exception */ + static PyObject *errmsg_fn = NULL; + PyObject *pymsg; + if (errmsg_fn == NULL) { + PyObject *decoder = PyImport_ImportModule("simplejson.decoder"); + if (decoder == NULL) + return; + errmsg_fn = PyObject_GetAttrString(decoder, "errmsg"); + Py_DECREF(decoder); + if (errmsg_fn == NULL) + return; + } + pymsg = PyObject_CallFunction(errmsg_fn, "(zOO&)", msg, s, _convertPyInt_FromSsize_t, &end); + if (pymsg) { + PyErr_SetObject(PyExc_ValueError, pymsg); + Py_DECREF(pymsg); + } +} + +static PyObject * +join_list_unicode(PyObject *lst) +{ + /* return u''.join(lst) */ + static PyObject *joinfn = NULL; + if (joinfn == NULL) { + PyObject *ustr = PyUnicode_FromUnicode(NULL, 0); + if (ustr == NULL) + return NULL; + + joinfn = PyObject_GetAttrString(ustr, "join"); + Py_DECREF(ustr); + if (joinfn == NULL) + return NULL; + } + return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); +} + +static PyObject * +join_list_string(PyObject *lst) +{ + /* return ''.join(lst) */ + static PyObject *joinfn = NULL; + if (joinfn == NULL) { + PyObject *ustr = PyString_FromStringAndSize(NULL, 0); + if (ustr == NULL) + return NULL; + + joinfn = PyObject_GetAttrString(ustr, "join"); + Py_DECREF(ustr); + if (joinfn == NULL) + return NULL; + } + return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); +} + +static PyObject * +_build_rval_index_tuple(PyObject *rval, Py_ssize_t idx) { + /* return (rval, idx) tuple, stealing reference to rval */ + PyObject *tpl; + PyObject *pyidx; + /* + steal a reference to rval, returns (rval, idx) + */ + if (rval == NULL) { + return NULL; + } + pyidx = PyInt_FromSsize_t(idx); + if (pyidx == NULL) { + Py_DECREF(rval); + return NULL; + } + tpl = PyTuple_New(2); + if (tpl == NULL) { + Py_DECREF(pyidx); + Py_DECREF(rval); + return NULL; + } + PyTuple_SET_ITEM(tpl, 0, rval); + PyTuple_SET_ITEM(tpl, 1, pyidx); + return tpl; +} + +static PyObject * +scanstring_str(PyObject *pystr, Py_ssize_t end, char *encoding, int strict, Py_ssize_t *next_end_ptr) +{ + /* Read the JSON string from PyString pystr. + end is the index of the first character after the quote. + encoding is the encoding of pystr (must be an ASCII superset) + if strict is zero then literal control characters are allowed + *next_end_ptr is a return-by-reference index of the character + after the end quote + + Return value is a new PyString (if ASCII-only) or PyUnicode + */ + PyObject *rval; + Py_ssize_t len = PyString_GET_SIZE(pystr); + Py_ssize_t begin = end - 1; + Py_ssize_t next = begin; + int has_unicode = 0; + char *buf = PyString_AS_STRING(pystr); + PyObject *chunks = PyList_New(0); + if (chunks == NULL) { + goto bail; + } + if (end < 0 || len <= end) { + PyErr_SetString(PyExc_ValueError, "end is out of bounds"); + goto bail; + } + while (1) { + /* Find the end of the string or the next escape */ + Py_UNICODE c = 0; + PyObject *chunk = NULL; + for (next = end; next < len; next++) { + c = (unsigned char)buf[next]; + if (c == '"' || c == '\\') { + break; + } + else if (strict && c <= 0x1f) { + raise_errmsg("Invalid control character at", pystr, next); + goto bail; + } + else if (c > 0x7f) { + has_unicode = 1; + } + } + if (!(c == '"' || c == '\\')) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + /* Pick up this chunk if it's not zero length */ + if (next != end) { + PyObject *strchunk = PyString_FromStringAndSize(&buf[end], next - end); + if (strchunk == NULL) { + goto bail; + } + if (has_unicode) { + chunk = PyUnicode_FromEncodedObject(strchunk, encoding, NULL); + Py_DECREF(strchunk); + if (chunk == NULL) { + goto bail; + } + } + else { + chunk = strchunk; + } + if (PyList_Append(chunks, chunk)) { + Py_DECREF(chunk); + goto bail; + } + Py_DECREF(chunk); + } + next++; + if (c == '"') { + end = next; + break; + } + if (next == len) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + c = buf[next]; + if (c != 'u') { + /* Non-unicode backslash escapes */ + end = next + 1; + switch (c) { + case '"': break; + case '\\': break; + case '/': break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = 0; + } + if (c == 0) { + raise_errmsg("Invalid \\escape", pystr, end - 2); + goto bail; + } + } + else { + c = 0; + next++; + end = next + 4; + if (end >= len) { + raise_errmsg("Invalid \\uXXXX escape", pystr, next - 1); + goto bail; + } + /* Decode 4 hex digits */ + for (; next < end; next++) { + Py_UNICODE digit = buf[next]; + c <<= 4; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } +#ifdef Py_UNICODE_WIDE + /* Surrogate pair */ + if ((c & 0xfc00) == 0xd800) { + Py_UNICODE c2 = 0; + if (end + 6 >= len) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + if (buf[next++] != '\\' || buf[next++] != 'u') { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + end += 6; + /* Decode 4 hex digits */ + for (; next < end; next++) { + c2 <<= 4; + Py_UNICODE digit = buf[next]; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c2 |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c2 |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c2 |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } + if ((c2 & 0xfc00) != 0xdc00) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00)); + } + else if ((c & 0xfc00) == 0xdc00) { + raise_errmsg("Unpaired low surrogate", pystr, end - 5); + goto bail; + } +#endif + } + if (c > 0x7f) { + has_unicode = 1; + } + if (has_unicode) { + chunk = PyUnicode_FromUnicode(&c, 1); + if (chunk == NULL) { + goto bail; + } + } + else { + char c_char = Py_CHARMASK(c); + chunk = PyString_FromStringAndSize(&c_char, 1); + if (chunk == NULL) { + goto bail; + } + } + if (PyList_Append(chunks, chunk)) { + Py_DECREF(chunk); + goto bail; + } + Py_DECREF(chunk); + } + + rval = join_list_string(chunks); + if (rval == NULL) { + goto bail; + } + Py_CLEAR(chunks); + *next_end_ptr = end; + return rval; +bail: + *next_end_ptr = -1; + Py_XDECREF(chunks); + return NULL; +} + + +static PyObject * +scanstring_unicode(PyObject *pystr, Py_ssize_t end, int strict, Py_ssize_t *next_end_ptr) +{ + /* Read the JSON string from PyUnicode pystr. + end is the index of the first character after the quote. + if strict is zero then literal control characters are allowed + *next_end_ptr is a return-by-reference index of the character + after the end quote + + Return value is a new PyUnicode + */ + PyObject *rval; + Py_ssize_t len = PyUnicode_GET_SIZE(pystr); + Py_ssize_t begin = end - 1; + Py_ssize_t next = begin; + const Py_UNICODE *buf = PyUnicode_AS_UNICODE(pystr); + PyObject *chunks = PyList_New(0); + if (chunks == NULL) { + goto bail; + } + if (end < 0 || len <= end) { + PyErr_SetString(PyExc_ValueError, "end is out of bounds"); + goto bail; + } + while (1) { + /* Find the end of the string or the next escape */ + Py_UNICODE c = 0; + PyObject *chunk = NULL; + for (next = end; next < len; next++) { + c = buf[next]; + if (c == '"' || c == '\\') { + break; + } + else if (strict && c <= 0x1f) { + raise_errmsg("Invalid control character at", pystr, next); + goto bail; + } + } + if (!(c == '"' || c == '\\')) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + /* Pick up this chunk if it's not zero length */ + if (next != end) { + chunk = PyUnicode_FromUnicode(&buf[end], next - end); + if (chunk == NULL) { + goto bail; + } + if (PyList_Append(chunks, chunk)) { + Py_DECREF(chunk); + goto bail; + } + Py_DECREF(chunk); + } + next++; + if (c == '"') { + end = next; + break; + } + if (next == len) { + raise_errmsg("Unterminated string starting at", pystr, begin); + goto bail; + } + c = buf[next]; + if (c != 'u') { + /* Non-unicode backslash escapes */ + end = next + 1; + switch (c) { + case '"': break; + case '\\': break; + case '/': break; + case 'b': c = '\b'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = 0; + } + if (c == 0) { + raise_errmsg("Invalid \\escape", pystr, end - 2); + goto bail; + } + } + else { + c = 0; + next++; + end = next + 4; + if (end >= len) { + raise_errmsg("Invalid \\uXXXX escape", pystr, next - 1); + goto bail; + } + /* Decode 4 hex digits */ + for (; next < end; next++) { + Py_UNICODE digit = buf[next]; + c <<= 4; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } +#ifdef Py_UNICODE_WIDE + /* Surrogate pair */ + if ((c & 0xfc00) == 0xd800) { + Py_UNICODE c2 = 0; + if (end + 6 >= len) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + if (buf[next++] != '\\' || buf[next++] != 'u') { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + end += 6; + /* Decode 4 hex digits */ + for (; next < end; next++) { + c2 <<= 4; + Py_UNICODE digit = buf[next]; + switch (digit) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + c2 |= (digit - '0'); break; + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': + c2 |= (digit - 'a' + 10); break; + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': + c2 |= (digit - 'A' + 10); break; + default: + raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); + goto bail; + } + } + if ((c2 & 0xfc00) != 0xdc00) { + raise_errmsg("Unpaired high surrogate", pystr, end - 5); + goto bail; + } + c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00)); + } + else if ((c & 0xfc00) == 0xdc00) { + raise_errmsg("Unpaired low surrogate", pystr, end - 5); + goto bail; + } +#endif + } + chunk = PyUnicode_FromUnicode(&c, 1); + if (chunk == NULL) { + goto bail; + } + if (PyList_Append(chunks, chunk)) { + Py_DECREF(chunk); + goto bail; + } + Py_DECREF(chunk); + } + + rval = join_list_unicode(chunks); + if (rval == NULL) { + goto bail; + } + Py_DECREF(chunks); + *next_end_ptr = end; + return rval; +bail: + *next_end_ptr = -1; + Py_XDECREF(chunks); + return NULL; +} + +PyDoc_STRVAR(pydoc_scanstring, + "scanstring(basestring, end, encoding, strict=True) -> (str, end)\n" + "\n" + "Scan the string s for a JSON string. End is the index of the\n" + "character in s after the quote that started the JSON string.\n" + "Unescapes all valid JSON string escape sequences and raises ValueError\n" + "on attempt to decode an invalid string. If strict is False then literal\n" + "control characters are allowed in the string.\n" + "\n" + "Returns a tuple of the decoded string and the index of the character in s\n" + "after the end quote." +); + +static PyObject * +py_scanstring(PyObject* self UNUSED, PyObject *args) +{ + PyObject *pystr; + PyObject *rval; + Py_ssize_t end; + Py_ssize_t next_end = -1; + char *encoding = NULL; + int strict = 1; + if (!PyArg_ParseTuple(args, "OO&|zi:scanstring", &pystr, _convertPyInt_AsSsize_t, &end, &encoding, &strict)) { + return NULL; + } + if (encoding == NULL) { + encoding = DEFAULT_ENCODING; + } + if (PyString_Check(pystr)) { + rval = scanstring_str(pystr, end, encoding, strict, &next_end); + } + else if (PyUnicode_Check(pystr)) { + rval = scanstring_unicode(pystr, end, strict, &next_end); + } + else { + PyErr_Format(PyExc_TypeError, + "first argument must be a string, not %.80s", + Py_TYPE(pystr)->tp_name); + return NULL; + } + return _build_rval_index_tuple(rval, next_end); +} + +PyDoc_STRVAR(pydoc_encode_basestring_ascii, + "encode_basestring_ascii(basestring) -> str\n" + "\n" + "Return an ASCII-only JSON representation of a Python string" +); + +static PyObject * +py_encode_basestring_ascii(PyObject* self UNUSED, PyObject *pystr) +{ + /* Return an ASCII-only JSON representation of a Python string */ + /* METH_O */ + if (PyString_Check(pystr)) { + return ascii_escape_str(pystr); + } + else if (PyUnicode_Check(pystr)) { + return ascii_escape_unicode(pystr); + } + else { + PyErr_Format(PyExc_TypeError, + "first argument must be a string, not %.80s", + Py_TYPE(pystr)->tp_name); + return NULL; + } +} + +static void +scanner_dealloc(PyObject *self) +{ + /* Deallocate scanner object */ + scanner_clear(self); + Py_TYPE(self)->tp_free(self); +} + +static int +scanner_traverse(PyObject *self, visitproc visit, void *arg) +{ + PyScannerObject *s; + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + Py_VISIT(s->encoding); + Py_VISIT(s->strict); + Py_VISIT(s->object_hook); + Py_VISIT(s->parse_float); + Py_VISIT(s->parse_int); + Py_VISIT(s->parse_constant); + return 0; +} + +static int +scanner_clear(PyObject *self) +{ + PyScannerObject *s; + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + Py_CLEAR(s->encoding); + Py_CLEAR(s->strict); + Py_CLEAR(s->object_hook); + Py_CLEAR(s->parse_float); + Py_CLEAR(s->parse_int); + Py_CLEAR(s->parse_constant); + return 0; +} + +static PyObject * +_parse_object_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON object from PyString pystr. + idx is the index of the first character after the opening curly brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing curly brace. + + Returns a new PyObject (usually a dict, but object_hook can change that) + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; + PyObject *rval = PyDict_New(); + PyObject *key = NULL; + PyObject *val = NULL; + char *encoding = PyString_AS_STRING(s->encoding); + int strict = PyObject_IsTrue(s->strict); + Py_ssize_t next_idx; + if (rval == NULL) + return NULL; + + /* skip whitespace after { */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the object is non-empty */ + if (idx <= end_idx && str[idx] != '}') { + while (idx <= end_idx) { + /* read key */ + if (str[idx] != '"') { + raise_errmsg("Expecting property name", pystr, idx); + goto bail; + } + key = scanstring_str(pystr, idx + 1, encoding, strict, &next_idx); + if (key == NULL) + goto bail; + idx = next_idx; + + /* skip whitespace between key and : delimiter, read :, skip whitespace */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + if (idx > end_idx || str[idx] != ':') { + raise_errmsg("Expecting : delimiter", pystr, idx); + goto bail; + } + idx++; + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* read any JSON data type */ + val = scan_once_str(s, pystr, idx, &next_idx); + if (val == NULL) + goto bail; + + if (PyDict_SetItem(rval, key, val) == -1) + goto bail; + + Py_CLEAR(key); + Py_CLEAR(val); + idx = next_idx; + + /* skip whitespace before } or , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the object is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == '}') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting , delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , delimiter */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + /* verify that idx < end_idx, str[idx] should be '}' */ + if (idx > end_idx || str[idx] != '}') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + /* if object_hook is not None: rval = object_hook(rval) */ + if (s->object_hook != Py_None) { + val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); + if (val == NULL) + goto bail; + Py_DECREF(rval); + rval = val; + val = NULL; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(key); + Py_XDECREF(val); + Py_DECREF(rval); + return NULL; +} + +static PyObject * +_parse_object_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON object from PyUnicode pystr. + idx is the index of the first character after the opening curly brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing curly brace. + + Returns a new PyObject (usually a dict, but object_hook can change that) + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; + PyObject *val = NULL; + PyObject *rval = PyDict_New(); + PyObject *key = NULL; + int strict = PyObject_IsTrue(s->strict); + Py_ssize_t next_idx; + if (rval == NULL) + return NULL; + + /* skip whitespace after { */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the object is non-empty */ + if (idx <= end_idx && str[idx] != '}') { + while (idx <= end_idx) { + /* read key */ + if (str[idx] != '"') { + raise_errmsg("Expecting property name", pystr, idx); + goto bail; + } + key = scanstring_unicode(pystr, idx + 1, strict, &next_idx); + if (key == NULL) + goto bail; + idx = next_idx; + + /* skip whitespace between key and : delimiter, read :, skip whitespace */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + if (idx > end_idx || str[idx] != ':') { + raise_errmsg("Expecting : delimiter", pystr, idx); + goto bail; + } + idx++; + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* read any JSON term */ + val = scan_once_unicode(s, pystr, idx, &next_idx); + if (val == NULL) + goto bail; + + if (PyDict_SetItem(rval, key, val) == -1) + goto bail; + + Py_CLEAR(key); + Py_CLEAR(val); + idx = next_idx; + + /* skip whitespace before } or , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the object is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == '}') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting , delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , delimiter */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + + /* verify that idx < end_idx, str[idx] should be '}' */ + if (idx > end_idx || str[idx] != '}') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + + /* if object_hook is not None: rval = object_hook(rval) */ + if (s->object_hook != Py_None) { + val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); + if (val == NULL) + goto bail; + Py_DECREF(rval); + rval = val; + val = NULL; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(key); + Py_XDECREF(val); + Py_DECREF(rval); + return NULL; +} + +static PyObject * +_parse_array_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON array from PyString pystr. + idx is the index of the first character after the opening brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing brace. + + Returns a new PyList + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; + PyObject *val = NULL; + PyObject *rval = PyList_New(0); + Py_ssize_t next_idx; + if (rval == NULL) + return NULL; + + /* skip whitespace after [ */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the array is non-empty */ + if (idx <= end_idx && str[idx] != ']') { + while (idx <= end_idx) { + + /* read any JSON term and de-tuplefy the (rval, idx) */ + val = scan_once_str(s, pystr, idx, &next_idx); + if (val == NULL) + goto bail; + + if (PyList_Append(rval, val) == -1) + goto bail; + + Py_CLEAR(val); + idx = next_idx; + + /* skip whitespace between term and , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the array is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == ']') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting , delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + + /* verify that idx < end_idx, str[idx] should be ']' */ + if (idx > end_idx || str[idx] != ']') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(val); + Py_DECREF(rval); + return NULL; +} + +static PyObject * +_parse_array_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON array from PyString pystr. + idx is the index of the first character after the opening brace. + *next_idx_ptr is a return-by-reference index to the first character after + the closing brace. + + Returns a new PyList + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; + PyObject *val = NULL; + PyObject *rval = PyList_New(0); + Py_ssize_t next_idx; + if (rval == NULL) + return NULL; + + /* skip whitespace after [ */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* only loop if the array is non-empty */ + if (idx <= end_idx && str[idx] != ']') { + while (idx <= end_idx) { + + /* read any JSON term */ + val = scan_once_unicode(s, pystr, idx, &next_idx); + if (val == NULL) + goto bail; + + if (PyList_Append(rval, val) == -1) + goto bail; + + Py_CLEAR(val); + idx = next_idx; + + /* skip whitespace between term and , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + + /* bail if the array is closed or we didn't get the , delimiter */ + if (idx > end_idx) break; + if (str[idx] == ']') { + break; + } + else if (str[idx] != ',') { + raise_errmsg("Expecting , delimiter", pystr, idx); + goto bail; + } + idx++; + + /* skip whitespace after , */ + while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; + } + } + + /* verify that idx < end_idx, str[idx] should be ']' */ + if (idx > end_idx || str[idx] != ']') { + raise_errmsg("Expecting object", pystr, end_idx); + goto bail; + } + *next_idx_ptr = idx + 1; + return rval; +bail: + Py_XDECREF(val); + Py_DECREF(rval); + return NULL; +} + +static PyObject * +_parse_constant(PyScannerObject *s, char *constant, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { + /* Read a JSON constant from PyString pystr. + constant is the constant string that was found + ("NaN", "Infinity", "-Infinity"). + idx is the index of the first character of the constant + *next_idx_ptr is a return-by-reference index to the first character after + the constant. + + Returns the result of parse_constant + */ + PyObject *cstr; + PyObject *rval; + /* constant is "NaN", "Infinity", or "-Infinity" */ + cstr = PyString_InternFromString(constant); + if (cstr == NULL) + return NULL; + + /* rval = parse_constant(constant) */ + rval = PyObject_CallFunctionObjArgs(s->parse_constant, cstr, NULL); + idx += PyString_GET_SIZE(cstr); + Py_DECREF(cstr); + *next_idx_ptr = idx; + return rval; +} + +static PyObject * +_match_number_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssize_t *next_idx_ptr) { + /* Read a JSON number from PyString pystr. + idx is the index of the first character of the number + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of that number: + PyInt, PyLong, or PyFloat. + May return other types if parse_int or parse_float are set + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; + Py_ssize_t idx = start; + int is_float = 0; + PyObject *rval; + PyObject *numstr; + + /* read a sign if it's there, make sure it's not the end of the string */ + if (str[idx] == '-') { + idx++; + if (idx > end_idx) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + } + + /* read as many integer digits as we find as long as it doesn't start with 0 */ + if (str[idx] >= '1' && str[idx] <= '9') { + idx++; + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + /* if it starts with 0 we only expect one integer digit */ + else if (str[idx] == '0') { + idx++; + } + /* no integer digits, error */ + else { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + /* if the next char is '.' followed by a digit then read all float digits */ + if (idx < end_idx && str[idx] == '.' && str[idx + 1] >= '0' && str[idx + 1] <= '9') { + is_float = 1; + idx += 2; + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + + /* if the next char is 'e' or 'E' then maybe read the exponent (or backtrack) */ + if (idx < end_idx && (str[idx] == 'e' || str[idx] == 'E')) { + + /* save the index of the 'e' or 'E' just in case we need to backtrack */ + Py_ssize_t e_start = idx; + idx++; + + /* read an exponent sign if present */ + if (idx < end_idx && (str[idx] == '-' || str[idx] == '+')) idx++; + + /* read all digits */ + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + + /* if we got a digit, then parse as float. if not, backtrack */ + if (str[idx - 1] >= '0' && str[idx - 1] <= '9') { + is_float = 1; + } + else { + idx = e_start; + } + } + + /* copy the section we determined to be a number */ + numstr = PyString_FromStringAndSize(&str[start], idx - start); + if (numstr == NULL) + return NULL; + if (is_float) { + /* parse as a float using a fast path if available, otherwise call user defined method */ + if (s->parse_float != (PyObject *)&PyFloat_Type) { + rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); + } + else { + rval = PyFloat_FromDouble(PyOS_ascii_atof(PyString_AS_STRING(numstr))); + } + } + else { + /* parse as an int using a fast path if available, otherwise call user defined method */ + if (s->parse_int != (PyObject *)&PyInt_Type) { + rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); + } + else { + rval = PyInt_FromString(PyString_AS_STRING(numstr), NULL, 10); + } + } + Py_DECREF(numstr); + *next_idx_ptr = idx; + return rval; +} + +static PyObject * +_match_number_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssize_t *next_idx_ptr) { + /* Read a JSON number from PyUnicode pystr. + idx is the index of the first character of the number + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of that number: + PyInt, PyLong, or PyFloat. + May return other types if parse_int or parse_float are set + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; + Py_ssize_t idx = start; + int is_float = 0; + PyObject *rval; + PyObject *numstr; + + /* read a sign if it's there, make sure it's not the end of the string */ + if (str[idx] == '-') { + idx++; + if (idx > end_idx) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + } + + /* read as many integer digits as we find as long as it doesn't start with 0 */ + if (str[idx] >= '1' && str[idx] <= '9') { + idx++; + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + /* if it starts with 0 we only expect one integer digit */ + else if (str[idx] == '0') { + idx++; + } + /* no integer digits, error */ + else { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + + /* if the next char is '.' followed by a digit then read all float digits */ + if (idx < end_idx && str[idx] == '.' && str[idx + 1] >= '0' && str[idx + 1] <= '9') { + is_float = 1; + idx += 2; + while (idx < end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + } + + /* if the next char is 'e' or 'E' then maybe read the exponent (or backtrack) */ + if (idx < end_idx && (str[idx] == 'e' || str[idx] == 'E')) { + Py_ssize_t e_start = idx; + idx++; + + /* read an exponent sign if present */ + if (idx < end_idx && (str[idx] == '-' || str[idx] == '+')) idx++; + + /* read all digits */ + while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; + + /* if we got a digit, then parse as float. if not, backtrack */ + if (str[idx - 1] >= '0' && str[idx - 1] <= '9') { + is_float = 1; + } + else { + idx = e_start; + } + } + + /* copy the section we determined to be a number */ + numstr = PyUnicode_FromUnicode(&str[start], idx - start); + if (numstr == NULL) + return NULL; + if (is_float) { + /* parse as a float using a fast path if available, otherwise call user defined method */ + if (s->parse_float != (PyObject *)&PyFloat_Type) { + rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); + } + else { + rval = PyFloat_FromString(numstr, NULL); + } + } + else { + /* no fast path for unicode -> int, just call */ + rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); + } + Py_DECREF(numstr); + *next_idx_ptr = idx; + return rval; +} + +static PyObject * +scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) +{ + /* Read one JSON term (of any kind) from PyString pystr. + idx is the index of the first character of the term + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of the term. + */ + char *str = PyString_AS_STRING(pystr); + Py_ssize_t length = PyString_GET_SIZE(pystr); + if (idx >= length) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + switch (str[idx]) { + case '"': + /* string */ + return scanstring_str(pystr, idx + 1, + PyString_AS_STRING(s->encoding), + PyObject_IsTrue(s->strict), + next_idx_ptr); + case '{': + /* object */ + return _parse_object_str(s, pystr, idx + 1, next_idx_ptr); + case '[': + /* array */ + return _parse_array_str(s, pystr, idx + 1, next_idx_ptr); + case 'n': + /* null */ + if ((idx + 3 < length) && str[idx + 1] == 'u' && str[idx + 2] == 'l' && str[idx + 3] == 'l') { + Py_INCREF(Py_None); + *next_idx_ptr = idx + 4; + return Py_None; + } + break; + case 't': + /* true */ + if ((idx + 3 < length) && str[idx + 1] == 'r' && str[idx + 2] == 'u' && str[idx + 3] == 'e') { + Py_INCREF(Py_True); + *next_idx_ptr = idx + 4; + return Py_True; + } + break; + case 'f': + /* false */ + if ((idx + 4 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'l' && str[idx + 3] == 's' && str[idx + 4] == 'e') { + Py_INCREF(Py_False); + *next_idx_ptr = idx + 5; + return Py_False; + } + break; + case 'N': + /* NaN */ + if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { + return _parse_constant(s, "NaN", idx, next_idx_ptr); + } + break; + case 'I': + /* Infinity */ + if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { + return _parse_constant(s, "Infinity", idx, next_idx_ptr); + } + break; + case '-': + /* -Infinity */ + if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { + return _parse_constant(s, "-Infinity", idx, next_idx_ptr); + } + break; + } + /* Didn't find a string, object, array, or named constant. Look for a number. */ + return _match_number_str(s, pystr, idx, next_idx_ptr); +} + +static PyObject * +scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) +{ + /* Read one JSON term (of any kind) from PyUnicode pystr. + idx is the index of the first character of the term + *next_idx_ptr is a return-by-reference index to the first character after + the number. + + Returns a new PyObject representation of the term. + */ + Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); + Py_ssize_t length = PyUnicode_GET_SIZE(pystr); + if (idx >= length) { + PyErr_SetNone(PyExc_StopIteration); + return NULL; + } + switch (str[idx]) { + case '"': + /* string */ + return scanstring_unicode(pystr, idx + 1, + PyObject_IsTrue(s->strict), + next_idx_ptr); + case '{': + /* object */ + return _parse_object_unicode(s, pystr, idx + 1, next_idx_ptr); + case '[': + /* array */ + return _parse_array_unicode(s, pystr, idx + 1, next_idx_ptr); + case 'n': + /* null */ + if ((idx + 3 < length) && str[idx + 1] == 'u' && str[idx + 2] == 'l' && str[idx + 3] == 'l') { + Py_INCREF(Py_None); + *next_idx_ptr = idx + 4; + return Py_None; + } + break; + case 't': + /* true */ + if ((idx + 3 < length) && str[idx + 1] == 'r' && str[idx + 2] == 'u' && str[idx + 3] == 'e') { + Py_INCREF(Py_True); + *next_idx_ptr = idx + 4; + return Py_True; + } + break; + case 'f': + /* false */ + if ((idx + 4 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'l' && str[idx + 3] == 's' && str[idx + 4] == 'e') { + Py_INCREF(Py_False); + *next_idx_ptr = idx + 5; + return Py_False; + } + break; + case 'N': + /* NaN */ + if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { + return _parse_constant(s, "NaN", idx, next_idx_ptr); + } + break; + case 'I': + /* Infinity */ + if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { + return _parse_constant(s, "Infinity", idx, next_idx_ptr); + } + break; + case '-': + /* -Infinity */ + if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { + return _parse_constant(s, "-Infinity", idx, next_idx_ptr); + } + break; + } + /* Didn't find a string, object, array, or named constant. Look for a number. */ + return _match_number_unicode(s, pystr, idx, next_idx_ptr); +} + +static PyObject * +scanner_call(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* Python callable interface to scan_once_{str,unicode} */ + PyObject *pystr; + PyObject *rval; + Py_ssize_t idx; + Py_ssize_t next_idx = -1; + static char *kwlist[] = {"string", "idx", NULL}; + PyScannerObject *s; + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&:scan_once", kwlist, &pystr, _convertPyInt_AsSsize_t, &idx)) + return NULL; + + if (PyString_Check(pystr)) { + rval = scan_once_str(s, pystr, idx, &next_idx); + } + else if (PyUnicode_Check(pystr)) { + rval = scan_once_unicode(s, pystr, idx, &next_idx); + } + else { + PyErr_Format(PyExc_TypeError, + "first argument must be a string, not %.80s", + Py_TYPE(pystr)->tp_name); + return NULL; + } + return _build_rval_index_tuple(rval, next_idx); +} + +static PyObject * +scanner_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyScannerObject *s; + s = (PyScannerObject *)type->tp_alloc(type, 0); + if (s != NULL) { + s->encoding = NULL; + s->strict = NULL; + s->object_hook = NULL; + s->parse_float = NULL; + s->parse_int = NULL; + s->parse_constant = NULL; + } + return (PyObject *)s; +} + +static int +scanner_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* Initialize Scanner object */ + PyObject *ctx; + static char *kwlist[] = {"context", NULL}; + PyScannerObject *s; + + assert(PyScanner_Check(self)); + s = (PyScannerObject *)self; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:make_scanner", kwlist, &ctx)) + return -1; + + /* PyString_AS_STRING is used on encoding */ + s->encoding = PyObject_GetAttrString(ctx, "encoding"); + if (s->encoding == Py_None) { + Py_DECREF(Py_None); + s->encoding = PyString_InternFromString(DEFAULT_ENCODING); + } + else if (PyUnicode_Check(s->encoding)) { + PyObject *tmp = PyUnicode_AsEncodedString(s->encoding, NULL, NULL); + Py_DECREF(s->encoding); + s->encoding = tmp; + } + if (s->encoding == NULL || !PyString_Check(s->encoding)) + goto bail; + + /* All of these will fail "gracefully" so we don't need to verify them */ + s->strict = PyObject_GetAttrString(ctx, "strict"); + if (s->strict == NULL) + goto bail; + s->object_hook = PyObject_GetAttrString(ctx, "object_hook"); + if (s->object_hook == NULL) + goto bail; + s->parse_float = PyObject_GetAttrString(ctx, "parse_float"); + if (s->parse_float == NULL) + goto bail; + s->parse_int = PyObject_GetAttrString(ctx, "parse_int"); + if (s->parse_int == NULL) + goto bail; + s->parse_constant = PyObject_GetAttrString(ctx, "parse_constant"); + if (s->parse_constant == NULL) + goto bail; + + return 0; + +bail: + Py_CLEAR(s->encoding); + Py_CLEAR(s->strict); + Py_CLEAR(s->object_hook); + Py_CLEAR(s->parse_float); + Py_CLEAR(s->parse_int); + Py_CLEAR(s->parse_constant); + return -1; +} + +PyDoc_STRVAR(scanner_doc, "JSON scanner object"); + +static +PyTypeObject PyScannerType = { + PyObject_HEAD_INIT(NULL) + 0, /* tp_internal */ + "simplejson._speedups.Scanner", /* tp_name */ + sizeof(PyScannerObject), /* tp_basicsize */ + 0, /* tp_itemsize */ + scanner_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + scanner_call, /* tp_call */ + 0, /* tp_str */ + 0,/* PyObject_GenericGetAttr, */ /* tp_getattro */ + 0,/* PyObject_GenericSetAttr, */ /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ + scanner_doc, /* tp_doc */ + scanner_traverse, /* tp_traverse */ + scanner_clear, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + scanner_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + scanner_init, /* tp_init */ + 0,/* PyType_GenericAlloc, */ /* tp_alloc */ + scanner_new, /* tp_new */ + 0,/* PyObject_GC_Del, */ /* tp_free */ +}; + +static PyObject * +encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyEncoderObject *s; + s = (PyEncoderObject *)type->tp_alloc(type, 0); + if (s != NULL) { + s->markers = NULL; + s->defaultfn = NULL; + s->encoder = NULL; + s->indent = NULL; + s->key_separator = NULL; + s->item_separator = NULL; + s->sort_keys = NULL; + s->skipkeys = NULL; + } + return (PyObject *)s; +} + +static int +encoder_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* initialize Encoder object */ + static char *kwlist[] = {"markers", "default", "encoder", "indent", "key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", NULL}; + + PyEncoderObject *s; + PyObject *allow_nan; + + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOOOOOO:make_encoder", kwlist, + &s->markers, &s->defaultfn, &s->encoder, &s->indent, &s->key_separator, &s->item_separator, &s->sort_keys, &s->skipkeys, &allow_nan)) + return -1; + + Py_INCREF(s->markers); + Py_INCREF(s->defaultfn); + Py_INCREF(s->encoder); + Py_INCREF(s->indent); + Py_INCREF(s->key_separator); + Py_INCREF(s->item_separator); + Py_INCREF(s->sort_keys); + Py_INCREF(s->skipkeys); + s->fast_encode = (PyCFunction_Check(s->encoder) && PyCFunction_GetFunction(s->encoder) == (PyCFunction)py_encode_basestring_ascii); + s->allow_nan = PyObject_IsTrue(allow_nan); + return 0; +} + +static PyObject * +encoder_call(PyObject *self, PyObject *args, PyObject *kwds) +{ + /* Python callable interface to encode_listencode_obj */ + static char *kwlist[] = {"obj", "_current_indent_level", NULL}; + PyObject *obj; + PyObject *rval; + Py_ssize_t indent_level; + PyEncoderObject *s; + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&:_iterencode", kwlist, + &obj, _convertPyInt_AsSsize_t, &indent_level)) + return NULL; + rval = PyList_New(0); + if (rval == NULL) + return NULL; + if (encoder_listencode_obj(s, rval, obj, indent_level)) { + Py_DECREF(rval); + return NULL; + } + return rval; +} + +static PyObject * +_encoded_const(PyObject *obj) +{ + /* Return the JSON string representation of None, True, False */ + if (obj == Py_None) { + static PyObject *s_null = NULL; + if (s_null == NULL) { + s_null = PyString_InternFromString("null"); + } + Py_INCREF(s_null); + return s_null; + } + else if (obj == Py_True) { + static PyObject *s_true = NULL; + if (s_true == NULL) { + s_true = PyString_InternFromString("true"); + } + Py_INCREF(s_true); + return s_true; + } + else if (obj == Py_False) { + static PyObject *s_false = NULL; + if (s_false == NULL) { + s_false = PyString_InternFromString("false"); + } + Py_INCREF(s_false); + return s_false; + } + else { + PyErr_SetString(PyExc_ValueError, "not a const"); + return NULL; + } +} + +static PyObject * +encoder_encode_float(PyEncoderObject *s, PyObject *obj) +{ + /* Return the JSON representation of a PyFloat */ + double i = PyFloat_AS_DOUBLE(obj); + if (!Py_IS_FINITE(i)) { + if (!s->allow_nan) { + PyErr_SetString(PyExc_ValueError, "Out of range float values are not JSON compliant"); + return NULL; + } + if (i > 0) { + return PyString_FromString("Infinity"); + } + else if (i < 0) { + return PyString_FromString("-Infinity"); + } + else { + return PyString_FromString("NaN"); + } + } + /* Use a better float format here? */ + return PyObject_Repr(obj); +} + +static PyObject * +encoder_encode_string(PyEncoderObject *s, PyObject *obj) +{ + /* Return the JSON representation of a string */ + if (s->fast_encode) + return py_encode_basestring_ascii(NULL, obj); + else + return PyObject_CallFunctionObjArgs(s->encoder, obj, NULL); +} + +static int +_steal_list_append(PyObject *lst, PyObject *stolen) +{ + /* Append stolen and then decrement its reference count */ + int rval = PyList_Append(lst, stolen); + Py_DECREF(stolen); + return rval; +} + +static int +encoder_listencode_obj(PyEncoderObject *s, PyObject *rval, PyObject *obj, Py_ssize_t indent_level) +{ + /* Encode Python object obj to a JSON term, rval is a PyList */ + PyObject *newobj; + int rv; + + if (obj == Py_None || obj == Py_True || obj == Py_False) { + PyObject *cstr = _encoded_const(obj); + if (cstr == NULL) + return -1; + return _steal_list_append(rval, cstr); + } + else if (PyString_Check(obj) || PyUnicode_Check(obj)) + { + PyObject *encoded = encoder_encode_string(s, obj); + if (encoded == NULL) + return -1; + return _steal_list_append(rval, encoded); + } + else if (PyInt_Check(obj) || PyLong_Check(obj)) { + PyObject *encoded = PyObject_Str(obj); + if (encoded == NULL) + return -1; + return _steal_list_append(rval, encoded); + } + else if (PyFloat_Check(obj)) { + PyObject *encoded = encoder_encode_float(s, obj); + if (encoded == NULL) + return -1; + return _steal_list_append(rval, encoded); + } + else if (PyList_Check(obj) || PyTuple_Check(obj)) { + return encoder_listencode_list(s, rval, obj, indent_level); + } + else if (PyDict_Check(obj)) { + return encoder_listencode_dict(s, rval, obj, indent_level); + } + else { + PyObject *ident = NULL; + if (s->markers != Py_None) { + int has_key; + ident = PyLong_FromVoidPtr(obj); + if (ident == NULL) + return -1; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) { + if (has_key != -1) + PyErr_SetString(PyExc_ValueError, "Circular reference detected"); + Py_DECREF(ident); + return -1; + } + if (PyDict_SetItem(s->markers, ident, obj)) { + Py_DECREF(ident); + return -1; + } + } + newobj = PyObject_CallFunctionObjArgs(s->defaultfn, obj, NULL); + if (newobj == NULL) { + Py_XDECREF(ident); + return -1; + } + rv = encoder_listencode_obj(s, rval, newobj, indent_level); + Py_DECREF(newobj); + if (rv) { + Py_XDECREF(ident); + return -1; + } + if (ident != NULL) { + if (PyDict_DelItem(s->markers, ident)) { + Py_XDECREF(ident); + return -1; + } + Py_XDECREF(ident); + } + return rv; + } +} + +static int +encoder_listencode_dict(PyEncoderObject *s, PyObject *rval, PyObject *dct, Py_ssize_t indent_level) +{ + /* Encode Python dict dct a JSON term, rval is a PyList */ + static PyObject *open_dict = NULL; + static PyObject *close_dict = NULL; + static PyObject *empty_dict = NULL; + PyObject *kstr = NULL; + PyObject *ident = NULL; + PyObject *key, *value; + Py_ssize_t pos; + int skipkeys; + Py_ssize_t idx; + + if (open_dict == NULL || close_dict == NULL || empty_dict == NULL) { + open_dict = PyString_InternFromString("{"); + close_dict = PyString_InternFromString("}"); + empty_dict = PyString_InternFromString("{}"); + if (open_dict == NULL || close_dict == NULL || empty_dict == NULL) + return -1; + } + if (PyDict_Size(dct) == 0) + return PyList_Append(rval, empty_dict); + + if (s->markers != Py_None) { + int has_key; + ident = PyLong_FromVoidPtr(dct); + if (ident == NULL) + goto bail; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) { + if (has_key != -1) + PyErr_SetString(PyExc_ValueError, "Circular reference detected"); + goto bail; + } + if (PyDict_SetItem(s->markers, ident, dct)) { + goto bail; + } + } + + if (PyList_Append(rval, open_dict)) + goto bail; + + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level += 1; + /* + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + separator = _item_separator + newline_indent + buf += newline_indent + */ + } + + /* TODO: C speedup not implemented for sort_keys */ + + pos = 0; + skipkeys = PyObject_IsTrue(s->skipkeys); + idx = 0; + while (PyDict_Next(dct, &pos, &key, &value)) { + PyObject *encoded; + + if (PyString_Check(key) || PyUnicode_Check(key)) { + Py_INCREF(key); + kstr = key; + } + else if (PyFloat_Check(key)) { + kstr = encoder_encode_float(s, key); + if (kstr == NULL) + goto bail; + } + else if (PyInt_Check(key) || PyLong_Check(key)) { + kstr = PyObject_Str(key); + if (kstr == NULL) + goto bail; + } + else if (key == Py_True || key == Py_False || key == Py_None) { + kstr = _encoded_const(key); + if (kstr == NULL) + goto bail; + } + else if (skipkeys) { + continue; + } + else { + /* TODO: include repr of key */ + PyErr_SetString(PyExc_ValueError, "keys must be a string"); + goto bail; + } + + if (idx) { + if (PyList_Append(rval, s->item_separator)) + goto bail; + } + + encoded = encoder_encode_string(s, kstr); + Py_CLEAR(kstr); + if (encoded == NULL) + goto bail; + if (PyList_Append(rval, encoded)) { + Py_DECREF(encoded); + goto bail; + } + Py_DECREF(encoded); + if (PyList_Append(rval, s->key_separator)) + goto bail; + if (encoder_listencode_obj(s, rval, value, indent_level)) + goto bail; + idx += 1; + } + if (ident != NULL) { + if (PyDict_DelItem(s->markers, ident)) + goto bail; + Py_CLEAR(ident); + } + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level -= 1; + /* + yield '\n' + (' ' * (_indent * _current_indent_level)) + */ + } + if (PyList_Append(rval, close_dict)) + goto bail; + return 0; + +bail: + Py_XDECREF(kstr); + Py_XDECREF(ident); + return -1; +} + + +static int +encoder_listencode_list(PyEncoderObject *s, PyObject *rval, PyObject *seq, Py_ssize_t indent_level) +{ + /* Encode Python list seq to a JSON term, rval is a PyList */ + static PyObject *open_array = NULL; + static PyObject *close_array = NULL; + static PyObject *empty_array = NULL; + PyObject *ident = NULL; + PyObject *s_fast = NULL; + Py_ssize_t num_items; + PyObject **seq_items; + Py_ssize_t i; + + if (open_array == NULL || close_array == NULL || empty_array == NULL) { + open_array = PyString_InternFromString("["); + close_array = PyString_InternFromString("]"); + empty_array = PyString_InternFromString("[]"); + if (open_array == NULL || close_array == NULL || empty_array == NULL) + return -1; + } + ident = NULL; + s_fast = PySequence_Fast(seq, "_iterencode_list needs a sequence"); + if (s_fast == NULL) + return -1; + num_items = PySequence_Fast_GET_SIZE(s_fast); + if (num_items == 0) { + Py_DECREF(s_fast); + return PyList_Append(rval, empty_array); + } + + if (s->markers != Py_None) { + int has_key; + ident = PyLong_FromVoidPtr(seq); + if (ident == NULL) + goto bail; + has_key = PyDict_Contains(s->markers, ident); + if (has_key) { + if (has_key != -1) + PyErr_SetString(PyExc_ValueError, "Circular reference detected"); + goto bail; + } + if (PyDict_SetItem(s->markers, ident, seq)) { + goto bail; + } + } + + seq_items = PySequence_Fast_ITEMS(s_fast); + if (PyList_Append(rval, open_array)) + goto bail; + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level += 1; + /* + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + separator = _item_separator + newline_indent + buf += newline_indent + */ + } + for (i = 0; i < num_items; i++) { + PyObject *obj = seq_items[i]; + if (i) { + if (PyList_Append(rval, s->item_separator)) + goto bail; + } + if (encoder_listencode_obj(s, rval, obj, indent_level)) + goto bail; + } + if (ident != NULL) { + if (PyDict_DelItem(s->markers, ident)) + goto bail; + Py_CLEAR(ident); + } + if (s->indent != Py_None) { + /* TODO: DOES NOT RUN */ + indent_level -= 1; + /* + yield '\n' + (' ' * (_indent * _current_indent_level)) + */ + } + if (PyList_Append(rval, close_array)) + goto bail; + Py_DECREF(s_fast); + return 0; + +bail: + Py_XDECREF(ident); + Py_DECREF(s_fast); + return -1; +} + +static void +encoder_dealloc(PyObject *self) +{ + /* Deallocate Encoder */ + encoder_clear(self); + Py_TYPE(self)->tp_free(self); +} + +static int +encoder_traverse(PyObject *self, visitproc visit, void *arg) +{ + PyEncoderObject *s; + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + Py_VISIT(s->markers); + Py_VISIT(s->defaultfn); + Py_VISIT(s->encoder); + Py_VISIT(s->indent); + Py_VISIT(s->key_separator); + Py_VISIT(s->item_separator); + Py_VISIT(s->sort_keys); + Py_VISIT(s->skipkeys); + return 0; +} + +static int +encoder_clear(PyObject *self) +{ + /* Deallocate Encoder */ + PyEncoderObject *s; + assert(PyEncoder_Check(self)); + s = (PyEncoderObject *)self; + Py_CLEAR(s->markers); + Py_CLEAR(s->defaultfn); + Py_CLEAR(s->encoder); + Py_CLEAR(s->indent); + Py_CLEAR(s->key_separator); + Py_CLEAR(s->item_separator); + Py_CLEAR(s->sort_keys); + Py_CLEAR(s->skipkeys); + return 0; +} + +PyDoc_STRVAR(encoder_doc, "_iterencode(obj, _current_indent_level) -> iterable"); + +static +PyTypeObject PyEncoderType = { + PyObject_HEAD_INIT(NULL) + 0, /* tp_internal */ + "simplejson._speedups.Encoder", /* tp_name */ + sizeof(PyEncoderObject), /* tp_basicsize */ + 0, /* tp_itemsize */ + encoder_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + encoder_call, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ + encoder_doc, /* tp_doc */ + encoder_traverse, /* tp_traverse */ + encoder_clear, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + encoder_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + encoder_init, /* tp_init */ + 0, /* tp_alloc */ + encoder_new, /* tp_new */ + 0, /* tp_free */ +}; + +static PyMethodDef speedups_methods[] = { + {"encode_basestring_ascii", + (PyCFunction)py_encode_basestring_ascii, + METH_O, + pydoc_encode_basestring_ascii}, + {"scanstring", + (PyCFunction)py_scanstring, + METH_VARARGS, + pydoc_scanstring}, + {NULL, NULL, 0, NULL} +}; + +PyDoc_STRVAR(module_doc, +"simplejson speedups\n"); + +void +init_speedups(void) +{ + PyObject *m; + PyScannerType.tp_new = PyType_GenericNew; + if (PyType_Ready(&PyScannerType) < 0) + return; + PyEncoderType.tp_new = PyType_GenericNew; + if (PyType_Ready(&PyEncoderType) < 0) + return; + m = Py_InitModule3("_speedups", speedups_methods, module_doc); + Py_INCREF((PyObject*)&PyScannerType); + PyModule_AddObject(m, "make_scanner", (PyObject*)&PyScannerType); + Py_INCREF((PyObject*)&PyEncoderType); + PyModule_AddObject(m, "make_encoder", (PyObject*)&PyEncoderType); +} diff --git a/resources/lib/simplejson/decoder.py b/resources/lib/simplejson/decoder.py new file mode 100644 index 0000000..b769ea4 --- /dev/null +++ b/resources/lib/simplejson/decoder.py @@ -0,0 +1,354 @@ +"""Implementation of JSONDecoder +""" +import re +import sys +import struct + +from simplejson.scanner import make_scanner +try: + from simplejson._speedups import scanstring as c_scanstring +except ImportError: + c_scanstring = None + +__all__ = ['JSONDecoder'] + +FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL + +def _floatconstants(): + _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') + if sys.byteorder != 'big': + _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] + nan, inf = struct.unpack('dd', _BYTES) + return nan, inf, -inf + +NaN, PosInf, NegInf = _floatconstants() + + +def linecol(doc, pos): + lineno = doc.count('\n', 0, pos) + 1 + if lineno == 1: + colno = pos + else: + colno = pos - doc.rindex('\n', 0, pos) + return lineno, colno + + +def errmsg(msg, doc, pos, end=None): + # Note that this function is called from _speedups + lineno, colno = linecol(doc, pos) + if end is None: + #fmt = '{0}: line {1} column {2} (char {3})' + #return fmt.format(msg, lineno, colno, pos) + fmt = '%s: line %d column %d (char %d)' + return fmt % (msg, lineno, colno, pos) + endlineno, endcolno = linecol(doc, end) + #fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})' + #return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end) + fmt = '%s: line %d column %d - line %d column %d (char %d - %d)' + return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end) + + +_CONSTANTS = { + '-Infinity': NegInf, + 'Infinity': PosInf, + 'NaN': NaN, +} + +STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) +BACKSLASH = { + '"': u'"', '\\': u'\\', '/': u'/', + 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', +} + +DEFAULT_ENCODING = "utf-8" + +def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match): + """Scan the string s for a JSON string. End is the index of the + character in s after the quote that started the JSON string. + Unescapes all valid JSON string escape sequences and raises ValueError + on attempt to decode an invalid string. If strict is False then literal + control characters are allowed in the string. + + Returns a tuple of the decoded string and the index of the character in s + after the end quote.""" + if encoding is None: + encoding = DEFAULT_ENCODING + chunks = [] + _append = chunks.append + begin = end - 1 + while 1: + chunk = _m(s, end) + if chunk is None: + raise ValueError( + errmsg("Unterminated string starting at", s, begin)) + end = chunk.end() + content, terminator = chunk.groups() + # Content is contains zero or more unescaped string characters + if content: + if not isinstance(content, unicode): + content = unicode(content, encoding) + _append(content) + # Terminator is the end of string, a literal control character, + # or a backslash denoting that an escape sequence follows + if terminator == '"': + break + elif terminator != '\\': + if strict: + msg = "Invalid control character %r at" % (terminator,) + #msg = "Invalid control character {0!r} at".format(terminator) + raise ValueError(errmsg(msg, s, end)) + else: + _append(terminator) + continue + try: + esc = s[end] + except IndexError: + raise ValueError( + errmsg("Unterminated string starting at", s, begin)) + # If not a unicode escape sequence, must be in the lookup table + if esc != 'u': + try: + char = _b[esc] + except KeyError: + msg = "Invalid \\escape: " + repr(esc) + raise ValueError(errmsg(msg, s, end)) + end += 1 + else: + # Unicode escape sequence + esc = s[end + 1:end + 5] + next_end = end + 5 + if len(esc) != 4: + msg = "Invalid \\uXXXX escape" + raise ValueError(errmsg(msg, s, end)) + uni = int(esc, 16) + # Check for surrogate pair on UCS-4 systems + if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: + msg = "Invalid \\uXXXX\\uXXXX surrogate pair" + if not s[end + 5:end + 7] == '\\u': + raise ValueError(errmsg(msg, s, end)) + esc2 = s[end + 7:end + 11] + if len(esc2) != 4: + raise ValueError(errmsg(msg, s, end)) + uni2 = int(esc2, 16) + uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) + next_end += 6 + char = unichr(uni) + end = next_end + # Append the unescaped character + _append(char) + return u''.join(chunks), end + + +# Use speedup if available +scanstring = c_scanstring or py_scanstring + +WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) +WHITESPACE_STR = ' \t\n\r' + +def JSONObject((s, end), encoding, strict, scan_once, object_hook, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + pairs = {} + # Use a slice to prevent IndexError from being raised, the following + # check will raise a more specific ValueError if the string is empty + nextchar = s[end:end + 1] + # Normally we expect nextchar == '"' + if nextchar != '"': + if nextchar in _ws: + end = _w(s, end).end() + nextchar = s[end:end + 1] + # Trivial empty object + if nextchar == '}': + return pairs, end + 1 + elif nextchar != '"': + raise ValueError(errmsg("Expecting property name", s, end)) + end += 1 + while True: + key, end = scanstring(s, end, encoding, strict) + + # To skip some function call overhead we optimize the fast paths where + # the JSON key separator is ": " or just ":". + if s[end:end + 1] != ':': + end = _w(s, end).end() + if s[end:end + 1] != ':': + raise ValueError(errmsg("Expecting : delimiter", s, end)) + + end += 1 + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + try: + value, end = scan_once(s, end) + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + pairs[key] = value + + try: + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + end += 1 + + if nextchar == '}': + break + elif nextchar != ',': + raise ValueError(errmsg("Expecting , delimiter", s, end - 1)) + + try: + nextchar = s[end] + if nextchar in _ws: + end += 1 + nextchar = s[end] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end] + except IndexError: + nextchar = '' + + end += 1 + if nextchar != '"': + raise ValueError(errmsg("Expecting property name", s, end - 1)) + + if object_hook is not None: + pairs = object_hook(pairs) + return pairs, end + +def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): + values = [] + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + # Look-ahead for trivial empty array + if nextchar == ']': + return values, end + 1 + _append = values.append + while True: + try: + value, end = scan_once(s, end) + except StopIteration: + raise ValueError(errmsg("Expecting object", s, end)) + _append(value) + nextchar = s[end:end + 1] + if nextchar in _ws: + end = _w(s, end + 1).end() + nextchar = s[end:end + 1] + end += 1 + if nextchar == ']': + break + elif nextchar != ',': + raise ValueError(errmsg("Expecting , delimiter", s, end)) + + try: + if s[end] in _ws: + end += 1 + if s[end] in _ws: + end = _w(s, end + 1).end() + except IndexError: + pass + + return values, end + +class JSONDecoder(object): + """Simple JSON decoder + + Performs the following translations in decoding by default: + + +---------------+-------------------+ + | JSON | Python | + +===============+===================+ + | object | dict | + +---------------+-------------------+ + | array | list | + +---------------+-------------------+ + | string | unicode | + +---------------+-------------------+ + | number (int) | int, long | + +---------------+-------------------+ + | number (real) | float | + +---------------+-------------------+ + | true | True | + +---------------+-------------------+ + | false | False | + +---------------+-------------------+ + | null | None | + +---------------+-------------------+ + + It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as + their corresponding ``float`` values, which is outside the JSON spec. + + """ + + def __init__(self, encoding=None, object_hook=None, parse_float=None, + parse_int=None, parse_constant=None, strict=True): + """``encoding`` determines the encoding used to interpret any ``str`` + objects decoded by this instance (utf-8 by default). It has no + effect when decoding ``unicode`` objects. + + Note that currently only encodings that are a superset of ASCII work, + strings of other encodings should be passed in as ``unicode``. + + ``object_hook``, if specified, will be called with the result + of every JSON object decoded and its return value will be used in + place of the given ``dict``. This can be used to provide custom + deserializations (e.g. to support JSON-RPC class hinting). + + ``parse_float``, if specified, will be called with the string + of every JSON float to be decoded. By default this is equivalent to + float(num_str). This can be used to use another datatype or parser + for JSON floats (e.g. decimal.Decimal). + + ``parse_int``, if specified, will be called with the string + of every JSON int to be decoded. By default this is equivalent to + int(num_str). This can be used to use another datatype or parser + for JSON integers (e.g. float). + + ``parse_constant``, if specified, will be called with one of the + following strings: -Infinity, Infinity, NaN. + This can be used to raise an exception if invalid JSON numbers + are encountered. + + """ + self.encoding = encoding + self.object_hook = object_hook + self.parse_float = parse_float or float + self.parse_int = parse_int or int + self.parse_constant = parse_constant or _CONSTANTS.__getitem__ + self.strict = strict + self.parse_object = JSONObject + self.parse_array = JSONArray + self.parse_string = scanstring + self.scan_once = make_scanner(self) + + def decode(self, s, _w=WHITESPACE.match): + """Return the Python representation of ``s`` (a ``str`` or ``unicode`` + instance containing a JSON document) + + """ + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + end = _w(s, end).end() + if end != len(s): + raise ValueError(errmsg("Extra data", s, end, len(s))) + return obj + + def raw_decode(self, s, idx=0): + """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning + with a JSON document) and return a 2-tuple of the Python + representation and the index in ``s`` where the document ended. + + This can be used to decode a JSON document from a string that may + have extraneous data at the end. + + """ + try: + obj, end = self.scan_once(s, idx) + except StopIteration: + raise ValueError("No JSON object could be decoded") + return obj, end diff --git a/resources/lib/simplejson/encoder.py b/resources/lib/simplejson/encoder.py new file mode 100644 index 0000000..cf58290 --- /dev/null +++ b/resources/lib/simplejson/encoder.py @@ -0,0 +1,440 @@ +"""Implementation of JSONEncoder +""" +import re + +try: + from simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii +except ImportError: + c_encode_basestring_ascii = None +try: + from simplejson._speedups import make_encoder as c_make_encoder +except ImportError: + c_make_encoder = None + +ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') +ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') +HAS_UTF8 = re.compile(r'[\x80-\xff]') +ESCAPE_DCT = { + '\\': '\\\\', + '"': '\\"', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', +} +for i in range(0x20): + #ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) + ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) + +# Assume this produces an infinity on all machines (probably not guaranteed) +INFINITY = float('1e66666') +FLOAT_REPR = repr + +def encode_basestring(s): + """Return a JSON representation of a Python string + + """ + def replace(match): + return ESCAPE_DCT[match.group(0)] + return '"' + ESCAPE.sub(replace, s) + '"' + + +def py_encode_basestring_ascii(s): + """Return an ASCII-only JSON representation of a Python string + + """ + if isinstance(s, str) and HAS_UTF8.search(s) is not None: + s = s.decode('utf-8') + def replace(match): + s = match.group(0) + try: + return ESCAPE_DCT[s] + except KeyError: + n = ord(s) + if n < 0x10000: + #return '\\u{0:04x}'.format(n) + return '\\u%04x' % (n,) + else: + # surrogate pair + n -= 0x10000 + s1 = 0xd800 | ((n >> 10) & 0x3ff) + s2 = 0xdc00 | (n & 0x3ff) + #return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) + return '\\u%04x\\u%04x' % (s1, s2) + return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' + + +encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii + +class JSONEncoder(object): + """Extensible JSON encoder for Python data structures. + + Supports the following objects and types by default: + + +-------------------+---------------+ + | Python | JSON | + +===================+===============+ + | dict | object | + +-------------------+---------------+ + | list, tuple | array | + +-------------------+---------------+ + | str, unicode | string | + +-------------------+---------------+ + | int, long, float | number | + +-------------------+---------------+ + | True | true | + +-------------------+---------------+ + | False | false | + +-------------------+---------------+ + | None | null | + +-------------------+---------------+ + + To extend this to recognize other objects, subclass and implement a + ``.default()`` method with another method that returns a serializable + object for ``o`` if possible, otherwise it should call the superclass + implementation (to raise ``TypeError``). + + """ + item_separator = ', ' + key_separator = ': ' + def __init__(self, skipkeys=False, ensure_ascii=True, + check_circular=True, allow_nan=True, sort_keys=False, + indent=None, separators=None, encoding='utf-8', default=None): + """Constructor for JSONEncoder, with sensible defaults. + + If skipkeys is false, then it is a TypeError to attempt + encoding of keys that are not str, int, long, float or None. If + skipkeys is True, such items are simply skipped. + + If ensure_ascii is true, the output is guaranteed to be str + objects with all incoming unicode characters escaped. If + ensure_ascii is false, the output will be unicode object. + + If check_circular is true, then lists, dicts, and custom encoded + objects will be checked for circular references during encoding to + prevent an infinite recursion (which would cause an OverflowError). + Otherwise, no such check takes place. + + If allow_nan is true, then NaN, Infinity, and -Infinity will be + encoded as such. This behavior is not JSON specification compliant, + but is consistent with most JavaScript based encoders and decoders. + Otherwise, it will be a ValueError to encode such floats. + + If sort_keys is true, then the output of dictionaries will be + sorted by key; this is useful for regression tests to ensure + that JSON serializations can be compared on a day-to-day basis. + + If indent is a non-negative integer, then JSON array + elements and object members will be pretty-printed with that + indent level. An indent level of 0 will only insert newlines. + None is the most compact representation. + + If specified, separators should be a (item_separator, key_separator) + tuple. The default is (', ', ': '). To get the most compact JSON + representation you should specify (',', ':') to eliminate whitespace. + + If specified, default is a function that gets called for objects + that can't otherwise be serialized. It should return a JSON encodable + version of the object or raise a ``TypeError``. + + If encoding is not None, then all input strings will be + transformed into unicode using that encoding prior to JSON-encoding. + The default is UTF-8. + + """ + + self.skipkeys = skipkeys + self.ensure_ascii = ensure_ascii + self.check_circular = check_circular + self.allow_nan = allow_nan + self.sort_keys = sort_keys + self.indent = indent + if separators is not None: + self.item_separator, self.key_separator = separators + if default is not None: + self.default = default + self.encoding = encoding + + def default(self, o): + """Implement this method in a subclass such that it returns + a serializable object for ``o``, or calls the base implementation + (to raise a ``TypeError``). + + For example, to support arbitrary iterators, you could + implement default like this:: + + def default(self, o): + try: + iterable = iter(o) + except TypeError: + pass + else: + return list(iterable) + return JSONEncoder.default(self, o) + + """ + raise TypeError(repr(o) + " is not JSON serializable") + + def encode(self, o): + """Return a JSON string representation of a Python data structure. + + >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) + '{"foo": ["bar", "baz"]}' + + """ + # This is for extremely simple cases and benchmarks. + if isinstance(o, basestring): + if isinstance(o, str): + _encoding = self.encoding + if (_encoding is not None + and not (_encoding == 'utf-8')): + o = o.decode(_encoding) + if self.ensure_ascii: + return encode_basestring_ascii(o) + else: + return encode_basestring(o) + # This doesn't pass the iterator directly to ''.join() because the + # exceptions aren't as detailed. The list call should be roughly + # equivalent to the PySequence_Fast that ''.join() would do. + chunks = self.iterencode(o, _one_shot=True) + if not isinstance(chunks, (list, tuple)): + chunks = list(chunks) + return ''.join(chunks) + + def iterencode(self, o, _one_shot=False): + """Encode the given object and yield each string + representation as available. + + For example:: + + for chunk in JSONEncoder().iterencode(bigobject): + mysocket.write(chunk) + + """ + if self.check_circular: + markers = {} + else: + markers = None + if self.ensure_ascii: + _encoder = encode_basestring_ascii + else: + _encoder = encode_basestring + if self.encoding != 'utf-8': + def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): + if isinstance(o, str): + o = o.decode(_encoding) + return _orig_encoder(o) + + def floatstr(o, allow_nan=self.allow_nan, _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY): + # Check for specials. Note that this type of test is processor- and/or + # platform-specific, so do tests which don't depend on the internals. + + if o != o: + text = 'NaN' + elif o == _inf: + text = 'Infinity' + elif o == _neginf: + text = '-Infinity' + else: + return _repr(o) + + if not allow_nan: + raise ValueError( + "Out of range float values are not JSON compliant: " + + repr(o)) + + return text + + + if _one_shot and c_make_encoder is not None and not self.indent and not self.sort_keys: + _iterencode = c_make_encoder( + markers, self.default, _encoder, self.indent, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, self.allow_nan) + else: + _iterencode = _make_iterencode( + markers, self.default, _encoder, self.indent, floatstr, + self.key_separator, self.item_separator, self.sort_keys, + self.skipkeys, _one_shot) + return _iterencode(o, 0) + +def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, + ## HACK: hand-optimized bytecode; turn globals into locals + False=False, + True=True, + ValueError=ValueError, + basestring=basestring, + dict=dict, + float=float, + id=id, + int=int, + isinstance=isinstance, + list=list, + long=long, + str=str, + tuple=tuple, + ): + + def _iterencode_list(lst, _current_indent_level): + if not lst: + yield '[]' + return + if markers is not None: + markerid = id(lst) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = lst + buf = '[' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + separator = _item_separator + newline_indent + buf += newline_indent + else: + newline_indent = None + separator = _item_separator + first = True + for value in lst: + if first: + first = False + else: + buf = separator + if isinstance(value, basestring): + yield buf + _encoder(value) + elif value is None: + yield buf + 'null' + elif value is True: + yield buf + 'true' + elif value is False: + yield buf + 'false' + elif isinstance(value, (int, long)): + yield buf + str(value) + elif isinstance(value, float): + yield buf + _floatstr(value) + else: + yield buf + if isinstance(value, (list, tuple)): + chunks = _iterencode_list(value, _current_indent_level) + elif isinstance(value, dict): + chunks = _iterencode_dict(value, _current_indent_level) + else: + chunks = _iterencode(value, _current_indent_level) + for chunk in chunks: + yield chunk + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + (' ' * (_indent * _current_indent_level)) + yield ']' + if markers is not None: + del markers[markerid] + + def _iterencode_dict(dct, _current_indent_level): + if not dct: + yield '{}' + return + if markers is not None: + markerid = id(dct) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = dct + yield '{' + if _indent is not None: + _current_indent_level += 1 + newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) + item_separator = _item_separator + newline_indent + yield newline_indent + else: + newline_indent = None + item_separator = _item_separator + first = True + if _sort_keys: + items = dct.items() + items.sort(key=lambda kv: kv[0]) + else: + items = dct.iteritems() + for key, value in items: + if isinstance(key, basestring): + pass + # JavaScript is weakly typed for these, so it makes sense to + # also allow them. Many encoders seem to do something like this. + elif isinstance(key, float): + key = _floatstr(key) + elif key is True: + key = 'true' + elif key is False: + key = 'false' + elif key is None: + key = 'null' + elif isinstance(key, (int, long)): + key = str(key) + elif _skipkeys: + continue + else: + raise TypeError("key " + repr(key) + " is not a string") + if first: + first = False + else: + yield item_separator + yield _encoder(key) + yield _key_separator + if isinstance(value, basestring): + yield _encoder(value) + elif value is None: + yield 'null' + elif value is True: + yield 'true' + elif value is False: + yield 'false' + elif isinstance(value, (int, long)): + yield str(value) + elif isinstance(value, float): + yield _floatstr(value) + else: + if isinstance(value, (list, tuple)): + chunks = _iterencode_list(value, _current_indent_level) + elif isinstance(value, dict): + chunks = _iterencode_dict(value, _current_indent_level) + else: + chunks = _iterencode(value, _current_indent_level) + for chunk in chunks: + yield chunk + if newline_indent is not None: + _current_indent_level -= 1 + yield '\n' + (' ' * (_indent * _current_indent_level)) + yield '}' + if markers is not None: + del markers[markerid] + + def _iterencode(o, _current_indent_level): + if isinstance(o, basestring): + yield _encoder(o) + elif o is None: + yield 'null' + elif o is True: + yield 'true' + elif o is False: + yield 'false' + elif isinstance(o, (int, long)): + yield str(o) + elif isinstance(o, float): + yield _floatstr(o) + elif isinstance(o, (list, tuple)): + for chunk in _iterencode_list(o, _current_indent_level): + yield chunk + elif isinstance(o, dict): + for chunk in _iterencode_dict(o, _current_indent_level): + yield chunk + else: + if markers is not None: + markerid = id(o) + if markerid in markers: + raise ValueError("Circular reference detected") + markers[markerid] = o + o = _default(o) + for chunk in _iterencode(o, _current_indent_level): + yield chunk + if markers is not None: + del markers[markerid] + + return _iterencode diff --git a/resources/lib/simplejson/scanner.py b/resources/lib/simplejson/scanner.py new file mode 100644 index 0000000..adbc6ec --- /dev/null +++ b/resources/lib/simplejson/scanner.py @@ -0,0 +1,65 @@ +"""JSON token scanner +""" +import re +try: + from simplejson._speedups import make_scanner as c_make_scanner +except ImportError: + c_make_scanner = None + +__all__ = ['make_scanner'] + +NUMBER_RE = re.compile( + r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', + (re.VERBOSE | re.MULTILINE | re.DOTALL)) + +def py_make_scanner(context): + parse_object = context.parse_object + parse_array = context.parse_array + parse_string = context.parse_string + match_number = NUMBER_RE.match + encoding = context.encoding + strict = context.strict + parse_float = context.parse_float + parse_int = context.parse_int + parse_constant = context.parse_constant + object_hook = context.object_hook + + def _scan_once(string, idx): + try: + nextchar = string[idx] + except IndexError: + raise StopIteration + + if nextchar == '"': + return parse_string(string, idx + 1, encoding, strict) + elif nextchar == '{': + return parse_object((string, idx + 1), encoding, strict, _scan_once, object_hook) + elif nextchar == '[': + return parse_array((string, idx + 1), _scan_once) + elif nextchar == 'n' and string[idx:idx + 4] == 'null': + return None, idx + 4 + elif nextchar == 't' and string[idx:idx + 4] == 'true': + return True, idx + 4 + elif nextchar == 'f' and string[idx:idx + 5] == 'false': + return False, idx + 5 + + m = match_number(string, idx) + if m is not None: + integer, frac, exp = m.groups() + if frac or exp: + res = parse_float(integer + (frac or '') + (exp or '')) + else: + res = parse_int(integer) + return res, m.end() + elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': + return parse_constant('NaN'), idx + 3 + elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': + return parse_constant('Infinity'), idx + 8 + elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': + return parse_constant('-Infinity'), idx + 9 + else: + raise StopIteration + + return _scan_once + +make_scanner = c_make_scanner or py_make_scanner diff --git a/resources/lib/simplejson/tests/__init__.py b/resources/lib/simplejson/tests/__init__.py new file mode 100644 index 0000000..17c9796 --- /dev/null +++ b/resources/lib/simplejson/tests/__init__.py @@ -0,0 +1,23 @@ +import unittest +import doctest + +def additional_tests(): + import simplejson + import simplejson.encoder + import simplejson.decoder + suite = unittest.TestSuite() + for mod in (simplejson, simplejson.encoder, simplejson.decoder): + suite.addTest(doctest.DocTestSuite(mod)) + suite.addTest(doctest.DocFileSuite('../../index.rst')) + return suite + +def main(): + suite = additional_tests() + runner = unittest.TextTestRunner() + runner.run(suite) + +if __name__ == '__main__': + import os + import sys + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + main() diff --git a/resources/lib/simplejson/tests/test_check_circular.py b/resources/lib/simplejson/tests/test_check_circular.py new file mode 100644 index 0000000..af6463d --- /dev/null +++ b/resources/lib/simplejson/tests/test_check_circular.py @@ -0,0 +1,30 @@ +from unittest import TestCase +import simplejson as json + +def default_iterable(obj): + return list(obj) + +class TestCheckCircular(TestCase): + def test_circular_dict(self): + dct = {} + dct['a'] = dct + self.assertRaises(ValueError, json.dumps, dct) + + def test_circular_list(self): + lst = [] + lst.append(lst) + self.assertRaises(ValueError, json.dumps, lst) + + def test_circular_composite(self): + dct2 = {} + dct2['a'] = [] + dct2['a'].append(dct2) + self.assertRaises(ValueError, json.dumps, dct2) + + def test_circular_default(self): + json.dumps([set()], default=default_iterable) + self.assertRaises(TypeError, json.dumps, [set()]) + + def test_circular_off_default(self): + json.dumps([set()], default=default_iterable, check_circular=False) + self.assertRaises(TypeError, json.dumps, [set()], check_circular=False) diff --git a/resources/lib/simplejson/tests/test_decode.py b/resources/lib/simplejson/tests/test_decode.py new file mode 100644 index 0000000..1cd701d --- /dev/null +++ b/resources/lib/simplejson/tests/test_decode.py @@ -0,0 +1,22 @@ +import decimal +from unittest import TestCase + +import simplejson as json + +class TestDecode(TestCase): + def test_decimal(self): + rval = json.loads('1.1', parse_float=decimal.Decimal) + self.assert_(isinstance(rval, decimal.Decimal)) + self.assertEquals(rval, decimal.Decimal('1.1')) + + def test_float(self): + rval = json.loads('1', parse_int=float) + self.assert_(isinstance(rval, float)) + self.assertEquals(rval, 1.0) + + def test_decoder_optimizations(self): + # Several optimizations were made that skip over calls to + # the whitespace regex, so this test is designed to try and + # exercise the uncommon cases. The array cases are already covered. + rval = json.loads('{ "key" : "value" , "k":"v" }') + self.assertEquals(rval, {"key":"value", "k":"v"}) diff --git a/resources/lib/simplejson/tests/test_default.py b/resources/lib/simplejson/tests/test_default.py new file mode 100644 index 0000000..139e42b --- /dev/null +++ b/resources/lib/simplejson/tests/test_default.py @@ -0,0 +1,9 @@ +from unittest import TestCase + +import simplejson as json + +class TestDefault(TestCase): + def test_default(self): + self.assertEquals( + json.dumps(type, default=repr), + json.dumps(repr(type))) diff --git a/resources/lib/simplejson/tests/test_dump.py b/resources/lib/simplejson/tests/test_dump.py new file mode 100644 index 0000000..4de37cf --- /dev/null +++ b/resources/lib/simplejson/tests/test_dump.py @@ -0,0 +1,21 @@ +from unittest import TestCase +from cStringIO import StringIO + +import simplejson as json + +class TestDump(TestCase): + def test_dump(self): + sio = StringIO() + json.dump({}, sio) + self.assertEquals(sio.getvalue(), '{}') + + def test_dumps(self): + self.assertEquals(json.dumps({}), '{}') + + def test_encode_truefalse(self): + self.assertEquals(json.dumps( + {True: False, False: True}, sort_keys=True), + '{"false": true, "true": false}') + self.assertEquals(json.dumps( + {2: 3.0, 4.0: 5L, False: 1, 6L: True, "7": 0}, sort_keys=True), + '{"false": 1, "2": 3.0, "4.0": 5, "6": true, "7": 0}') diff --git a/resources/lib/simplejson/tests/test_encode_basestring_ascii.py b/resources/lib/simplejson/tests/test_encode_basestring_ascii.py new file mode 100644 index 0000000..7128495 --- /dev/null +++ b/resources/lib/simplejson/tests/test_encode_basestring_ascii.py @@ -0,0 +1,38 @@ +from unittest import TestCase + +import simplejson.encoder + +CASES = [ + (u'/\\"\ucafe\ubabe\uab98\ufcde\ubcda\uef4a\x08\x0c\n\r\t`1~!@#$%^&*()_+-=[]{}|;:\',./<>?', '"/\\\\\\"\\ucafe\\ubabe\\uab98\\ufcde\\ubcda\\uef4a\\b\\f\\n\\r\\t`1~!@#$%^&*()_+-=[]{}|;:\',./<>?"'), + (u'\u0123\u4567\u89ab\ucdef\uabcd\uef4a', '"\\u0123\\u4567\\u89ab\\ucdef\\uabcd\\uef4a"'), + (u'controls', '"controls"'), + (u'\x08\x0c\n\r\t', '"\\b\\f\\n\\r\\t"'), + (u'{"object with 1 member":["array with 1 element"]}', '"{\\"object with 1 member\\":[\\"array with 1 element\\"]}"'), + (u' s p a c e d ', '" s p a c e d "'), + (u'\U0001d120', '"\\ud834\\udd20"'), + (u'\u03b1\u03a9', '"\\u03b1\\u03a9"'), + ('\xce\xb1\xce\xa9', '"\\u03b1\\u03a9"'), + (u'\u03b1\u03a9', '"\\u03b1\\u03a9"'), + ('\xce\xb1\xce\xa9', '"\\u03b1\\u03a9"'), + (u'\u03b1\u03a9', '"\\u03b1\\u03a9"'), + (u'\u03b1\u03a9', '"\\u03b1\\u03a9"'), + (u"`1~!@#$%^&*()_+-={':[,]}|;.?", '"`1~!@#$%^&*()_+-={\':[,]}|;.?"'), + (u'\x08\x0c\n\r\t', '"\\b\\f\\n\\r\\t"'), + (u'\u0123\u4567\u89ab\ucdef\uabcd\uef4a', '"\\u0123\\u4567\\u89ab\\ucdef\\uabcd\\uef4a"'), +] + +class TestEncodeBaseStringAscii(TestCase): + def test_py_encode_basestring_ascii(self): + self._test_encode_basestring_ascii(simplejson.encoder.py_encode_basestring_ascii) + + def test_c_encode_basestring_ascii(self): + if not simplejson.encoder.c_encode_basestring_ascii: + return + self._test_encode_basestring_ascii(simplejson.encoder.c_encode_basestring_ascii) + + def _test_encode_basestring_ascii(self, encode_basestring_ascii): + fname = encode_basestring_ascii.__name__ + for input_string, expect in CASES: + result = encode_basestring_ascii(input_string) + self.assertEquals(result, expect, + '%r != %r for %s(%r)' % (result, expect, fname, input_string)) diff --git a/resources/lib/simplejson/tests/test_fail.py b/resources/lib/simplejson/tests/test_fail.py new file mode 100644 index 0000000..002eea0 --- /dev/null +++ b/resources/lib/simplejson/tests/test_fail.py @@ -0,0 +1,76 @@ +from unittest import TestCase + +import simplejson as json + +# Fri Dec 30 18:57:26 2005 +JSONDOCS = [ + # http://json.org/JSON_checker/test/fail1.json + '"A JSON payload should be an object or array, not a string."', + # http://json.org/JSON_checker/test/fail2.json + '["Unclosed array"', + # http://json.org/JSON_checker/test/fail3.json + '{unquoted_key: "keys must be quoted}', + # http://json.org/JSON_checker/test/fail4.json + '["extra comma",]', + # http://json.org/JSON_checker/test/fail5.json + '["double extra comma",,]', + # http://json.org/JSON_checker/test/fail6.json + '[ , "<-- missing value"]', + # http://json.org/JSON_checker/test/fail7.json + '["Comma after the close"],', + # http://json.org/JSON_checker/test/fail8.json + '["Extra close"]]', + # http://json.org/JSON_checker/test/fail9.json + '{"Extra comma": true,}', + # http://json.org/JSON_checker/test/fail10.json + '{"Extra value after close": true} "misplaced quoted value"', + # http://json.org/JSON_checker/test/fail11.json + '{"Illegal expression": 1 + 2}', + # http://json.org/JSON_checker/test/fail12.json + '{"Illegal invocation": alert()}', + # http://json.org/JSON_checker/test/fail13.json + '{"Numbers cannot have leading zeroes": 013}', + # http://json.org/JSON_checker/test/fail14.json + '{"Numbers cannot be hex": 0x14}', + # http://json.org/JSON_checker/test/fail15.json + '["Illegal backslash escape: \\x15"]', + # http://json.org/JSON_checker/test/fail16.json + '["Illegal backslash escape: \\\'"]', + # http://json.org/JSON_checker/test/fail17.json + '["Illegal backslash escape: \\017"]', + # http://json.org/JSON_checker/test/fail18.json + '[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]', + # http://json.org/JSON_checker/test/fail19.json + '{"Missing colon" null}', + # http://json.org/JSON_checker/test/fail20.json + '{"Double colon":: null}', + # http://json.org/JSON_checker/test/fail21.json + '{"Comma instead of colon", null}', + # http://json.org/JSON_checker/test/fail22.json + '["Colon instead of comma": false]', + # http://json.org/JSON_checker/test/fail23.json + '["Bad value", truth]', + # http://json.org/JSON_checker/test/fail24.json + "['single quote']", + # http://code.google.com/p/simplejson/issues/detail?id=3 + u'["A\u001FZ control characters in string"]', +] + +SKIPS = { + 1: "why not have a string payload?", + 18: "spec doesn't specify any nesting limitations", +} + +class TestFail(TestCase): + def test_failures(self): + for idx, doc in enumerate(JSONDOCS): + idx = idx + 1 + if idx in SKIPS: + json.loads(doc) + continue + try: + json.loads(doc) + except ValueError: + pass + else: + self.fail("Expected failure for fail%d.json: %r" % (idx, doc)) diff --git a/resources/lib/simplejson/tests/test_float.py b/resources/lib/simplejson/tests/test_float.py new file mode 100644 index 0000000..1a2b98a --- /dev/null +++ b/resources/lib/simplejson/tests/test_float.py @@ -0,0 +1,15 @@ +import math +from unittest import TestCase + +import simplejson as json + +class TestFloat(TestCase): + def test_floats(self): + for num in [1617161771.7650001, math.pi, math.pi**100, math.pi**-100, 3.1]: + self.assertEquals(float(json.dumps(num)), num) + self.assertEquals(json.loads(json.dumps(num)), num) + + def test_ints(self): + for num in [1, 1L, 1<<32, 1<<64]: + self.assertEquals(json.dumps(num), str(num)) + self.assertEquals(int(json.dumps(num)), num) diff --git a/resources/lib/simplejson/tests/test_indent.py b/resources/lib/simplejson/tests/test_indent.py new file mode 100644 index 0000000..66e19b9 --- /dev/null +++ b/resources/lib/simplejson/tests/test_indent.py @@ -0,0 +1,41 @@ +from unittest import TestCase + +import simplejson as json +import textwrap + +class TestIndent(TestCase): + def test_indent(self): + h = [['blorpie'], ['whoops'], [], 'd-shtaeou', 'd-nthiouh', 'i-vhbjkhnth', + {'nifty': 87}, {'field': 'yes', 'morefield': False} ] + + expect = textwrap.dedent("""\ + [ + [ + "blorpie" + ], + [ + "whoops" + ], + [], + "d-shtaeou", + "d-nthiouh", + "i-vhbjkhnth", + { + "nifty": 87 + }, + { + "field": "yes", + "morefield": false + } + ]""") + + + d1 = json.dumps(h) + d2 = json.dumps(h, indent=2, sort_keys=True, separators=(',', ': ')) + + h1 = json.loads(d1) + h2 = json.loads(d2) + + self.assertEquals(h1, h) + self.assertEquals(h2, h) + self.assertEquals(d2, expect) diff --git a/resources/lib/simplejson/tests/test_pass1.py b/resources/lib/simplejson/tests/test_pass1.py new file mode 100644 index 0000000..c3d6302 --- /dev/null +++ b/resources/lib/simplejson/tests/test_pass1.py @@ -0,0 +1,76 @@ +from unittest import TestCase + +import simplejson as json + +# from http://json.org/JSON_checker/test/pass1.json +JSON = r''' +[ + "JSON Test Pattern pass1", + {"object with 1 member":["array with 1 element"]}, + {}, + [], + -42, + true, + false, + null, + { + "integer": 1234567890, + "real": -9876.543210, + "e": 0.123456789e-12, + "E": 1.234567890E+34, + "": 23456789012E666, + "zero": 0, + "one": 1, + "space": " ", + "quote": "\"", + "backslash": "\\", + "controls": "\b\f\n\r\t", + "slash": "/ & \/", + "alpha": "abcdefghijklmnopqrstuvwyz", + "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ", + "digit": "0123456789", + "special": "`1~!@#$%^&*()_+-={':[,]}|;.?", + "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A", + "true": true, + "false": false, + "null": null, + "array":[ ], + "object":{ }, + "address": "50 St. James Street", + "url": "http://www.JSON.org/", + "comment": "// /* */": " ", + " s p a c e d " :[1,2 , 3 + +, + +4 , 5 , 6 ,7 ], + "compact": [1,2,3,4,5,6,7], + "jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}", + "quotes": "" \u0022 %22 0x22 034 "", + "\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?" +: "A key can be any string" + }, + 0.5 ,98.6 +, +99.44 +, + +1066 + + +,"rosebud"] +''' + +class TestPass1(TestCase): + def test_parse(self): + # test in/out equivalence and parsing + res = json.loads(JSON) + out = json.dumps(res) + self.assertEquals(res, json.loads(out)) + try: + json.dumps(res, allow_nan=False) + except ValueError: + pass + else: + self.fail("23456789012E666 should be out of range") diff --git a/resources/lib/simplejson/tests/test_pass2.py b/resources/lib/simplejson/tests/test_pass2.py new file mode 100644 index 0000000..de4ee00 --- /dev/null +++ b/resources/lib/simplejson/tests/test_pass2.py @@ -0,0 +1,14 @@ +from unittest import TestCase +import simplejson as json + +# from http://json.org/JSON_checker/test/pass2.json +JSON = r''' +[[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]] +''' + +class TestPass2(TestCase): + def test_parse(self): + # test in/out equivalence and parsing + res = json.loads(JSON) + out = json.dumps(res) + self.assertEquals(res, json.loads(out)) diff --git a/resources/lib/simplejson/tests/test_pass3.py b/resources/lib/simplejson/tests/test_pass3.py new file mode 100644 index 0000000..f591aba --- /dev/null +++ b/resources/lib/simplejson/tests/test_pass3.py @@ -0,0 +1,20 @@ +from unittest import TestCase + +import simplejson as json + +# from http://json.org/JSON_checker/test/pass3.json +JSON = r''' +{ + "JSON Test Pattern pass3": { + "The outermost value": "must be an object or array.", + "In this test": "It is an object." + } +} +''' + +class TestPass3(TestCase): + def test_parse(self): + # test in/out equivalence and parsing + res = json.loads(JSON) + out = json.dumps(res) + self.assertEquals(res, json.loads(out)) diff --git a/resources/lib/simplejson/tests/test_recursion.py b/resources/lib/simplejson/tests/test_recursion.py new file mode 100644 index 0000000..97422a6 --- /dev/null +++ b/resources/lib/simplejson/tests/test_recursion.py @@ -0,0 +1,67 @@ +from unittest import TestCase + +import simplejson as json + +class JSONTestObject: + pass + + +class RecursiveJSONEncoder(json.JSONEncoder): + recurse = False + def default(self, o): + if o is JSONTestObject: + if self.recurse: + return [JSONTestObject] + else: + return 'JSONTestObject' + return json.JSONEncoder.default(o) + + +class TestRecursion(TestCase): + def test_listrecursion(self): + x = [] + x.append(x) + try: + json.dumps(x) + except ValueError: + pass + else: + self.fail("didn't raise ValueError on list recursion") + x = [] + y = [x] + x.append(y) + try: + json.dumps(x) + except ValueError: + pass + else: + self.fail("didn't raise ValueError on alternating list recursion") + y = [] + x = [y, y] + # ensure that the marker is cleared + json.dumps(x) + + def test_dictrecursion(self): + x = {} + x["test"] = x + try: + json.dumps(x) + except ValueError: + pass + else: + self.fail("didn't raise ValueError on dict recursion") + x = {} + y = {"a": x, "b": x} + # ensure that the marker is cleared + json.dumps(x) + + def test_defaultrecursion(self): + enc = RecursiveJSONEncoder() + self.assertEquals(enc.encode(JSONTestObject), '"JSONTestObject"') + enc.recurse = True + try: + enc.encode(JSONTestObject) + except ValueError: + pass + else: + self.fail("didn't raise ValueError on default recursion") diff --git a/resources/lib/simplejson/tests/test_scanstring.py b/resources/lib/simplejson/tests/test_scanstring.py new file mode 100644 index 0000000..b08dec7 --- /dev/null +++ b/resources/lib/simplejson/tests/test_scanstring.py @@ -0,0 +1,111 @@ +import sys +import decimal +from unittest import TestCase + +import simplejson as json +import simplejson.decoder + +class TestScanString(TestCase): + def test_py_scanstring(self): + self._test_scanstring(simplejson.decoder.py_scanstring) + + def test_c_scanstring(self): + if not simplejson.decoder.c_scanstring: + return + self._test_scanstring(simplejson.decoder.c_scanstring) + + def _test_scanstring(self, scanstring): + self.assertEquals( + scanstring('"z\\ud834\\udd20x"', 1, None, True), + (u'z\U0001d120x', 16)) + + if sys.maxunicode == 65535: + self.assertEquals( + scanstring(u'"z\U0001d120x"', 1, None, True), + (u'z\U0001d120x', 6)) + else: + self.assertEquals( + scanstring(u'"z\U0001d120x"', 1, None, True), + (u'z\U0001d120x', 5)) + + self.assertEquals( + scanstring('"\\u007b"', 1, None, True), + (u'{', 8)) + + self.assertEquals( + scanstring('"A JSON payload should be an object or array, not a string."', 1, None, True), + (u'A JSON payload should be an object or array, not a string.', 60)) + + self.assertEquals( + scanstring('["Unclosed array"', 2, None, True), + (u'Unclosed array', 17)) + + self.assertEquals( + scanstring('["extra comma",]', 2, None, True), + (u'extra comma', 14)) + + self.assertEquals( + scanstring('["double extra comma",,]', 2, None, True), + (u'double extra comma', 21)) + + self.assertEquals( + scanstring('["Comma after the close"],', 2, None, True), + (u'Comma after the close', 24)) + + self.assertEquals( + scanstring('["Extra close"]]', 2, None, True), + (u'Extra close', 14)) + + self.assertEquals( + scanstring('{"Extra comma": true,}', 2, None, True), + (u'Extra comma', 14)) + + self.assertEquals( + scanstring('{"Extra value after close": true} "misplaced quoted value"', 2, None, True), + (u'Extra value after close', 26)) + + self.assertEquals( + scanstring('{"Illegal expression": 1 + 2}', 2, None, True), + (u'Illegal expression', 21)) + + self.assertEquals( + scanstring('{"Illegal invocation": alert()}', 2, None, True), + (u'Illegal invocation', 21)) + + self.assertEquals( + scanstring('{"Numbers cannot have leading zeroes": 013}', 2, None, True), + (u'Numbers cannot have leading zeroes', 37)) + + self.assertEquals( + scanstring('{"Numbers cannot be hex": 0x14}', 2, None, True), + (u'Numbers cannot be hex', 24)) + + self.assertEquals( + scanstring('[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]', 21, None, True), + (u'Too deep', 30)) + + self.assertEquals( + scanstring('{"Missing colon" null}', 2, None, True), + (u'Missing colon', 16)) + + self.assertEquals( + scanstring('{"Double colon":: null}', 2, None, True), + (u'Double colon', 15)) + + self.assertEquals( + scanstring('{"Comma instead of colon", null}', 2, None, True), + (u'Comma instead of colon', 25)) + + self.assertEquals( + scanstring('["Colon instead of comma": false]', 2, None, True), + (u'Colon instead of comma', 25)) + + self.assertEquals( + scanstring('["Bad value", truth]', 2, None, True), + (u'Bad value', 12)) + + def test_issue3623(self): + self.assertRaises(ValueError, json.decoder.scanstring, "xxx", 1, + "xxx") + self.assertRaises(UnicodeDecodeError, + json.encoder.encode_basestring_ascii, "xx\xff") diff --git a/resources/lib/simplejson/tests/test_separators.py b/resources/lib/simplejson/tests/test_separators.py new file mode 100644 index 0000000..8fa0dac --- /dev/null +++ b/resources/lib/simplejson/tests/test_separators.py @@ -0,0 +1,42 @@ +import textwrap +from unittest import TestCase + +import simplejson as json + + +class TestSeparators(TestCase): + def test_separators(self): + h = [['blorpie'], ['whoops'], [], 'd-shtaeou', 'd-nthiouh', 'i-vhbjkhnth', + {'nifty': 87}, {'field': 'yes', 'morefield': False} ] + + expect = textwrap.dedent("""\ + [ + [ + "blorpie" + ] , + [ + "whoops" + ] , + [] , + "d-shtaeou" , + "d-nthiouh" , + "i-vhbjkhnth" , + { + "nifty" : 87 + } , + { + "field" : "yes" , + "morefield" : false + } + ]""") + + + d1 = json.dumps(h) + d2 = json.dumps(h, indent=2, sort_keys=True, separators=(' ,', ' : ')) + + h1 = json.loads(d1) + h2 = json.loads(d2) + + self.assertEquals(h1, h) + self.assertEquals(h2, h) + self.assertEquals(d2, expect) diff --git a/resources/lib/simplejson/tests/test_unicode.py b/resources/lib/simplejson/tests/test_unicode.py new file mode 100644 index 0000000..6f4384a --- /dev/null +++ b/resources/lib/simplejson/tests/test_unicode.py @@ -0,0 +1,64 @@ +from unittest import TestCase + +import simplejson as json + +class TestUnicode(TestCase): + def test_encoding1(self): + encoder = json.JSONEncoder(encoding='utf-8') + u = u'\N{GREEK SMALL LETTER ALPHA}\N{GREEK CAPITAL LETTER OMEGA}' + s = u.encode('utf-8') + ju = encoder.encode(u) + js = encoder.encode(s) + self.assertEquals(ju, js) + + def test_encoding2(self): + u = u'\N{GREEK SMALL LETTER ALPHA}\N{GREEK CAPITAL LETTER OMEGA}' + s = u.encode('utf-8') + ju = json.dumps(u, encoding='utf-8') + js = json.dumps(s, encoding='utf-8') + self.assertEquals(ju, js) + + def test_encoding3(self): + u = u'\N{GREEK SMALL LETTER ALPHA}\N{GREEK CAPITAL LETTER OMEGA}' + j = json.dumps(u) + self.assertEquals(j, '"\\u03b1\\u03a9"') + + def test_encoding4(self): + u = u'\N{GREEK SMALL LETTER ALPHA}\N{GREEK CAPITAL LETTER OMEGA}' + j = json.dumps([u]) + self.assertEquals(j, '["\\u03b1\\u03a9"]') + + def test_encoding5(self): + u = u'\N{GREEK SMALL LETTER ALPHA}\N{GREEK CAPITAL LETTER OMEGA}' + j = json.dumps(u, ensure_ascii=False) + self.assertEquals(j, u'"%s"' % (u,)) + + def test_encoding6(self): + u = u'\N{GREEK SMALL LETTER ALPHA}\N{GREEK CAPITAL LETTER OMEGA}' + j = json.dumps([u], ensure_ascii=False) + self.assertEquals(j, u'["%s"]' % (u,)) + + def test_big_unicode_encode(self): + u = u'\U0001d120' + self.assertEquals(json.dumps(u), '"\\ud834\\udd20"') + self.assertEquals(json.dumps(u, ensure_ascii=False), u'"\U0001d120"') + + def test_big_unicode_decode(self): + u = u'z\U0001d120x' + self.assertEquals(json.loads('"' + u + '"'), u) + self.assertEquals(json.loads('"z\\ud834\\udd20x"'), u) + + def test_unicode_decode(self): + for i in range(0, 0xd7ff): + u = unichr(i) + s = '"\\u%04x"' % (i,) + self.assertEquals(json.loads(s), u) + + def test_default_encoding(self): + self.assertEquals(json.loads(u'{"a": "\xe9"}'.encode('utf-8')), + {'a': u'\xe9'}) + + def test_unicode_preservation(self): + self.assertEquals(type(json.loads(u'""')), unicode) + self.assertEquals(type(json.loads(u'"a"')), unicode) + self.assertEquals(type(json.loads(u'["a"]')[0]), unicode) \ No newline at end of file diff --git a/resources/lib/simplejson/tool.py b/resources/lib/simplejson/tool.py new file mode 100644 index 0000000..9044331 --- /dev/null +++ b/resources/lib/simplejson/tool.py @@ -0,0 +1,37 @@ +r"""Command-line tool to validate and pretty-print JSON + +Usage:: + + $ echo '{"json":"obj"}' | python -m simplejson.tool + { + "json": "obj" + } + $ echo '{ 1.2:3.4}' | python -m simplejson.tool + Expecting property name: line 1 column 2 (char 2) + +""" +import sys +import simplejson + +def main(): + if len(sys.argv) == 1: + infile = sys.stdin + outfile = sys.stdout + elif len(sys.argv) == 2: + infile = open(sys.argv[1], 'rb') + outfile = sys.stdout + elif len(sys.argv) == 3: + infile = open(sys.argv[1], 'rb') + outfile = open(sys.argv[2], 'wb') + else: + raise SystemExit(sys.argv[0] + " [infile [outfile]]") + try: + obj = simplejson.load(infile) + except ValueError, e: + raise SystemExit(e) + simplejson.dump(obj, outfile, sort_keys=True, indent=4) + outfile.write('\n') + + +if __name__ == '__main__': + main() diff --git a/resources/settings.xml b/resources/settings.xml new file mode 100644 index 0000000..825d507 --- /dev/null +++ b/resources/settings.xml @@ -0,0 +1,7 @@ + + + + + + + -- 2.20.1