mirror of
https://github.com/wakatime/sublime-wakatime.git
synced 2023-08-10 21:13:02 +03:00
Compare commits
46 Commits
Author | SHA1 | Date | |
---|---|---|---|
9b5c59e677 | |||
71ce25a326 | |||
f2f14207f5 | |||
ac2ec0e73c | |||
040a76b93c | |||
dab0621b97 | |||
675f9ecd69 | |||
a6f92b9c74 | |||
bfcc242d7e | |||
762027644f | |||
3c4ceb95fa | |||
d6d8bceca0 | |||
acaad2dc83 | |||
23c5801080 | |||
05a3bfbb53 | |||
8faaa3b0e3 | |||
4bcddf2a98 | |||
b51ae5c2c4 | |||
5cd0061653 | |||
651c84325e | |||
89368529cb | |||
f1f408284b | |||
7053932731 | |||
b6c4956521 | |||
68a2557884 | |||
c7ee7258fb | |||
aaff2503fb | |||
00a1193bd3 | |||
2371daac1b | |||
4395db2b2d | |||
fc8c61fa3f | |||
aa30110343 | |||
b671856341 | |||
b801759cdf | |||
919064200b | |||
911b5656d7 | |||
48bbab33b4 | |||
3b2aafe004 | |||
aa0b2d6d70 | |||
1a6f588d94 | |||
373ebf933f | |||
7fb47228f9 | |||
4fca5e1c06 | |||
cb2d126c47 | |||
17404bf848 | |||
510eea0a8b |
112
HISTORY.rst
112
HISTORY.rst
@ -3,6 +3,118 @@ History
|
||||
-------
|
||||
|
||||
|
||||
4.0.5 (2015-05-15)
|
||||
++++++++++++++++++
|
||||
|
||||
- correctly display caller and lineno in log file when debug is true
|
||||
- project passed with --project argument will always be used
|
||||
- new --alternate-project argument
|
||||
- upgrade wakatime cli to v4.0.12
|
||||
|
||||
|
||||
4.0.4 (2015-05-12)
|
||||
++++++++++++++++++
|
||||
|
||||
- reuse SSL connection over multiple processes for improved performance
|
||||
- upgrade wakatime cli to v4.0.11
|
||||
|
||||
|
||||
4.0.3 (2015-05-06)
|
||||
++++++++++++++++++
|
||||
|
||||
- send cursorpos to wakatime cli
|
||||
- upgrade wakatime cli to v4.0.10
|
||||
|
||||
|
||||
4.0.2 (2015-05-06)
|
||||
++++++++++++++++++
|
||||
|
||||
- only send heartbeats for the currently active buffer
|
||||
|
||||
|
||||
4.0.1 (2015-05-06)
|
||||
++++++++++++++++++
|
||||
|
||||
- ignore git temporary files
|
||||
- don't send two write heartbeats within 2 seconds of eachother
|
||||
|
||||
|
||||
4.0.0 (2015-04-12)
|
||||
++++++++++++++++++
|
||||
|
||||
- listen for selection modified instead of buffer activated for better performance
|
||||
|
||||
|
||||
3.0.19 (2015-04-07)
|
||||
+++++++++++++++++++
|
||||
|
||||
- fix bug in project detection when folder not found
|
||||
|
||||
|
||||
3.0.18 (2015-04-04)
|
||||
+++++++++++++++++++
|
||||
|
||||
- upgrade wakatime cli to v4.0.8
|
||||
- added api_url config option to .wakatime.cfg file
|
||||
|
||||
|
||||
3.0.17 (2015-04-02)
|
||||
+++++++++++++++++++
|
||||
|
||||
- use open folder as current project when not using revision control
|
||||
|
||||
|
||||
3.0.16 (2015-04-02)
|
||||
+++++++++++++++++++
|
||||
|
||||
- copy list when obfuscating api key so original command is not modified
|
||||
|
||||
|
||||
3.0.15 (2015-04-01)
|
||||
+++++++++++++++++++
|
||||
|
||||
- obfuscate api key when logging to Sublime Text Console in debug mode
|
||||
|
||||
|
||||
3.0.14 (2015-03-31)
|
||||
+++++++++++++++++++
|
||||
|
||||
- always use external python binary because ST builtin python does not support checking SSL certs
|
||||
- upgrade wakatime cli to v4.0.6
|
||||
|
||||
|
||||
3.0.13 (2015-03-23)
|
||||
+++++++++++++++++++
|
||||
|
||||
- correctly check for SSL support in ST built-in python
|
||||
- fix status bar message
|
||||
|
||||
|
||||
3.0.12 (2015-03-23)
|
||||
+++++++++++++++++++
|
||||
|
||||
- always use unicode function from compat module when encoding log messages
|
||||
|
||||
|
||||
3.0.11 (2015-03-23)
|
||||
+++++++++++++++++++
|
||||
|
||||
- upgrade simplejson package to v3.6.5
|
||||
|
||||
|
||||
3.0.10 (2015-03-22)
|
||||
+++++++++++++++++++
|
||||
|
||||
- ability to disable status bar message from WakaTime.sublime-settings file
|
||||
|
||||
|
||||
3.0.9 (2015-03-20)
|
||||
++++++++++++++++++
|
||||
|
||||
- status bar message showing when WakaTime plugin is enabled
|
||||
- moved some logic into thread to help prevent slow plugin warning message
|
||||
|
||||
|
||||
3.0.8 (2015-03-09)
|
||||
++++++++++++++++++
|
||||
|
||||
|
@ -18,7 +18,7 @@ Heads Up! For Sublime Text 2 on Windows & Linux, WakaTime depends on [Python](ht
|
||||
|
||||
c) Type `wakatime`, then press `enter` with the `WakaTime` plugin selected.
|
||||
|
||||
3. Enter your [api key](https://wakatime.com/settings#apikey) from https://wakatime.com/settings#apikey, then press `enter`.
|
||||
3. Enter your [api key](https://wakatime.com/settings#apikey), then press `enter`.
|
||||
|
||||
4. Use Sublime and your time will be tracked for you automatically.
|
||||
|
||||
|
213
WakaTime.py
213
WakaTime.py
@ -6,7 +6,9 @@ License: BSD, see LICENSE for more details.
|
||||
Website: https://wakatime.com/
|
||||
==========================================================="""
|
||||
|
||||
__version__ = '3.0.8'
|
||||
|
||||
__version__ = '4.0.5'
|
||||
|
||||
|
||||
import sublime
|
||||
import sublime_plugin
|
||||
@ -18,40 +20,32 @@ import sys
|
||||
import time
|
||||
import threading
|
||||
import webbrowser
|
||||
from os.path import expanduser, dirname, basename, realpath, join
|
||||
from datetime import datetime
|
||||
from subprocess import Popen
|
||||
|
||||
|
||||
# globals
|
||||
ACTION_FREQUENCY = 2
|
||||
HEARTBEAT_FREQUENCY = 2
|
||||
ST_VERSION = int(sublime.version())
|
||||
PLUGIN_DIR = dirname(realpath(__file__))
|
||||
API_CLIENT = '%s/packages/wakatime/cli.py' % PLUGIN_DIR
|
||||
PLUGIN_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
API_CLIENT = os.path.join(PLUGIN_DIR, 'packages', 'wakatime', 'cli.py')
|
||||
SETTINGS_FILE = 'WakaTime.sublime-settings'
|
||||
SETTINGS = {}
|
||||
LAST_ACTION = {
|
||||
LAST_HEARTBEAT = {
|
||||
'time': 0,
|
||||
'file': None,
|
||||
'is_write': False,
|
||||
}
|
||||
HAS_SSL = False
|
||||
LOCK = threading.RLock()
|
||||
PYTHON_LOCATION = None
|
||||
|
||||
|
||||
# add wakatime package to path
|
||||
sys.path.insert(0, join(PLUGIN_DIR, 'packages'))
|
||||
|
||||
# check if we have SSL support
|
||||
sys.path.insert(0, os.path.join(PLUGIN_DIR, 'packages'))
|
||||
try:
|
||||
import ssl
|
||||
import socket
|
||||
assert ssl
|
||||
assert socket.ssl
|
||||
HAS_SSL = True
|
||||
except (ImportError, AttributeError):
|
||||
from subprocess import Popen
|
||||
|
||||
if HAS_SSL:
|
||||
# import wakatime package so we can use built-in python
|
||||
import wakatime
|
||||
from wakatime.base import parseConfigFile
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def createConfigFile():
|
||||
@ -79,7 +73,6 @@ def prompt_api_key():
|
||||
|
||||
default_key = ''
|
||||
try:
|
||||
from wakatime.base import parseConfigFile
|
||||
configs = parseConfigFile()
|
||||
if configs is not None:
|
||||
if configs.has_option('settings', 'api_key'):
|
||||
@ -121,7 +114,7 @@ def python_binary():
|
||||
except:
|
||||
pass
|
||||
for path in glob.iglob('/python*'):
|
||||
path = realpath(join(path, 'pythonw'))
|
||||
path = os.path.realpath(os.path.join(path, 'pythonw'))
|
||||
try:
|
||||
Popen([path, '--version'])
|
||||
PYTHON_LOCATION = path
|
||||
@ -131,44 +124,80 @@ def python_binary():
|
||||
return None
|
||||
|
||||
|
||||
def enough_time_passed(now, last_time):
|
||||
if now - last_time > ACTION_FREQUENCY * 60:
|
||||
def obfuscate_apikey(command_list):
|
||||
cmd = list(command_list)
|
||||
apikey_index = None
|
||||
for num in range(len(cmd)):
|
||||
if cmd[num] == '--key':
|
||||
apikey_index = num + 1
|
||||
break
|
||||
if apikey_index is not None and apikey_index < len(cmd):
|
||||
cmd[apikey_index] = '********-****-****-****-********' + cmd[apikey_index][-4:]
|
||||
return cmd
|
||||
|
||||
|
||||
def enough_time_passed(now, last_heartbeat, is_write):
|
||||
if now - last_heartbeat['time'] > HEARTBEAT_FREQUENCY * 60:
|
||||
return True
|
||||
if is_write and now - last_heartbeat['time'] > 2:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def find_project_name_from_folders(folders):
|
||||
try:
|
||||
def find_folder_containing_file(folders, current_file):
|
||||
"""Returns absolute path to folder containing the file.
|
||||
"""
|
||||
|
||||
parent_folder = None
|
||||
|
||||
current_folder = current_file
|
||||
while True:
|
||||
for folder in folders:
|
||||
for file_name in os.listdir(folder):
|
||||
if file_name.endswith('.sublime-project'):
|
||||
return file_name.replace('.sublime-project', '', 1)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
if os.path.realpath(os.path.dirname(current_folder)) == os.path.realpath(folder):
|
||||
parent_folder = folder
|
||||
break
|
||||
if parent_folder is not None:
|
||||
break
|
||||
if not current_folder or os.path.dirname(current_folder) == current_folder:
|
||||
break
|
||||
current_folder = os.path.dirname(current_folder)
|
||||
|
||||
return parent_folder
|
||||
|
||||
|
||||
def handle_action(view, is_write=False):
|
||||
global LOCK, LAST_ACTION
|
||||
with LOCK:
|
||||
def find_project_from_folders(folders, current_file):
|
||||
"""Find project name from open folders.
|
||||
"""
|
||||
|
||||
folder = find_folder_containing_file(folders, current_file)
|
||||
return os.path.basename(folder) if folder else None
|
||||
|
||||
|
||||
def is_view_active(view):
|
||||
if view:
|
||||
active_window = sublime.active_window()
|
||||
if active_window:
|
||||
active_view = active_window.active_view()
|
||||
if active_view:
|
||||
return active_view.buffer_id() == view.buffer_id()
|
||||
return False
|
||||
|
||||
|
||||
def handle_heartbeat(view, is_write=False):
|
||||
window = view.window()
|
||||
if window is not None:
|
||||
target_file = view.file_name()
|
||||
if target_file:
|
||||
project = view.window().project_file_name() if hasattr(view.window(), 'project_file_name') else None
|
||||
if project:
|
||||
project = basename(project).replace('.sublime-project', '', 1)
|
||||
thread = SendActionThread(target_file, is_write=is_write, project=project, folders=view.window().folders())
|
||||
thread.start()
|
||||
LAST_ACTION = {
|
||||
'file': target_file,
|
||||
'time': time.time(),
|
||||
'is_write': is_write,
|
||||
}
|
||||
project = window.project_data() if hasattr(window, 'project_data') else None
|
||||
folders = window.folders()
|
||||
thread = SendHeartbeatThread(target_file, view, is_write=is_write, project=project, folders=folders)
|
||||
thread.start()
|
||||
|
||||
|
||||
class SendActionThread(threading.Thread):
|
||||
class SendHeartbeatThread(threading.Thread):
|
||||
|
||||
def __init__(self, target_file, is_write=False, project=None, folders=None, force=False):
|
||||
def __init__(self, target_file, view, is_write=False, project=None, folders=None, force=False):
|
||||
threading.Thread.__init__(self)
|
||||
self.lock = LOCK
|
||||
self.target_file = target_file
|
||||
self.is_write = is_write
|
||||
self.project = project
|
||||
@ -177,15 +206,18 @@ class SendActionThread(threading.Thread):
|
||||
self.debug = SETTINGS.get('debug')
|
||||
self.api_key = SETTINGS.get('api_key', '')
|
||||
self.ignore = SETTINGS.get('ignore', [])
|
||||
self.last_action = LAST_ACTION
|
||||
self.last_heartbeat = LAST_HEARTBEAT.copy()
|
||||
self.cursorpos = view.sel()[0].begin() if view.sel() else None
|
||||
self.view = view
|
||||
|
||||
def run(self):
|
||||
if self.target_file:
|
||||
self.timestamp = time.time()
|
||||
if self.force or (self.is_write and not self.last_action['is_write']) or self.target_file != self.last_action['file'] or enough_time_passed(self.timestamp, self.last_action['time']):
|
||||
self.send()
|
||||
with self.lock:
|
||||
if self.target_file:
|
||||
self.timestamp = time.time()
|
||||
if self.force or self.target_file != self.last_heartbeat['file'] or enough_time_passed(self.timestamp, self.last_heartbeat, self.is_write):
|
||||
self.send_heartbeat()
|
||||
|
||||
def send(self):
|
||||
def send_heartbeat(self):
|
||||
if not self.api_key:
|
||||
print('[WakaTime] Error: missing api key.')
|
||||
return
|
||||
@ -199,46 +231,55 @@ class SendActionThread(threading.Thread):
|
||||
]
|
||||
if self.is_write:
|
||||
cmd.append('--write')
|
||||
if self.project:
|
||||
cmd.extend(['--project', self.project])
|
||||
if self.project and self.project.get('name'):
|
||||
cmd.extend(['--alternate-project', self.project.get('name')])
|
||||
elif self.folders:
|
||||
project_name = find_project_name_from_folders(self.folders)
|
||||
project_name = find_project_from_folders(self.folders, self.target_file)
|
||||
if project_name:
|
||||
cmd.extend(['--project', project_name])
|
||||
cmd.extend(['--alternate-project', project_name])
|
||||
if self.cursorpos is not None:
|
||||
cmd.extend(['--cursorpos', '{0}'.format(self.cursorpos)])
|
||||
for pattern in self.ignore:
|
||||
cmd.extend(['--ignore', pattern])
|
||||
if self.debug:
|
||||
cmd.append('--verbose')
|
||||
if HAS_SSL:
|
||||
if python_binary():
|
||||
cmd.insert(0, python_binary())
|
||||
if self.debug:
|
||||
print('[WakaTime] %s' % ' '.join(cmd))
|
||||
code = wakatime.main(cmd)
|
||||
if code != 0:
|
||||
print('[WakaTime] Error: Response code %d from wakatime package.' % code)
|
||||
else:
|
||||
python = python_binary()
|
||||
if python:
|
||||
cmd.insert(0, python)
|
||||
if self.debug:
|
||||
print('[WakaTime] %s %s' % (python, ' '.join(cmd)))
|
||||
if platform.system() == 'Windows':
|
||||
Popen(cmd, shell=False)
|
||||
else:
|
||||
with open(join(expanduser('~'), '.wakatime.log'), 'a') as stderr:
|
||||
Popen(cmd, stderr=stderr)
|
||||
print('[WakaTime] %s' % ' '.join(obfuscate_apikey(cmd)))
|
||||
if platform.system() == 'Windows':
|
||||
Popen(cmd, shell=False)
|
||||
else:
|
||||
print('[WakaTime] Error: Unable to find python binary.')
|
||||
with open(os.path.join(os.path.expanduser('~'), '.wakatime.log'), 'a') as stderr:
|
||||
Popen(cmd, stderr=stderr)
|
||||
self.sent()
|
||||
else:
|
||||
print('[WakaTime] Error: Unable to find python binary.')
|
||||
|
||||
def sent(self):
|
||||
sublime.set_timeout(self.set_status_bar, 0)
|
||||
sublime.set_timeout(self.set_last_heartbeat, 0)
|
||||
|
||||
def set_status_bar(self):
|
||||
if SETTINGS.get('status_bar_message'):
|
||||
self.view.set_status('wakatime', 'WakaTime active {0}'.format(datetime.now().strftime('%I:%M %p')))
|
||||
|
||||
def set_last_heartbeat(self):
|
||||
global LAST_HEARTBEAT
|
||||
LAST_HEARTBEAT = {
|
||||
'file': self.target_file,
|
||||
'time': self.timestamp,
|
||||
'is_write': self.is_write,
|
||||
}
|
||||
|
||||
|
||||
def plugin_loaded():
|
||||
global SETTINGS
|
||||
print('[WakaTime] Initializing WakaTime plugin v%s' % __version__)
|
||||
|
||||
if not HAS_SSL:
|
||||
python = python_binary()
|
||||
if not python:
|
||||
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
|
||||
return
|
||||
if not python_binary():
|
||||
sublime.error_message("Unable to find Python binary!\nWakaTime needs Python to work correctly.\n\nGo to https://www.python.org/downloads")
|
||||
return
|
||||
|
||||
SETTINGS = sublime.load_settings(SETTINGS_FILE)
|
||||
after_loaded()
|
||||
@ -257,13 +298,15 @@ if ST_VERSION < 3000:
|
||||
class WakatimeListener(sublime_plugin.EventListener):
|
||||
|
||||
def on_post_save(self, view):
|
||||
handle_action(view, is_write=True)
|
||||
handle_heartbeat(view, is_write=True)
|
||||
|
||||
def on_activated(self, view):
|
||||
handle_action(view)
|
||||
def on_selection_modified(self, view):
|
||||
if is_view_active(view):
|
||||
handle_heartbeat(view)
|
||||
|
||||
def on_modified(self, view):
|
||||
handle_action(view)
|
||||
if is_view_active(view):
|
||||
handle_heartbeat(view)
|
||||
|
||||
|
||||
class WakatimeDashboardCommand(sublime_plugin.ApplicationCommand):
|
||||
|
@ -9,8 +9,12 @@
|
||||
|
||||
// Ignore files; Files (including absolute paths) that match one of these
|
||||
// POSIX regular expressions will not be logged.
|
||||
"ignore": ["^/tmp/", "^/etc/", "^/var/"],
|
||||
"ignore": ["^/tmp/", "^/etc/", "^/var/", "COMMIT_EDITMSG$", "PULLREQ_EDITMSG$", "MERGE_MSG$", "TAG_EDITMSG$"],
|
||||
|
||||
// Debug mode. Set to true for verbose logging. Defaults to false.
|
||||
"debug": false
|
||||
"debug": false,
|
||||
|
||||
// Status bar message. Set to false to hide status bar message.
|
||||
// Defaults to true.
|
||||
"status_bar_message": true
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
__title__ = 'wakatime'
|
||||
__description__ = 'Common interface to the WakaTime api.'
|
||||
__url__ = 'https://github.com/wakatime/wakatime'
|
||||
__version_info__ = ('4', '0', '4')
|
||||
__version_info__ = ('4', '0', '12')
|
||||
__version__ = '.'.join(__version_info__)
|
||||
__author__ = 'Alan Hamlett'
|
||||
__author_email__ = 'alan@wakatime.com'
|
||||
|
@ -24,19 +24,19 @@ try:
|
||||
except ImportError:
|
||||
import configparser
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages'))
|
||||
|
||||
from .__about__ import __version__
|
||||
from .compat import u, open, is_py3
|
||||
from .logger import setup_logging
|
||||
from .offlinequeue import Queue
|
||||
from .log import setup_logging
|
||||
from .project import find_project
|
||||
from .stats import get_file_stats
|
||||
from .packages import argparse
|
||||
from .packages import simplejson as json
|
||||
from .packages import requests
|
||||
from .packages.requests.exceptions import RequestException
|
||||
from .project import find_project
|
||||
from .session_cache import SessionCache
|
||||
from .stats import get_file_stats
|
||||
try:
|
||||
from .packages import tzlocal
|
||||
except:
|
||||
@ -147,14 +147,20 @@ def parseArguments(argv):
|
||||
type=float,
|
||||
help='optional floating-point unix epoch timestamp; '+
|
||||
'uses current time by default')
|
||||
parser.add_argument('--lineno', dest='lineno',
|
||||
help='optional line number; current line being edited')
|
||||
parser.add_argument('--cursorpos', dest='cursorpos',
|
||||
help='optional cursor position in the current file')
|
||||
parser.add_argument('--notfile', dest='notfile', action='store_true',
|
||||
help='when set, will accept any value for the file. for example, '+
|
||||
'a domain name or other item you want to log time towards.')
|
||||
parser.add_argument('--proxy', dest='proxy',
|
||||
help='optional https proxy url; for example: '+
|
||||
'https://user:pass@localhost:8080')
|
||||
parser.add_argument('--project', dest='project_name',
|
||||
help='optional project name; will auto-discover by default')
|
||||
parser.add_argument('--project', dest='project',
|
||||
help='optional project name')
|
||||
parser.add_argument('--alternate-project', dest='alternate_project',
|
||||
help='optional alternate project name; auto-discovered project takes priority')
|
||||
parser.add_argument('--disableoffline', dest='offline',
|
||||
action='store_false',
|
||||
help='disables offline time logging instead of queuing logged time')
|
||||
@ -172,6 +178,8 @@ def parseArguments(argv):
|
||||
help=argparse.SUPPRESS)
|
||||
parser.add_argument('--logfile', dest='logfile',
|
||||
help='defaults to ~/.wakatime.log')
|
||||
parser.add_argument('--apiurl', dest='api_url',
|
||||
help='heartbeats api url; for debugging with a local server')
|
||||
parser.add_argument('--config', dest='config',
|
||||
help='defaults to ~/.wakatime.conf')
|
||||
parser.add_argument('--verbose', dest='verbose', action='store_true',
|
||||
@ -238,6 +246,8 @@ def parseArguments(argv):
|
||||
args.verbose = configs.getboolean('settings', 'debug')
|
||||
if not args.logfile and configs.has_option('settings', 'logfile'):
|
||||
args.logfile = configs.get('settings', 'logfile')
|
||||
if not args.api_url and configs.has_option('settings', 'api_url'):
|
||||
args.api_url = configs.get('settings', 'api_url')
|
||||
|
||||
return args, configs
|
||||
|
||||
@ -294,10 +304,14 @@ def get_user_agent(plugin):
|
||||
|
||||
|
||||
def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=None,
|
||||
timestamp=None, isWrite=None, plugin=None, offline=None,
|
||||
hidefilenames=None, notfile=False, proxy=None, **kwargs):
|
||||
url = 'https://wakatime.com/api/v1/heartbeats'
|
||||
log.debug('Sending heartbeat to api at %s' % url)
|
||||
timestamp=None, isWrite=None, plugin=None, offline=None, notfile=False,
|
||||
hidefilenames=None, proxy=None, api_url=None, **kwargs):
|
||||
"""Sends heartbeat as POST request to WakaTime api server.
|
||||
"""
|
||||
|
||||
if not api_url:
|
||||
api_url = 'https://wakatime.com/api/v1/heartbeats'
|
||||
log.debug('Sending heartbeat to api at %s' % api_url)
|
||||
data = {
|
||||
'time': timestamp,
|
||||
'file': targetFile,
|
||||
@ -314,6 +328,10 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non
|
||||
data['language'] = stats['language']
|
||||
if stats.get('dependencies'):
|
||||
data['dependencies'] = stats['dependencies']
|
||||
if stats.get('lineno'):
|
||||
data['lineno'] = stats['lineno']
|
||||
if stats.get('cursorpos'):
|
||||
data['cursorpos'] = stats['cursorpos']
|
||||
if isWrite:
|
||||
data['is_write'] = isWrite
|
||||
if project:
|
||||
@ -344,10 +362,13 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non
|
||||
if tz:
|
||||
headers['TimeZone'] = u(tz.zone)
|
||||
|
||||
session_cache = SessionCache()
|
||||
session = session_cache.get()
|
||||
|
||||
# log time to api
|
||||
response = None
|
||||
try:
|
||||
response = requests.post(url, data=request_body, headers=headers,
|
||||
response = session.post(api_url, data=request_body, headers=headers,
|
||||
proxies=proxies)
|
||||
except RequestException:
|
||||
exception_data = {
|
||||
@ -369,6 +390,7 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non
|
||||
log.debug({
|
||||
'response_code': response_code,
|
||||
})
|
||||
session_cache.save(session)
|
||||
return True
|
||||
if offline:
|
||||
if response_code != 400:
|
||||
@ -394,6 +416,7 @@ def send_heartbeat(project=None, branch=None, stats={}, key=None, targetFile=Non
|
||||
'response_code': response_code,
|
||||
'response_content': response_content,
|
||||
})
|
||||
session_cache.delete()
|
||||
return False
|
||||
|
||||
|
||||
@ -416,16 +439,20 @@ def main(argv=None):
|
||||
|
||||
if os.path.isfile(args.targetFile) or args.notfile:
|
||||
|
||||
stats = get_file_stats(args.targetFile, notfile=args.notfile)
|
||||
stats = get_file_stats(args.targetFile, notfile=args.notfile,
|
||||
lineno=args.lineno, cursorpos=args.cursorpos)
|
||||
|
||||
project = None
|
||||
if not args.notfile:
|
||||
project = find_project(args.targetFile, configs=configs)
|
||||
branch = None
|
||||
project_name = args.project_name
|
||||
project_name = args.project
|
||||
if project:
|
||||
branch = project.branch()
|
||||
project_name = project.name()
|
||||
if not project_name:
|
||||
project_name = project.name()
|
||||
if not project_name:
|
||||
project_name = args.alternate_project
|
||||
|
||||
if send_heartbeat(
|
||||
project=project_name,
|
||||
@ -438,18 +465,21 @@ def main(argv=None):
|
||||
heartbeat = queue.pop()
|
||||
if heartbeat is None:
|
||||
break
|
||||
sent = send_heartbeat(project=heartbeat['project'],
|
||||
targetFile=heartbeat['file'],
|
||||
timestamp=heartbeat['time'],
|
||||
branch=heartbeat['branch'],
|
||||
stats=json.loads(heartbeat['stats']),
|
||||
key=args.key,
|
||||
isWrite=heartbeat['is_write'],
|
||||
plugin=heartbeat['plugin'],
|
||||
offline=args.offline,
|
||||
hidefilenames=args.hidefilenames,
|
||||
notfile=args.notfile,
|
||||
proxy=args.proxy)
|
||||
sent = send_heartbeat(
|
||||
project=heartbeat['project'],
|
||||
targetFile=heartbeat['file'],
|
||||
timestamp=heartbeat['time'],
|
||||
branch=heartbeat['branch'],
|
||||
stats=json.loads(heartbeat['stats']),
|
||||
key=args.key,
|
||||
isWrite=heartbeat['is_write'],
|
||||
plugin=heartbeat['plugin'],
|
||||
offline=args.offline,
|
||||
hidefilenames=args.hidefilenames,
|
||||
notfile=args.notfile,
|
||||
proxy=args.proxy,
|
||||
api_url=args.api_url,
|
||||
)
|
||||
if not sent:
|
||||
break
|
||||
return 0 # success
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wakatime.log
|
||||
~~~~~~~~~~~~
|
||||
wakatime.logger
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Provides the configured logger for writing JSON to the log file.
|
||||
|
||||
@ -37,28 +37,32 @@ class CustomEncoder(json.JSONEncoder):
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
|
||||
def setup(self, timestamp, isWrite, targetFile, version, plugin):
|
||||
def setup(self, timestamp, isWrite, targetFile, version, plugin, verbose):
|
||||
self.timestamp = timestamp
|
||||
self.isWrite = isWrite
|
||||
self.targetFile = targetFile
|
||||
self.version = version
|
||||
self.plugin = plugin
|
||||
self.verbose = verbose
|
||||
|
||||
def format(self, record):
|
||||
data = OrderedDict([
|
||||
('now', self.formatTime(record, self.datefmt)),
|
||||
('version', self.version),
|
||||
('plugin', self.plugin),
|
||||
('time', self.timestamp),
|
||||
('isWrite', self.isWrite),
|
||||
('file', self.targetFile),
|
||||
('level', record.levelname),
|
||||
('message', record.msg),
|
||||
])
|
||||
data['version'] = self.version
|
||||
data['plugin'] = self.plugin
|
||||
data['time'] = self.timestamp
|
||||
if self.verbose:
|
||||
data['caller'] = record.pathname
|
||||
data['lineno'] = record.lineno
|
||||
data['isWrite'] = self.isWrite
|
||||
data['file'] = self.targetFile
|
||||
if not self.isWrite:
|
||||
del data['isWrite']
|
||||
data['level'] = record.levelname
|
||||
data['message'] = record.msg
|
||||
if not self.plugin:
|
||||
del data['plugin']
|
||||
if not self.isWrite:
|
||||
del data['isWrite']
|
||||
return CustomEncoder().encode(data)
|
||||
|
||||
def formatException(self, exc_info):
|
||||
@ -73,6 +77,7 @@ def set_log_level(logger, args):
|
||||
|
||||
|
||||
def setup_logging(args, version):
|
||||
logging.captureWarnings(True)
|
||||
logger = logging.getLogger('WakaTime')
|
||||
set_log_level(logger, args)
|
||||
if len(logger.handlers) > 0:
|
||||
@ -83,6 +88,7 @@ def setup_logging(args, version):
|
||||
targetFile=args.targetFile,
|
||||
version=version,
|
||||
plugin=args.plugin,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
logger.handlers[0].setFormatter(formatter)
|
||||
return logger
|
||||
@ -97,7 +103,9 @@ def setup_logging(args, version):
|
||||
targetFile=args.targetFile,
|
||||
version=version,
|
||||
plugin=args.plugin,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logging.getLogger('py.warnings').addHandler(handler)
|
||||
return logger
|
@ -1,10 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wakatime.queue
|
||||
~~~~~~~~~~~~~~
|
||||
wakatime.offlinequeue
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Queue for offline time logging.
|
||||
http://wakatime.com
|
||||
Queue for saving heartbeats while offline.
|
||||
|
||||
:copyright: (c) 2014 Alan Hamlett.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
|
@ -42,7 +42,7 @@ is at <http://python-requests.org>.
|
||||
"""
|
||||
|
||||
__title__ = 'requests'
|
||||
__version__ = '2.5.3'
|
||||
__version__ = '2.6.0'
|
||||
__build__ = 0x020503
|
||||
__author__ = 'Kenneth Reitz'
|
||||
__license__ = 'Apache 2.0'
|
||||
|
@ -11,10 +11,10 @@ and maintain connections.
|
||||
import socket
|
||||
|
||||
from .models import Response
|
||||
from .packages.urllib3 import Retry
|
||||
from .packages.urllib3.poolmanager import PoolManager, proxy_from_url
|
||||
from .packages.urllib3.response import HTTPResponse
|
||||
from .packages.urllib3.util import Timeout as TimeoutSauce
|
||||
from .packages.urllib3.util.retry import Retry
|
||||
from .compat import urlparse, basestring
|
||||
from .utils import (DEFAULT_CA_BUNDLE_PATH, get_encoding_from_headers,
|
||||
prepend_scheme_if_needed, get_auth_from_url, urldefragauth)
|
||||
|
@ -16,7 +16,6 @@ from . import sessions
|
||||
|
||||
def request(method, url, **kwargs):
|
||||
"""Constructs and sends a :class:`Request <Request>`.
|
||||
Returns :class:`Response <Response>` object.
|
||||
|
||||
:param method: method for the new :class:`Request` object.
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
@ -37,6 +36,8 @@ def request(method, url, **kwargs):
|
||||
:param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided.
|
||||
:param stream: (optional) if ``False``, the response content will be immediately downloaded.
|
||||
:param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
|
||||
Usage::
|
||||
|
||||
@ -55,10 +56,12 @@ def request(method, url, **kwargs):
|
||||
|
||||
|
||||
def get(url, **kwargs):
|
||||
"""Sends a GET request. Returns :class:`Response` object.
|
||||
"""Sends a GET request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
kwargs.setdefault('allow_redirects', True)
|
||||
@ -66,10 +69,12 @@ def get(url, **kwargs):
|
||||
|
||||
|
||||
def options(url, **kwargs):
|
||||
"""Sends a OPTIONS request. Returns :class:`Response` object.
|
||||
"""Sends a OPTIONS request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
kwargs.setdefault('allow_redirects', True)
|
||||
@ -77,10 +82,12 @@ def options(url, **kwargs):
|
||||
|
||||
|
||||
def head(url, **kwargs):
|
||||
"""Sends a HEAD request. Returns :class:`Response` object.
|
||||
"""Sends a HEAD request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
kwargs.setdefault('allow_redirects', False)
|
||||
@ -88,44 +95,52 @@ def head(url, **kwargs):
|
||||
|
||||
|
||||
def post(url, data=None, json=None, **kwargs):
|
||||
"""Sends a POST request. Returns :class:`Response` object.
|
||||
"""Sends a POST request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param json: (optional) json data to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request('post', url, data=data, json=json, **kwargs)
|
||||
|
||||
|
||||
def put(url, data=None, **kwargs):
|
||||
"""Sends a PUT request. Returns :class:`Response` object.
|
||||
"""Sends a PUT request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request('put', url, data=data, **kwargs)
|
||||
|
||||
|
||||
def patch(url, data=None, **kwargs):
|
||||
"""Sends a PATCH request. Returns :class:`Response` object.
|
||||
"""Sends a PATCH request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request('patch', url, data=data, **kwargs)
|
||||
|
||||
|
||||
def delete(url, **kwargs):
|
||||
"""Sends a DELETE request. Returns :class:`Response` object.
|
||||
"""Sends a DELETE request.
|
||||
|
||||
:param url: URL for the new :class:`Request` object.
|
||||
:param \*\*kwargs: Optional arguments that ``request`` takes.
|
||||
:return: :class:`Response <Response>` object
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
return request('delete', url, **kwargs)
|
||||
|
@ -143,12 +143,13 @@ class RequestEncodingMixin(object):
|
||||
else:
|
||||
fn = guess_filename(v) or k
|
||||
fp = v
|
||||
if isinstance(fp, str):
|
||||
fp = StringIO(fp)
|
||||
if isinstance(fp, (bytes, bytearray)):
|
||||
fp = BytesIO(fp)
|
||||
|
||||
rf = RequestField(name=k, data=fp.read(),
|
||||
if isinstance(fp, (str, bytes, bytearray)):
|
||||
fdata = fp
|
||||
else:
|
||||
fdata = fp.read()
|
||||
|
||||
rf = RequestField(name=k, data=fdata,
|
||||
filename=fn, headers=fh)
|
||||
rf.make_multipart(content_type=ft)
|
||||
new_fields.append(rf)
|
||||
@ -572,7 +573,11 @@ class Response(object):
|
||||
self.cookies = cookiejar_from_dict({})
|
||||
|
||||
#: The amount of time elapsed between sending the request
|
||||
#: and the arrival of the response (as a timedelta)
|
||||
#: and the arrival of the response (as a timedelta).
|
||||
#: This property specifically measures the time taken between sending
|
||||
#: the first byte of the request and finishing parsing the headers. It
|
||||
#: is therefore unaffected by consuming the response content or the
|
||||
#: value of the ``stream`` keyword argument.
|
||||
self.elapsed = datetime.timedelta(0)
|
||||
|
||||
#: The :class:`PreparedRequest <PreparedRequest>` object to which this
|
||||
|
@ -4,7 +4,7 @@ urllib3 - Thread-safe connection pooling and re-using.
|
||||
|
||||
__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)'
|
||||
__license__ = 'MIT'
|
||||
__version__ = 'dev'
|
||||
__version__ = '1.10.2'
|
||||
|
||||
|
||||
from .connectionpool import (
|
||||
|
@ -20,8 +20,6 @@ from .packages.six import iterkeys, itervalues, PY3
|
||||
__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict']
|
||||
|
||||
|
||||
MULTIPLE_HEADERS_ALLOWED = frozenset(['cookie', 'set-cookie', 'set-cookie2'])
|
||||
|
||||
_Null = object()
|
||||
|
||||
|
||||
@ -143,7 +141,10 @@ class HTTPHeaderDict(dict):
|
||||
def __init__(self, headers=None, **kwargs):
|
||||
dict.__init__(self)
|
||||
if headers is not None:
|
||||
self.extend(headers)
|
||||
if isinstance(headers, HTTPHeaderDict):
|
||||
self._copy_from(headers)
|
||||
else:
|
||||
self.extend(headers)
|
||||
if kwargs:
|
||||
self.extend(kwargs)
|
||||
|
||||
@ -223,11 +224,8 @@ class HTTPHeaderDict(dict):
|
||||
vals.append(val)
|
||||
else:
|
||||
# vals should be a tuple then, i.e. only one item so far
|
||||
if key_lower in MULTIPLE_HEADERS_ALLOWED:
|
||||
# Need to convert the tuple to list for further extension
|
||||
_dict_setitem(self, key_lower, [vals[0], vals[1], val])
|
||||
else:
|
||||
_dict_setitem(self, key_lower, new_vals)
|
||||
# Need to convert the tuple to list for further extension
|
||||
_dict_setitem(self, key_lower, [vals[0], vals[1], val])
|
||||
|
||||
def extend(*args, **kwargs):
|
||||
"""Generic import function for any type of header-like object.
|
||||
@ -276,14 +274,17 @@ class HTTPHeaderDict(dict):
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (type(self).__name__, dict(self.itermerged()))
|
||||
|
||||
def copy(self):
|
||||
clone = type(self)()
|
||||
for key in self:
|
||||
val = _dict_getitem(self, key)
|
||||
def _copy_from(self, other):
|
||||
for key in other:
|
||||
val = _dict_getitem(other, key)
|
||||
if isinstance(val, list):
|
||||
# Don't need to convert tuples
|
||||
val = list(val)
|
||||
_dict_setitem(clone, key, val)
|
||||
_dict_setitem(self, key, val)
|
||||
|
||||
def copy(self):
|
||||
clone = type(self)()
|
||||
clone._copy_from(self)
|
||||
return clone
|
||||
|
||||
def iteritems(self):
|
||||
|
@ -157,3 +157,8 @@ class InsecureRequestWarning(SecurityWarning):
|
||||
class SystemTimeWarning(SecurityWarning):
|
||||
"Warned when system time is suspected to be wrong"
|
||||
pass
|
||||
|
||||
|
||||
class InsecurePlatformWarning(SecurityWarning):
|
||||
"Warned when certain SSL configuration is not available on a platform."
|
||||
pass
|
||||
|
@ -1,7 +1,7 @@
|
||||
from binascii import hexlify, unhexlify
|
||||
from hashlib import md5, sha1, sha256
|
||||
|
||||
from ..exceptions import SSLError
|
||||
from ..exceptions import SSLError, InsecurePlatformWarning
|
||||
|
||||
|
||||
SSLContext = None
|
||||
@ -10,6 +10,7 @@ create_default_context = None
|
||||
|
||||
import errno
|
||||
import ssl
|
||||
import warnings
|
||||
|
||||
try: # Test for SSL features
|
||||
from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
|
||||
@ -69,6 +70,14 @@ except ImportError:
|
||||
self.ciphers = cipher_suite
|
||||
|
||||
def wrap_socket(self, socket, server_hostname=None):
|
||||
warnings.warn(
|
||||
'A true SSLContext object is not available. This prevents '
|
||||
'urllib3 from configuring SSL appropriately and may cause '
|
||||
'certain SSL connections to fail. For more information, see '
|
||||
'https://urllib3.readthedocs.org/en/latest/security.html'
|
||||
'#insecureplatformwarning.',
|
||||
InsecurePlatformWarning
|
||||
)
|
||||
kwargs = {
|
||||
'keyfile': self.keyfile,
|
||||
'certfile': self.certfile,
|
||||
|
@ -171,7 +171,10 @@ class SessionRedirectMixin(object):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
extract_cookies_to_jar(prepared_request._cookies, prepared_request, resp.raw)
|
||||
# Extract any cookies sent on the response to the cookiejar
|
||||
# in the new request. Because we've mutated our copied prepared
|
||||
# request, use the old one that we haven't yet touched.
|
||||
extract_cookies_to_jar(prepared_request._cookies, req, resp.raw)
|
||||
prepared_request._cookies.update(self.cookies)
|
||||
prepared_request.prepare_cookies(prepared_request._cookies)
|
||||
|
||||
|
@ -98,7 +98,7 @@ Using simplejson.tool from the shell to validate and pretty-print::
|
||||
Expecting property name: line 1 column 3 (char 2)
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
__version__ = '3.3.0'
|
||||
__version__ = '3.6.5'
|
||||
__all__ = [
|
||||
'dump', 'dumps', 'load', 'loads',
|
||||
'JSONDecoder', 'JSONDecodeError', 'JSONEncoder',
|
||||
@ -144,14 +144,15 @@ _default_encoder = JSONEncoder(
|
||||
item_sort_key=None,
|
||||
for_json=False,
|
||||
ignore_nan=False,
|
||||
int_as_string_bitcount=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, use_decimal=True,
|
||||
namedtuple_as_object=True, tuple_as_array=True,
|
||||
bigint_as_string=False, sort_keys=False, item_sort_key=None,
|
||||
for_json=False, ignore_nan=False, **kw):
|
||||
allow_nan=True, cls=None, indent=None, separators=None,
|
||||
encoding='utf-8', default=None, use_decimal=True,
|
||||
namedtuple_as_object=True, tuple_as_array=True,
|
||||
bigint_as_string=False, sort_keys=False, item_sort_key=None,
|
||||
for_json=False, ignore_nan=False, int_as_string_bitcount=None, **kw):
|
||||
"""Serialize ``obj`` as a JSON formatted stream to ``fp`` (a
|
||||
``.write()``-supporting file-like object).
|
||||
|
||||
@ -209,6 +210,10 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
|
||||
lossy operation that will not round-trip correctly and should be used
|
||||
sparingly.
|
||||
|
||||
If *int_as_string_bitcount* is a positive number (n), then int of size
|
||||
greater than or equal to 2**n or lower than or equal to -2**n will be
|
||||
encoded as strings.
|
||||
|
||||
If specified, *item_sort_key* is a callable used to sort the items in
|
||||
each dictionary. This is useful if you want to sort items other than
|
||||
in alphabetical order by key. This option takes precedence over
|
||||
@ -238,8 +243,11 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
|
||||
cls is None and indent is None and separators is None and
|
||||
encoding == 'utf-8' and default is None and use_decimal
|
||||
and namedtuple_as_object and tuple_as_array
|
||||
and not bigint_as_string and not item_sort_key
|
||||
and not for_json and not ignore_nan and not kw):
|
||||
and not bigint_as_string and not sort_keys
|
||||
and not item_sort_key and not for_json
|
||||
and not ignore_nan and int_as_string_bitcount is None
|
||||
and not kw
|
||||
):
|
||||
iterable = _default_encoder.iterencode(obj)
|
||||
else:
|
||||
if cls is None:
|
||||
@ -255,6 +263,7 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
|
||||
item_sort_key=item_sort_key,
|
||||
for_json=for_json,
|
||||
ignore_nan=ignore_nan,
|
||||
int_as_string_bitcount=int_as_string_bitcount,
|
||||
**kw).iterencode(obj)
|
||||
# could accelerate with writelines in some versions of Python, at
|
||||
# a debuggability cost
|
||||
@ -263,11 +272,11 @@ def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True,
|
||||
|
||||
|
||||
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, use_decimal=True,
|
||||
namedtuple_as_object=True, tuple_as_array=True,
|
||||
bigint_as_string=False, sort_keys=False, item_sort_key=None,
|
||||
for_json=False, ignore_nan=False, **kw):
|
||||
allow_nan=True, cls=None, indent=None, separators=None,
|
||||
encoding='utf-8', default=None, use_decimal=True,
|
||||
namedtuple_as_object=True, tuple_as_array=True,
|
||||
bigint_as_string=False, sort_keys=False, item_sort_key=None,
|
||||
for_json=False, ignore_nan=False, int_as_string_bitcount=None, **kw):
|
||||
"""Serialize ``obj`` to a JSON formatted ``str``.
|
||||
|
||||
If ``skipkeys`` is false then ``dict`` keys that are not basic types
|
||||
@ -319,6 +328,10 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
|
||||
or lower than -2**53 will be encoded as strings. This is to avoid the
|
||||
rounding that happens in Javascript otherwise.
|
||||
|
||||
If *int_as_string_bitcount* is a positive number (n), then int of size
|
||||
greater than or equal to 2**n or lower than or equal to -2**n will be
|
||||
encoded as strings.
|
||||
|
||||
If specified, *item_sort_key* is a callable used to sort the items in
|
||||
each dictionary. This is useful if you want to sort items other than
|
||||
in alphabetical order by key. This option takes precendence over
|
||||
@ -343,14 +356,17 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
|
||||
|
||||
"""
|
||||
# cached encoder
|
||||
if (not skipkeys and ensure_ascii and
|
||||
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 use_decimal
|
||||
and namedtuple_as_object and tuple_as_array
|
||||
and not bigint_as_string and not sort_keys
|
||||
and not item_sort_key and not for_json
|
||||
and not ignore_nan and not kw):
|
||||
and not ignore_nan and int_as_string_bitcount is None
|
||||
and not kw
|
||||
):
|
||||
return _default_encoder.encode(obj)
|
||||
if cls is None:
|
||||
cls = JSONEncoder
|
||||
@ -366,6 +382,7 @@ def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True,
|
||||
item_sort_key=item_sort_key,
|
||||
for_json=for_json,
|
||||
ignore_nan=ignore_nan,
|
||||
int_as_string_bitcount=int_as_string_bitcount,
|
||||
**kw).encode(obj)
|
||||
|
||||
|
||||
|
@ -168,7 +168,8 @@ typedef struct _PyEncoderObject {
|
||||
int use_decimal;
|
||||
int namedtuple_as_object;
|
||||
int tuple_as_array;
|
||||
int bigint_as_string;
|
||||
PyObject *max_long_size;
|
||||
PyObject *min_long_size;
|
||||
PyObject *item_sort_key;
|
||||
PyObject *item_sort_kw;
|
||||
int for_json;
|
||||
@ -187,6 +188,8 @@ static PyMemberDef encoder_members[] = {
|
||||
{"skipkeys", T_OBJECT, offsetof(PyEncoderObject, skipkeys_bool), READONLY, "skipkeys"},
|
||||
{"key_memo", T_OBJECT, offsetof(PyEncoderObject, key_memo), READONLY, "key_memo"},
|
||||
{"item_sort_key", T_OBJECT, offsetof(PyEncoderObject, item_sort_key), READONLY, "item_sort_key"},
|
||||
{"max_long_size", T_OBJECT, offsetof(PyEncoderObject, max_long_size), READONLY, "max_long_size"},
|
||||
{"min_long_size", T_OBJECT, offsetof(PyEncoderObject, min_long_size), READONLY, "min_long_size"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
@ -197,7 +200,7 @@ JSON_ParseEncoding(PyObject *encoding);
|
||||
static PyObject *
|
||||
JSON_UnicodeFromChar(JSON_UNICHR c);
|
||||
static PyObject *
|
||||
maybe_quote_bigint(PyObject *encoded, PyObject *obj);
|
||||
maybe_quote_bigint(PyEncoderObject* s, PyObject *encoded, PyObject *obj);
|
||||
static Py_ssize_t
|
||||
ascii_char_size(JSON_UNICHR c);
|
||||
static Py_ssize_t
|
||||
@ -384,35 +387,22 @@ JSON_UnicodeFromChar(JSON_UNICHR c)
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
maybe_quote_bigint(PyObject *encoded, PyObject *obj)
|
||||
maybe_quote_bigint(PyEncoderObject* s, PyObject *encoded, PyObject *obj)
|
||||
{
|
||||
static PyObject *big_long = NULL;
|
||||
static PyObject *small_long = NULL;
|
||||
if (big_long == NULL) {
|
||||
big_long = PyLong_FromLongLong(1LL << 53);
|
||||
if (big_long == NULL) {
|
||||
Py_DECREF(encoded);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
if (small_long == NULL) {
|
||||
small_long = PyLong_FromLongLong(-1LL << 53);
|
||||
if (small_long == NULL) {
|
||||
Py_DECREF(encoded);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
if (PyObject_RichCompareBool(obj, big_long, Py_GE) ||
|
||||
PyObject_RichCompareBool(obj, small_long, Py_LE)) {
|
||||
if (s->max_long_size != Py_None && s->min_long_size != Py_None) {
|
||||
if (PyObject_RichCompareBool(obj, s->max_long_size, Py_GE) ||
|
||||
PyObject_RichCompareBool(obj, s->min_long_size, Py_LE)) {
|
||||
#if PY_MAJOR_VERSION >= 3
|
||||
PyObject* quoted = PyUnicode_FromFormat("\"%U\"", encoded);
|
||||
PyObject* quoted = PyUnicode_FromFormat("\"%U\"", encoded);
|
||||
#else
|
||||
PyObject* quoted = PyString_FromFormat("\"%s\"",
|
||||
PyString_AsString(encoded));
|
||||
PyObject* quoted = PyString_FromFormat("\"%s\"",
|
||||
PyString_AsString(encoded));
|
||||
#endif
|
||||
Py_DECREF(encoded);
|
||||
encoded = quoted;
|
||||
Py_DECREF(encoded);
|
||||
encoded = quoted;
|
||||
}
|
||||
}
|
||||
|
||||
return encoded;
|
||||
}
|
||||
|
||||
@ -1023,13 +1013,13 @@ scanstring_str(PyObject *pystr, Py_ssize_t end, char *encoding, int strict, Py_s
|
||||
/* Surrogate pair */
|
||||
if ((c & 0xfc00) == 0xd800) {
|
||||
if (end + 6 < len && buf[next] == '\\' && buf[next+1] == 'u') {
|
||||
JSON_UNICHR c2 = 0;
|
||||
end += 6;
|
||||
/* Decode 4 hex digits */
|
||||
for (next += 2; next < end; next++) {
|
||||
c2 <<= 4;
|
||||
JSON_UNICHR digit = buf[next];
|
||||
switch (digit) {
|
||||
JSON_UNICHR c2 = 0;
|
||||
end += 6;
|
||||
/* Decode 4 hex digits */
|
||||
for (next += 2; next < end; next++) {
|
||||
c2 <<= 4;
|
||||
JSON_UNICHR 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;
|
||||
@ -1042,18 +1032,18 @@ scanstring_str(PyObject *pystr, Py_ssize_t end, char *encoding, int strict, Py_s
|
||||
default:
|
||||
raise_errmsg(ERR_STRING_ESC4, pystr, end - 5);
|
||||
goto bail;
|
||||
}
|
||||
}
|
||||
if ((c2 & 0xfc00) != 0xdc00) {
|
||||
/* not a low surrogate, rewind */
|
||||
end -= 6;
|
||||
next = end;
|
||||
}
|
||||
else {
|
||||
c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((c2 & 0xfc00) != 0xdc00) {
|
||||
/* not a low surrogate, rewind */
|
||||
end -= 6;
|
||||
next = end;
|
||||
}
|
||||
else {
|
||||
c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif /* PY_MAJOR_VERSION >= 3 || Py_UNICODE_WIDE */
|
||||
}
|
||||
if (c > 0x7f) {
|
||||
@ -1224,15 +1214,15 @@ scanstring_unicode(PyObject *pystr, Py_ssize_t end, int strict, Py_ssize_t *next
|
||||
/* Surrogate pair */
|
||||
if ((c & 0xfc00) == 0xd800) {
|
||||
JSON_UNICHR c2 = 0;
|
||||
if (end + 6 < len &&
|
||||
PyUnicode_READ(kind, buf, next) == '\\' &&
|
||||
PyUnicode_READ(kind, buf, next + 1) == 'u') {
|
||||
end += 6;
|
||||
/* Decode 4 hex digits */
|
||||
for (next += 2; next < end; next++) {
|
||||
JSON_UNICHR digit = PyUnicode_READ(kind, buf, next);
|
||||
c2 <<= 4;
|
||||
switch (digit) {
|
||||
if (end + 6 < len &&
|
||||
PyUnicode_READ(kind, buf, next) == '\\' &&
|
||||
PyUnicode_READ(kind, buf, next + 1) == 'u') {
|
||||
end += 6;
|
||||
/* Decode 4 hex digits */
|
||||
for (next += 2; next < end; next++) {
|
||||
JSON_UNICHR digit = PyUnicode_READ(kind, buf, next);
|
||||
c2 <<= 4;
|
||||
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;
|
||||
@ -1245,18 +1235,18 @@ scanstring_unicode(PyObject *pystr, Py_ssize_t end, int strict, Py_ssize_t *next
|
||||
default:
|
||||
raise_errmsg(ERR_STRING_ESC4, pystr, end - 5);
|
||||
goto bail;
|
||||
}
|
||||
}
|
||||
if ((c2 & 0xfc00) != 0xdc00) {
|
||||
/* not a low surrogate, rewind */
|
||||
end -= 6;
|
||||
next = end;
|
||||
}
|
||||
else {
|
||||
c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((c2 & 0xfc00) != 0xdc00) {
|
||||
/* not a low surrogate, rewind */
|
||||
end -= 6;
|
||||
next = end;
|
||||
}
|
||||
else {
|
||||
c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
APPEND_OLD_CHUNK
|
||||
@ -1443,10 +1433,10 @@ _parse_object_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_
|
||||
|
||||
/* only loop if the object is non-empty */
|
||||
if (idx <= end_idx && str[idx] != '}') {
|
||||
int trailing_delimiter = 0;
|
||||
int trailing_delimiter = 0;
|
||||
while (idx <= end_idx) {
|
||||
PyObject *memokey;
|
||||
trailing_delimiter = 0;
|
||||
trailing_delimiter = 0;
|
||||
|
||||
/* read key */
|
||||
if (str[idx] != '"') {
|
||||
@ -1506,7 +1496,7 @@ _parse_object_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_
|
||||
while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++;
|
||||
|
||||
/* bail if the object is closed or we didn't get the , delimiter */
|
||||
did_parse = 1;
|
||||
did_parse = 1;
|
||||
if (idx > end_idx) break;
|
||||
if (str[idx] == '}') {
|
||||
break;
|
||||
@ -1519,20 +1509,20 @@ _parse_object_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_
|
||||
|
||||
/* skip whitespace after , delimiter */
|
||||
while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++;
|
||||
trailing_delimiter = 1;
|
||||
trailing_delimiter = 1;
|
||||
}
|
||||
if (trailing_delimiter) {
|
||||
raise_errmsg(ERR_OBJECT_PROPERTY, pystr, idx);
|
||||
goto bail;
|
||||
}
|
||||
if (trailing_delimiter) {
|
||||
raise_errmsg(ERR_OBJECT_PROPERTY, pystr, idx);
|
||||
goto bail;
|
||||
}
|
||||
}
|
||||
/* verify that idx < end_idx, str[idx] should be '}' */
|
||||
if (idx > end_idx || str[idx] != '}') {
|
||||
if (did_parse) {
|
||||
raise_errmsg(ERR_OBJECT_DELIMITER, pystr, idx);
|
||||
} else {
|
||||
raise_errmsg(ERR_OBJECT_PROPERTY_FIRST, pystr, idx);
|
||||
}
|
||||
if (did_parse) {
|
||||
raise_errmsg(ERR_OBJECT_DELIMITER, pystr, idx);
|
||||
} else {
|
||||
raise_errmsg(ERR_OBJECT_PROPERTY_FIRST, pystr, idx);
|
||||
}
|
||||
goto bail;
|
||||
}
|
||||
|
||||
@ -1605,10 +1595,10 @@ _parse_object_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ss
|
||||
|
||||
/* only loop if the object is non-empty */
|
||||
if (idx <= end_idx && PyUnicode_READ(kind, str, idx) != '}') {
|
||||
int trailing_delimiter = 0;
|
||||
int trailing_delimiter = 0;
|
||||
while (idx <= end_idx) {
|
||||
PyObject *memokey;
|
||||
trailing_delimiter = 0;
|
||||
trailing_delimiter = 0;
|
||||
|
||||
/* read key */
|
||||
if (PyUnicode_READ(kind, str, idx) != '"') {
|
||||
@ -1670,7 +1660,7 @@ _parse_object_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ss
|
||||
|
||||
/* bail if the object is closed or we didn't get the ,
|
||||
delimiter */
|
||||
did_parse = 1;
|
||||
did_parse = 1;
|
||||
if (idx > end_idx) break;
|
||||
if (PyUnicode_READ(kind, str, idx) == '}') {
|
||||
break;
|
||||
@ -1683,21 +1673,21 @@ _parse_object_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ss
|
||||
|
||||
/* skip whitespace after , delimiter */
|
||||
while (idx <= end_idx && IS_WHITESPACE(PyUnicode_READ(kind, str, idx))) idx++;
|
||||
trailing_delimiter = 1;
|
||||
trailing_delimiter = 1;
|
||||
}
|
||||
if (trailing_delimiter) {
|
||||
raise_errmsg(ERR_OBJECT_PROPERTY, pystr, idx);
|
||||
goto bail;
|
||||
}
|
||||
if (trailing_delimiter) {
|
||||
raise_errmsg(ERR_OBJECT_PROPERTY, pystr, idx);
|
||||
goto bail;
|
||||
}
|
||||
}
|
||||
|
||||
/* verify that idx < end_idx, str[idx] should be '}' */
|
||||
if (idx > end_idx || PyUnicode_READ(kind, str, idx) != '}') {
|
||||
if (did_parse) {
|
||||
raise_errmsg(ERR_OBJECT_DELIMITER, pystr, idx);
|
||||
} else {
|
||||
raise_errmsg(ERR_OBJECT_PROPERTY_FIRST, pystr, idx);
|
||||
}
|
||||
if (did_parse) {
|
||||
raise_errmsg(ERR_OBJECT_DELIMITER, pystr, idx);
|
||||
} else {
|
||||
raise_errmsg(ERR_OBJECT_PROPERTY_FIRST, pystr, idx);
|
||||
}
|
||||
goto bail;
|
||||
}
|
||||
|
||||
@ -1754,9 +1744,9 @@ _parse_array_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t
|
||||
|
||||
/* only loop if the array is non-empty */
|
||||
if (idx <= end_idx && str[idx] != ']') {
|
||||
int trailing_delimiter = 0;
|
||||
int trailing_delimiter = 0;
|
||||
while (idx <= end_idx) {
|
||||
trailing_delimiter = 0;
|
||||
trailing_delimiter = 0;
|
||||
/* read any JSON term and de-tuplefy the (rval, idx) */
|
||||
val = scan_once_str(s, pystr, idx, &next_idx);
|
||||
if (val == NULL) {
|
||||
@ -1785,21 +1775,21 @@ _parse_array_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t
|
||||
|
||||
/* skip whitespace after , */
|
||||
while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++;
|
||||
trailing_delimiter = 1;
|
||||
trailing_delimiter = 1;
|
||||
}
|
||||
if (trailing_delimiter) {
|
||||
raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx);
|
||||
goto bail;
|
||||
}
|
||||
if (trailing_delimiter) {
|
||||
raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx);
|
||||
goto bail;
|
||||
}
|
||||
}
|
||||
|
||||
/* verify that idx < end_idx, str[idx] should be ']' */
|
||||
if (idx > end_idx || str[idx] != ']') {
|
||||
if (PyList_GET_SIZE(rval)) {
|
||||
raise_errmsg(ERR_ARRAY_DELIMITER, pystr, idx);
|
||||
} else {
|
||||
raise_errmsg(ERR_ARRAY_VALUE_FIRST, pystr, idx);
|
||||
}
|
||||
if (PyList_GET_SIZE(rval)) {
|
||||
raise_errmsg(ERR_ARRAY_DELIMITER, pystr, idx);
|
||||
} else {
|
||||
raise_errmsg(ERR_ARRAY_VALUE_FIRST, pystr, idx);
|
||||
}
|
||||
goto bail;
|
||||
}
|
||||
*next_idx_ptr = idx + 1;
|
||||
@ -1835,9 +1825,9 @@ _parse_array_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssi
|
||||
|
||||
/* only loop if the array is non-empty */
|
||||
if (idx <= end_idx && PyUnicode_READ(kind, str, idx) != ']') {
|
||||
int trailing_delimiter = 0;
|
||||
int trailing_delimiter = 0;
|
||||
while (idx <= end_idx) {
|
||||
trailing_delimiter = 0;
|
||||
trailing_delimiter = 0;
|
||||
/* read any JSON term */
|
||||
val = scan_once_unicode(s, pystr, idx, &next_idx);
|
||||
if (val == NULL) {
|
||||
@ -1866,21 +1856,21 @@ _parse_array_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssi
|
||||
|
||||
/* skip whitespace after , */
|
||||
while (idx <= end_idx && IS_WHITESPACE(PyUnicode_READ(kind, str, idx))) idx++;
|
||||
trailing_delimiter = 1;
|
||||
trailing_delimiter = 1;
|
||||
}
|
||||
if (trailing_delimiter) {
|
||||
raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx);
|
||||
goto bail;
|
||||
}
|
||||
if (trailing_delimiter) {
|
||||
raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx);
|
||||
goto bail;
|
||||
}
|
||||
}
|
||||
|
||||
/* verify that idx < end_idx, str[idx] should be ']' */
|
||||
if (idx > end_idx || PyUnicode_READ(kind, str, idx) != ']') {
|
||||
if (PyList_GET_SIZE(rval)) {
|
||||
raise_errmsg(ERR_ARRAY_DELIMITER, pystr, idx);
|
||||
} else {
|
||||
raise_errmsg(ERR_ARRAY_VALUE_FIRST, pystr, idx);
|
||||
}
|
||||
if (PyList_GET_SIZE(rval)) {
|
||||
raise_errmsg(ERR_ARRAY_DELIMITER, pystr, idx);
|
||||
} else {
|
||||
raise_errmsg(ERR_ARRAY_VALUE_FIRST, pystr, idx);
|
||||
}
|
||||
goto bail;
|
||||
}
|
||||
*next_idx_ptr = idx + 1;
|
||||
@ -2150,8 +2140,8 @@ scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *n
|
||||
Py_ssize_t length = PyString_GET_SIZE(pystr);
|
||||
PyObject *rval = NULL;
|
||||
int fallthrough = 0;
|
||||
if (idx >= length) {
|
||||
raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx);
|
||||
if (idx < 0 || idx >= length) {
|
||||
raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx);
|
||||
return NULL;
|
||||
}
|
||||
switch (str[idx]) {
|
||||
@ -2258,8 +2248,8 @@ scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_
|
||||
Py_ssize_t length = PyUnicode_GetLength(pystr);
|
||||
PyObject *rval = NULL;
|
||||
int fallthrough = 0;
|
||||
if (idx >= length) {
|
||||
raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx);
|
||||
if (idx < 0 || idx >= length) {
|
||||
raise_errmsg(ERR_EXPECTING_VALUE, pystr, idx);
|
||||
return NULL;
|
||||
}
|
||||
switch (PyUnicode_READ(kind, str, idx)) {
|
||||
@ -2568,6 +2558,8 @@ encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
|
||||
s->item_sort_key = NULL;
|
||||
s->item_sort_kw = NULL;
|
||||
s->Decimal = NULL;
|
||||
s->max_long_size = NULL;
|
||||
s->min_long_size = NULL;
|
||||
}
|
||||
return (PyObject *)s;
|
||||
}
|
||||
@ -2576,13 +2568,33 @@ 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", "key_memo", "use_decimal", "namedtuple_as_object", "tuple_as_array", "bigint_as_string", "item_sort_key", "encoding", "for_json", "ignore_nan", "Decimal", NULL};
|
||||
static char *kwlist[] = {
|
||||
"markers",
|
||||
"default",
|
||||
"encoder",
|
||||
"indent",
|
||||
"key_separator",
|
||||
"item_separator",
|
||||
"sort_keys",
|
||||
"skipkeys",
|
||||
"allow_nan",
|
||||
"key_memo",
|
||||
"use_decimal",
|
||||
"namedtuple_as_object",
|
||||
"tuple_as_array",
|
||||
"int_as_string_bitcount",
|
||||
"item_sort_key",
|
||||
"encoding",
|
||||
"for_json",
|
||||
"ignore_nan",
|
||||
"Decimal",
|
||||
NULL};
|
||||
|
||||
PyEncoderObject *s;
|
||||
PyObject *markers, *defaultfn, *encoder, *indent, *key_separator;
|
||||
PyObject *item_separator, *sort_keys, *skipkeys, *allow_nan, *key_memo;
|
||||
PyObject *use_decimal, *namedtuple_as_object, *tuple_as_array;
|
||||
PyObject *bigint_as_string, *item_sort_key, *encoding, *for_json;
|
||||
PyObject *int_as_string_bitcount, *item_sort_key, *encoding, *for_json;
|
||||
PyObject *ignore_nan, *Decimal;
|
||||
|
||||
assert(PyEncoder_Check(self));
|
||||
@ -2591,21 +2603,30 @@ encoder_init(PyObject *self, PyObject *args, PyObject *kwds)
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOOOOOOOOOOOOOOOO:make_encoder", kwlist,
|
||||
&markers, &defaultfn, &encoder, &indent, &key_separator, &item_separator,
|
||||
&sort_keys, &skipkeys, &allow_nan, &key_memo, &use_decimal,
|
||||
&namedtuple_as_object, &tuple_as_array, &bigint_as_string,
|
||||
&item_sort_key, &encoding, &for_json, &ignore_nan, &Decimal))
|
||||
&namedtuple_as_object, &tuple_as_array,
|
||||
&int_as_string_bitcount, &item_sort_key, &encoding, &for_json,
|
||||
&ignore_nan, &Decimal))
|
||||
return -1;
|
||||
|
||||
Py_INCREF(markers);
|
||||
s->markers = markers;
|
||||
Py_INCREF(defaultfn);
|
||||
s->defaultfn = defaultfn;
|
||||
Py_INCREF(encoder);
|
||||
s->encoder = encoder;
|
||||
s->encoding = JSON_ParseEncoding(encoding);
|
||||
if (s->encoding == NULL)
|
||||
return -1;
|
||||
Py_INCREF(indent);
|
||||
s->indent = indent;
|
||||
Py_INCREF(key_separator);
|
||||
s->key_separator = key_separator;
|
||||
Py_INCREF(item_separator);
|
||||
s->item_separator = item_separator;
|
||||
Py_INCREF(skipkeys);
|
||||
s->skipkeys_bool = skipkeys;
|
||||
s->skipkeys = PyObject_IsTrue(skipkeys);
|
||||
Py_INCREF(key_memo);
|
||||
s->key_memo = key_memo;
|
||||
s->fast_encode = (PyCFunction_Check(s->encoder) && PyCFunction_GetFunction(s->encoder) == (PyCFunction)py_encode_basestring_ascii);
|
||||
s->allow_or_ignore_nan = (
|
||||
@ -2614,10 +2635,38 @@ encoder_init(PyObject *self, PyObject *args, PyObject *kwds)
|
||||
s->use_decimal = PyObject_IsTrue(use_decimal);
|
||||
s->namedtuple_as_object = PyObject_IsTrue(namedtuple_as_object);
|
||||
s->tuple_as_array = PyObject_IsTrue(tuple_as_array);
|
||||
s->bigint_as_string = PyObject_IsTrue(bigint_as_string);
|
||||
if (PyInt_Check(int_as_string_bitcount) || PyLong_Check(int_as_string_bitcount)) {
|
||||
static const unsigned int long_long_bitsize = SIZEOF_LONG_LONG * 8;
|
||||
int int_as_string_bitcount_val = PyLong_AsLong(int_as_string_bitcount);
|
||||
if (int_as_string_bitcount_val > 0 && int_as_string_bitcount_val < long_long_bitsize) {
|
||||
s->max_long_size = PyLong_FromUnsignedLongLong(1ULL << int_as_string_bitcount_val);
|
||||
s->min_long_size = PyLong_FromLongLong(-1LL << int_as_string_bitcount_val);
|
||||
if (s->min_long_size == NULL || s->max_long_size == NULL) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"int_as_string_bitcount (%d) must be greater than 0 and less than the number of bits of a `long long` type (%u bits)",
|
||||
int_as_string_bitcount_val, long_long_bitsize);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (int_as_string_bitcount == Py_None) {
|
||||
Py_INCREF(Py_None);
|
||||
s->max_long_size = Py_None;
|
||||
Py_INCREF(Py_None);
|
||||
s->min_long_size = Py_None;
|
||||
}
|
||||
else {
|
||||
PyErr_SetString(PyExc_TypeError, "int_as_string_bitcount must be None or an integer");
|
||||
return -1;
|
||||
}
|
||||
if (item_sort_key != Py_None) {
|
||||
if (!PyCallable_Check(item_sort_key))
|
||||
if (!PyCallable_Check(item_sort_key)) {
|
||||
PyErr_SetString(PyExc_TypeError, "item_sort_key must be None or callable");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (PyObject_IsTrue(sort_keys)) {
|
||||
static PyObject *itemgetter0 = NULL;
|
||||
@ -2643,22 +2692,14 @@ encoder_init(PyObject *self, PyObject *args, PyObject *kwds)
|
||||
if (PyDict_SetItemString(s->item_sort_kw, "key", item_sort_key))
|
||||
return -1;
|
||||
}
|
||||
Py_INCREF(sort_keys);
|
||||
s->sort_keys = sort_keys;
|
||||
Py_INCREF(item_sort_key);
|
||||
s->item_sort_key = item_sort_key;
|
||||
Py_INCREF(Decimal);
|
||||
s->Decimal = Decimal;
|
||||
s->for_json = PyObject_IsTrue(for_json);
|
||||
|
||||
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->key_memo);
|
||||
Py_INCREF(s->skipkeys_bool);
|
||||
Py_INCREF(s->sort_keys);
|
||||
Py_INCREF(s->item_sort_key);
|
||||
Py_INCREF(s->Decimal);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -2801,11 +2842,9 @@ encoder_listencode_obj(PyEncoderObject *s, JSON_Accu *rval, PyObject *obj, Py_ss
|
||||
else if (PyInt_Check(obj) || PyLong_Check(obj)) {
|
||||
PyObject *encoded = PyObject_Str(obj);
|
||||
if (encoded != NULL) {
|
||||
if (s->bigint_as_string) {
|
||||
encoded = maybe_quote_bigint(encoded, obj);
|
||||
if (encoded == NULL)
|
||||
break;
|
||||
}
|
||||
encoded = maybe_quote_bigint(s, encoded, obj);
|
||||
if (encoded == NULL)
|
||||
break;
|
||||
rv = _steal_accumulate(rval, encoded);
|
||||
}
|
||||
}
|
||||
@ -3031,6 +3070,7 @@ encoder_listencode_dict(PyEncoderObject *s, JSON_Accu *rval, PyObject *dct, Py_s
|
||||
bail:
|
||||
Py_XDECREF(encoded);
|
||||
Py_XDECREF(items);
|
||||
Py_XDECREF(item);
|
||||
Py_XDECREF(iter);
|
||||
Py_XDECREF(kstr);
|
||||
Py_XDECREF(ident);
|
||||
@ -3157,6 +3197,8 @@ encoder_traverse(PyObject *self, visitproc visit, void *arg)
|
||||
Py_VISIT(s->sort_keys);
|
||||
Py_VISIT(s->item_sort_kw);
|
||||
Py_VISIT(s->item_sort_key);
|
||||
Py_VISIT(s->max_long_size);
|
||||
Py_VISIT(s->min_long_size);
|
||||
Py_VISIT(s->Decimal);
|
||||
return 0;
|
||||
}
|
||||
@ -3180,6 +3222,8 @@ encoder_clear(PyObject *self)
|
||||
Py_CLEAR(s->sort_keys);
|
||||
Py_CLEAR(s->item_sort_kw);
|
||||
Py_CLEAR(s->item_sort_key);
|
||||
Py_CLEAR(s->max_long_size);
|
||||
Py_CLEAR(s->min_long_size);
|
||||
Py_CLEAR(s->Decimal);
|
||||
return 0;
|
||||
}
|
||||
|
@ -20,7 +20,10 @@ if sys.version_info[0] < 3:
|
||||
|
||||
else:
|
||||
PY3 = True
|
||||
from imp import reload as reload_module
|
||||
if sys.version_info[:2] >= (3, 4):
|
||||
from importlib import reload as reload_module
|
||||
else:
|
||||
from imp import reload as reload_module
|
||||
import codecs
|
||||
def b(s):
|
||||
return codecs.latin_1_encode(s)[0]
|
||||
|
@ -281,7 +281,7 @@ class JSONDecoder(object):
|
||||
+---------------+-------------------+
|
||||
| array | list |
|
||||
+---------------+-------------------+
|
||||
| string | unicode |
|
||||
| string | str, unicode |
|
||||
+---------------+-------------------+
|
||||
| number (int) | int, long |
|
||||
+---------------+-------------------+
|
||||
@ -384,6 +384,17 @@ class JSONDecoder(object):
|
||||
have extraneous data at the end.
|
||||
|
||||
"""
|
||||
if idx < 0:
|
||||
# Ensure that raw_decode bails on negative indexes, the regex
|
||||
# would otherwise mask this behavior. #98
|
||||
raise JSONDecodeError('Expecting value', s, idx)
|
||||
if _PY3 and not isinstance(s, text_type):
|
||||
raise TypeError("Input string must be text, not bytes")
|
||||
# strip UTF-8 bom
|
||||
if len(s) > idx:
|
||||
ord0 = ord(s[idx])
|
||||
if ord0 == 0xfeff:
|
||||
idx += 1
|
||||
elif ord0 == 0xef and s[idx:idx + 3] == '\xef\xbb\xbf':
|
||||
idx += 3
|
||||
return self.scan_once(s, idx=_w(s, idx).end())
|
||||
|
@ -18,7 +18,7 @@ from simplejson.decoder import PosInf
|
||||
#ESCAPE = re.compile(ur'[\x00-\x1f\\"\b\f\n\r\t\u2028\u2029]')
|
||||
# This is required because u() will mangle the string and ur'' isn't valid
|
||||
# python3 syntax
|
||||
ESCAPE = re.compile(u('[\\x00-\\x1f\\\\"\\b\\f\\n\\r\\t\u2028\u2029]'))
|
||||
ESCAPE = re.compile(u'[\\x00-\\x1f\\\\"\\b\\f\\n\\r\\t\u2028\u2029]')
|
||||
ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])')
|
||||
HAS_UTF8 = re.compile(r'[\x80-\xff]')
|
||||
ESCAPE_DCT = {
|
||||
@ -116,12 +116,14 @@ class JSONEncoder(object):
|
||||
"""
|
||||
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,
|
||||
use_decimal=True, namedtuple_as_object=True,
|
||||
tuple_as_array=True, bigint_as_string=False,
|
||||
item_sort_key=None, for_json=False, ignore_nan=False):
|
||||
check_circular=True, allow_nan=True, sort_keys=False,
|
||||
indent=None, separators=None, encoding='utf-8', default=None,
|
||||
use_decimal=True, namedtuple_as_object=True,
|
||||
tuple_as_array=True, bigint_as_string=False,
|
||||
item_sort_key=None, for_json=False, ignore_nan=False,
|
||||
int_as_string_bitcount=None):
|
||||
"""Constructor for JSONEncoder, with sensible defaults.
|
||||
|
||||
If skipkeys is false, then it is a TypeError to attempt
|
||||
@ -180,6 +182,10 @@ class JSONEncoder(object):
|
||||
or lower than -2**53 will be encoded as strings. This is to avoid the
|
||||
rounding that happens in Javascript otherwise.
|
||||
|
||||
If int_as_string_bitcount is a positive number (n), then int of size
|
||||
greater than or equal to 2**n or lower than or equal to -2**n will be
|
||||
encoded as strings.
|
||||
|
||||
If specified, item_sort_key is a callable used to sort the items in
|
||||
each dictionary. This is useful if you want to sort items other than
|
||||
in alphabetical order by key.
|
||||
@ -207,6 +213,7 @@ class JSONEncoder(object):
|
||||
self.item_sort_key = item_sort_key
|
||||
self.for_json = for_json
|
||||
self.ignore_nan = ignore_nan
|
||||
self.int_as_string_bitcount = int_as_string_bitcount
|
||||
if indent is not None and not isinstance(indent, string_types):
|
||||
indent = indent * ' '
|
||||
self.indent = indent
|
||||
@ -265,7 +272,7 @@ class JSONEncoder(object):
|
||||
if self.ensure_ascii:
|
||||
return ''.join(chunks)
|
||||
else:
|
||||
return u('').join(chunks)
|
||||
return u''.join(chunks)
|
||||
|
||||
def iterencode(self, o, _one_shot=False):
|
||||
"""Encode the given object and yield each string
|
||||
@ -315,8 +322,9 @@ class JSONEncoder(object):
|
||||
|
||||
return text
|
||||
|
||||
|
||||
key_memo = {}
|
||||
int_as_string_bitcount = (
|
||||
53 if self.bigint_as_string else self.int_as_string_bitcount)
|
||||
if (_one_shot and c_make_encoder is not None
|
||||
and self.indent is None):
|
||||
_iterencode = c_make_encoder(
|
||||
@ -324,17 +332,17 @@ class JSONEncoder(object):
|
||||
self.key_separator, self.item_separator, self.sort_keys,
|
||||
self.skipkeys, self.allow_nan, key_memo, self.use_decimal,
|
||||
self.namedtuple_as_object, self.tuple_as_array,
|
||||
self.bigint_as_string, self.item_sort_key,
|
||||
self.encoding, self.for_json, self.ignore_nan,
|
||||
Decimal)
|
||||
int_as_string_bitcount,
|
||||
self.item_sort_key, self.encoding, self.for_json,
|
||||
self.ignore_nan, Decimal)
|
||||
else:
|
||||
_iterencode = _make_iterencode(
|
||||
markers, self.default, _encoder, self.indent, floatstr,
|
||||
self.key_separator, self.item_separator, self.sort_keys,
|
||||
self.skipkeys, _one_shot, self.use_decimal,
|
||||
self.namedtuple_as_object, self.tuple_as_array,
|
||||
self.bigint_as_string, self.item_sort_key,
|
||||
self.encoding, self.for_json,
|
||||
int_as_string_bitcount,
|
||||
self.item_sort_key, self.encoding, self.for_json,
|
||||
Decimal=Decimal)
|
||||
try:
|
||||
return _iterencode(o, 0)
|
||||
@ -358,7 +366,7 @@ class JSONEncoderForHTML(JSONEncoder):
|
||||
if self.ensure_ascii:
|
||||
return ''.join(chunks)
|
||||
else:
|
||||
return u('').join(chunks)
|
||||
return u''.join(chunks)
|
||||
|
||||
def iterencode(self, o, _one_shot=False):
|
||||
chunks = super(JSONEncoderForHTML, self).iterencode(o, _one_shot)
|
||||
@ -372,7 +380,8 @@ class JSONEncoderForHTML(JSONEncoder):
|
||||
def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
|
||||
_key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot,
|
||||
_use_decimal, _namedtuple_as_object, _tuple_as_array,
|
||||
_bigint_as_string, _item_sort_key, _encoding, _for_json,
|
||||
_int_as_string_bitcount, _item_sort_key,
|
||||
_encoding,_for_json,
|
||||
## HACK: hand-optimized bytecode; turn globals into locals
|
||||
_PY3=PY3,
|
||||
ValueError=ValueError,
|
||||
@ -392,6 +401,26 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
|
||||
elif _sort_keys and not _item_sort_key:
|
||||
_item_sort_key = itemgetter(0)
|
||||
|
||||
if (_int_as_string_bitcount is not None and
|
||||
(_int_as_string_bitcount <= 0 or
|
||||
not isinstance(_int_as_string_bitcount, integer_types))):
|
||||
raise TypeError("int_as_string_bitcount must be a positive integer")
|
||||
|
||||
def _encode_int(value):
|
||||
skip_quoting = (
|
||||
_int_as_string_bitcount is None
|
||||
or
|
||||
_int_as_string_bitcount < 1
|
||||
)
|
||||
if (
|
||||
skip_quoting or
|
||||
(-1 << _int_as_string_bitcount)
|
||||
< value <
|
||||
(1 << _int_as_string_bitcount)
|
||||
):
|
||||
return str(value)
|
||||
return '"' + str(value) + '"'
|
||||
|
||||
def _iterencode_list(lst, _current_indent_level):
|
||||
if not lst:
|
||||
yield '[]'
|
||||
@ -426,10 +455,7 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
|
||||
elif value is False:
|
||||
yield buf + 'false'
|
||||
elif isinstance(value, integer_types):
|
||||
yield ((buf + str(value))
|
||||
if (not _bigint_as_string or
|
||||
(-1 << 53) < value < (1 << 53))
|
||||
else (buf + '"' + str(value) + '"'))
|
||||
yield buf + _encode_int(value)
|
||||
elif isinstance(value, float):
|
||||
yield buf + _floatstr(value)
|
||||
elif _use_decimal and isinstance(value, Decimal):
|
||||
@ -540,10 +566,7 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
|
||||
elif value is False:
|
||||
yield 'false'
|
||||
elif isinstance(value, integer_types):
|
||||
yield (str(value)
|
||||
if (not _bigint_as_string or
|
||||
(-1 << 53) < value < (1 << 53))
|
||||
else ('"' + str(value) + '"'))
|
||||
yield _encode_int(value)
|
||||
elif isinstance(value, float):
|
||||
yield _floatstr(value)
|
||||
elif _use_decimal and isinstance(value, Decimal):
|
||||
@ -585,10 +608,7 @@ def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
|
||||
elif o is False:
|
||||
yield 'false'
|
||||
elif isinstance(o, integer_types):
|
||||
yield (str(o)
|
||||
if (not _bigint_as_string or
|
||||
(-1 << 53) < o < (1 << 53))
|
||||
else ('"' + str(o) + '"'))
|
||||
yield _encode_int(o)
|
||||
elif isinstance(o, float):
|
||||
yield _floatstr(o)
|
||||
else:
|
||||
|
@ -41,6 +41,9 @@ class JSONDecodeError(ValueError):
|
||||
else:
|
||||
self.endlineno, self.endcolno = None, None
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (self.msg, self.doc, self.pos, self.end)
|
||||
|
||||
|
||||
def linecol(doc, pos):
|
||||
lineno = doc.count('\n', 0, pos) + 1
|
||||
@ -115,6 +118,11 @@ def py_make_scanner(context):
|
||||
raise JSONDecodeError(errmsg, string, idx)
|
||||
|
||||
def scan_once(string, idx):
|
||||
if idx < 0:
|
||||
# Ensure the same behavior as the C speedup, otherwise
|
||||
# this would work for *some* negative string indices due
|
||||
# to the behavior of __getitem__ for strings. #98
|
||||
raise JSONDecodeError('Expecting value', string, idx)
|
||||
try:
|
||||
return _scan_once(string, idx)
|
||||
finally:
|
||||
|
@ -3,19 +3,16 @@ import unittest
|
||||
import doctest
|
||||
import sys
|
||||
|
||||
class OptionalExtensionTestSuite(unittest.TestSuite):
|
||||
|
||||
class NoExtensionTestSuite(unittest.TestSuite):
|
||||
def run(self, result):
|
||||
import simplejson
|
||||
run = unittest.TestSuite.run
|
||||
run(self, result)
|
||||
if simplejson._import_c_make_encoder() is None:
|
||||
TestMissingSpeedups().run(result)
|
||||
else:
|
||||
simplejson._toggle_speedups(False)
|
||||
run(self, result)
|
||||
simplejson._toggle_speedups(True)
|
||||
simplejson._toggle_speedups(False)
|
||||
result = unittest.TestSuite.run(self, result)
|
||||
simplejson._toggle_speedups(True)
|
||||
return result
|
||||
|
||||
|
||||
class TestMissingSpeedups(unittest.TestCase):
|
||||
def runTest(self):
|
||||
if hasattr(sys, 'pypy_translation_info'):
|
||||
@ -23,6 +20,7 @@ class TestMissingSpeedups(unittest.TestCase):
|
||||
elif hasattr(self, 'skipTest'):
|
||||
self.skipTest('_speedups.so is missing!')
|
||||
|
||||
|
||||
def additional_tests(suite=None):
|
||||
import simplejson
|
||||
import simplejson.encoder
|
||||
@ -36,34 +34,45 @@ def additional_tests(suite=None):
|
||||
|
||||
|
||||
def all_tests_suite():
|
||||
suite = unittest.TestLoader().loadTestsFromNames([
|
||||
'simplejson.tests.test_bigint_as_string',
|
||||
'simplejson.tests.test_check_circular',
|
||||
'simplejson.tests.test_decode',
|
||||
'simplejson.tests.test_default',
|
||||
'simplejson.tests.test_dump',
|
||||
'simplejson.tests.test_encode_basestring_ascii',
|
||||
'simplejson.tests.test_encode_for_html',
|
||||
'simplejson.tests.test_errors',
|
||||
'simplejson.tests.test_fail',
|
||||
'simplejson.tests.test_float',
|
||||
'simplejson.tests.test_indent',
|
||||
'simplejson.tests.test_pass1',
|
||||
'simplejson.tests.test_pass2',
|
||||
'simplejson.tests.test_pass3',
|
||||
'simplejson.tests.test_recursion',
|
||||
'simplejson.tests.test_scanstring',
|
||||
'simplejson.tests.test_separators',
|
||||
'simplejson.tests.test_speedups',
|
||||
'simplejson.tests.test_unicode',
|
||||
'simplejson.tests.test_decimal',
|
||||
'simplejson.tests.test_tuple',
|
||||
'simplejson.tests.test_namedtuple',
|
||||
'simplejson.tests.test_tool',
|
||||
'simplejson.tests.test_for_json',
|
||||
])
|
||||
suite = additional_tests(suite)
|
||||
return OptionalExtensionTestSuite([suite])
|
||||
def get_suite():
|
||||
return additional_tests(
|
||||
unittest.TestLoader().loadTestsFromNames([
|
||||
'simplejson.tests.test_bitsize_int_as_string',
|
||||
'simplejson.tests.test_bigint_as_string',
|
||||
'simplejson.tests.test_check_circular',
|
||||
'simplejson.tests.test_decode',
|
||||
'simplejson.tests.test_default',
|
||||
'simplejson.tests.test_dump',
|
||||
'simplejson.tests.test_encode_basestring_ascii',
|
||||
'simplejson.tests.test_encode_for_html',
|
||||
'simplejson.tests.test_errors',
|
||||
'simplejson.tests.test_fail',
|
||||
'simplejson.tests.test_float',
|
||||
'simplejson.tests.test_indent',
|
||||
'simplejson.tests.test_pass1',
|
||||
'simplejson.tests.test_pass2',
|
||||
'simplejson.tests.test_pass3',
|
||||
'simplejson.tests.test_recursion',
|
||||
'simplejson.tests.test_scanstring',
|
||||
'simplejson.tests.test_separators',
|
||||
'simplejson.tests.test_speedups',
|
||||
'simplejson.tests.test_unicode',
|
||||
'simplejson.tests.test_decimal',
|
||||
'simplejson.tests.test_tuple',
|
||||
'simplejson.tests.test_namedtuple',
|
||||
'simplejson.tests.test_tool',
|
||||
'simplejson.tests.test_for_json',
|
||||
]))
|
||||
suite = get_suite()
|
||||
import simplejson
|
||||
if simplejson._import_c_make_encoder() is None:
|
||||
suite.addTest(TestMissingSpeedups())
|
||||
else:
|
||||
suite = unittest.TestSuite([
|
||||
suite,
|
||||
NoExtensionTestSuite([get_suite()]),
|
||||
])
|
||||
return suite
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -1,7 +1,7 @@
|
||||
from unittest import TestCase
|
||||
|
||||
import simplejson as json
|
||||
from simplejson.compat import long_type
|
||||
|
||||
|
||||
class TestBigintAsString(TestCase):
|
||||
# Python 2.5, at least the one that ships on Mac OS X, calculates
|
||||
@ -15,44 +15,53 @@ class TestBigintAsString(TestCase):
|
||||
((-1 << 53) - 1, '-9007199254740993'),
|
||||
((-1 << 53) + 1, -9007199254740991)]
|
||||
|
||||
options = (
|
||||
{"bigint_as_string": True},
|
||||
{"int_as_string_bitcount": 53}
|
||||
)
|
||||
|
||||
def test_ints(self):
|
||||
for val, expect in self.values:
|
||||
self.assertEqual(
|
||||
val,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, bigint_as_string=True)))
|
||||
for opts in self.options:
|
||||
for val, expect in self.values:
|
||||
self.assertEqual(
|
||||
val,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, **opts)))
|
||||
|
||||
def test_lists(self):
|
||||
for val, expect in self.values:
|
||||
val = [val, val]
|
||||
expect = [expect, expect]
|
||||
self.assertEqual(
|
||||
val,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, bigint_as_string=True)))
|
||||
for opts in self.options:
|
||||
for val, expect in self.values:
|
||||
val = [val, val]
|
||||
expect = [expect, expect]
|
||||
self.assertEqual(
|
||||
val,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, **opts)))
|
||||
|
||||
def test_dicts(self):
|
||||
for val, expect in self.values:
|
||||
val = {'k': val}
|
||||
expect = {'k': expect}
|
||||
self.assertEqual(
|
||||
val,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, bigint_as_string=True)))
|
||||
for opts in self.options:
|
||||
for val, expect in self.values:
|
||||
val = {'k': val}
|
||||
expect = {'k': expect}
|
||||
self.assertEqual(
|
||||
val,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, **opts)))
|
||||
|
||||
def test_dict_keys(self):
|
||||
for val, _ in self.values:
|
||||
expect = {str(val): 'value'}
|
||||
val = {val: 'value'}
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, bigint_as_string=True)))
|
||||
for opts in self.options:
|
||||
for val, _ in self.values:
|
||||
expect = {str(val): 'value'}
|
||||
val = {val: 'value'}
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, **opts)))
|
||||
|
@ -0,0 +1,73 @@
|
||||
from unittest import TestCase
|
||||
|
||||
import simplejson as json
|
||||
|
||||
|
||||
class TestBitSizeIntAsString(TestCase):
|
||||
# Python 2.5, at least the one that ships on Mac OS X, calculates
|
||||
# 2 ** 31 as 0! It manages to calculate 1 << 31 correctly.
|
||||
values = [
|
||||
(200, 200),
|
||||
((1 << 31) - 1, (1 << 31) - 1),
|
||||
((1 << 31), str(1 << 31)),
|
||||
((1 << 31) + 1, str((1 << 31) + 1)),
|
||||
(-100, -100),
|
||||
((-1 << 31), str(-1 << 31)),
|
||||
((-1 << 31) - 1, str((-1 << 31) - 1)),
|
||||
((-1 << 31) + 1, (-1 << 31) + 1),
|
||||
]
|
||||
|
||||
def test_invalid_counts(self):
|
||||
for n in ['foo', -1, 0, 1.0]:
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
json.dumps, 0, int_as_string_bitcount=n)
|
||||
|
||||
def test_ints_outside_range_fails(self):
|
||||
self.assertNotEqual(
|
||||
str(1 << 15),
|
||||
json.loads(json.dumps(1 << 15, int_as_string_bitcount=16)),
|
||||
)
|
||||
|
||||
def test_ints(self):
|
||||
for val, expect in self.values:
|
||||
self.assertEqual(
|
||||
val,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, int_as_string_bitcount=31)),
|
||||
)
|
||||
|
||||
def test_lists(self):
|
||||
for val, expect in self.values:
|
||||
val = [val, val]
|
||||
expect = [expect, expect]
|
||||
self.assertEqual(
|
||||
val,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, int_as_string_bitcount=31)))
|
||||
|
||||
def test_dicts(self):
|
||||
for val, expect in self.values:
|
||||
val = {'k': val}
|
||||
expect = {'k': expect}
|
||||
self.assertEqual(
|
||||
val,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, int_as_string_bitcount=31)))
|
||||
|
||||
def test_dict_keys(self):
|
||||
for val, _ in self.values:
|
||||
expect = {str(val): 'value'}
|
||||
val = {val: 'value'}
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val)))
|
||||
self.assertEqual(
|
||||
expect,
|
||||
json.loads(json.dumps(val, int_as_string_bitcount=31)))
|
@ -86,3 +86,14 @@ class TestDecode(TestCase):
|
||||
self.assertEqual(
|
||||
({'a': {}}, 11),
|
||||
cls().raw_decode(" \n{\"a\": {}}"))
|
||||
|
||||
def test_bounds_checking(self):
|
||||
# https://github.com/simplejson/simplejson/issues/98
|
||||
j = json.decoder.JSONDecoder()
|
||||
for i in [4, 5, 6, -1, -2, -3, -4, -5, -6]:
|
||||
self.assertRaises(ValueError, j.scan_once, '1234', i)
|
||||
self.assertRaises(ValueError, j.raw_decode, '1234', i)
|
||||
x, y = sorted(['128931233', '472389423'], key=id)
|
||||
diff = id(x) - id(y)
|
||||
self.assertRaises(ValueError, j.scan_once, y, diff)
|
||||
self.assertRaises(ValueError, j.raw_decode, y, i)
|
||||
|
@ -119,3 +119,12 @@ class TestDump(TestCase):
|
||||
# the C API uses an accumulator that collects after 100,000 appends
|
||||
lst = [0] * 100000
|
||||
self.assertEqual(json.loads(json.dumps(lst)), lst)
|
||||
|
||||
def test_sort_keys(self):
|
||||
# https://github.com/simplejson/simplejson/issues/106
|
||||
for num_keys in range(2, 32):
|
||||
p = dict((str(x), x) for x in range(num_keys))
|
||||
sio = StringIO()
|
||||
json.dump(p, sio, sort_keys=True)
|
||||
self.assertEqual(sio.getvalue(), json.dumps(p, sort_keys=True))
|
||||
self.assertEqual(json.loads(sio.getvalue()), p)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import sys
|
||||
import sys, pickle
|
||||
from unittest import TestCase
|
||||
|
||||
import simplejson as json
|
||||
@ -33,3 +33,19 @@ class TestErrors(TestCase):
|
||||
self.fail('Expected JSONDecodeError')
|
||||
self.assertEqual(err.lineno, 1)
|
||||
self.assertEqual(err.colno, 10)
|
||||
|
||||
def test_error_is_pickable(self):
|
||||
err = None
|
||||
try:
|
||||
json.loads('{}\na\nb')
|
||||
except json.JSONDecodeError:
|
||||
err = sys.exc_info()[1]
|
||||
else:
|
||||
self.fail('Expected JSONDecodeError')
|
||||
s = pickle.dumps(err)
|
||||
e = pickle.loads(s)
|
||||
|
||||
self.assertEqual(err.msg, e.msg)
|
||||
self.assertEqual(err.doc, e.doc)
|
||||
self.assertEqual(err.pos, e.pos)
|
||||
self.assertEqual(err.end, e.end)
|
||||
|
@ -1,20 +1,39 @@
|
||||
import sys
|
||||
import unittest
|
||||
from unittest import TestCase
|
||||
|
||||
from simplejson import encoder, scanner
|
||||
|
||||
|
||||
def has_speedups():
|
||||
return encoder.c_make_encoder is not None
|
||||
|
||||
class TestDecode(TestCase):
|
||||
def test_make_scanner(self):
|
||||
|
||||
def skip_if_speedups_missing(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
if not has_speedups():
|
||||
return
|
||||
if hasattr(unittest, 'SkipTest'):
|
||||
raise unittest.SkipTest("C Extension not available")
|
||||
else:
|
||||
sys.stdout.write("C Extension not available")
|
||||
return
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class TestDecode(TestCase):
|
||||
@skip_if_speedups_missing
|
||||
def test_make_scanner(self):
|
||||
self.assertRaises(AttributeError, scanner.c_make_scanner, 1)
|
||||
|
||||
@skip_if_speedups_missing
|
||||
def test_make_encoder(self):
|
||||
if not has_speedups():
|
||||
return
|
||||
self.assertRaises(TypeError, encoder.c_make_encoder,
|
||||
self.assertRaises(
|
||||
TypeError,
|
||||
encoder.c_make_encoder,
|
||||
None,
|
||||
"\xCD\x7D\x3D\x4E\x12\x4C\xF9\x79\xD7\x52\xBA\x82\xF2\x27\x4A\x7D\xA0\xCA\x75",
|
||||
None)
|
||||
("\xCD\x7D\x3D\x4E\x12\x4C\xF9\x79\xD7"
|
||||
"\x52\xBA\x82\xF2\x27\x4A\x7D\xA0\xCA\x75"),
|
||||
None
|
||||
)
|
||||
|
@ -1,8 +1,9 @@
|
||||
import sys
|
||||
import codecs
|
||||
from unittest import TestCase
|
||||
|
||||
import simplejson as json
|
||||
from simplejson.compat import unichr, text_type, b, u
|
||||
from simplejson.compat import unichr, text_type, b, u, BytesIO
|
||||
|
||||
class TestUnicode(TestCase):
|
||||
def test_encoding1(self):
|
||||
@ -143,3 +144,10 @@ class TestUnicode(TestCase):
|
||||
self.assertEqual(
|
||||
json.dumps(c, ensure_ascii=False),
|
||||
'"' + c + '"')
|
||||
|
||||
def test_strip_bom(self):
|
||||
content = u"\u3053\u3093\u306b\u3061\u308f"
|
||||
json_doc = codecs.BOM_UTF8 + b(json.dumps(content))
|
||||
self.assertEqual(json.load(BytesIO(json_doc)), content)
|
||||
for doc in json_doc, json_doc.decode('utf8'):
|
||||
self.assertEqual(json.loads(doc), content)
|
||||
|
109
packages/wakatime/session_cache.py
Normal file
109
packages/wakatime/session_cache.py
Normal file
@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wakatime.session_cache
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Persist requests.Session for multiprocess SSL handshake pooling.
|
||||
|
||||
:copyright: (c) 2015 Alan Hamlett.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
try:
|
||||
import sqlite3
|
||||
HAS_SQL = True
|
||||
except ImportError:
|
||||
HAS_SQL = False
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'packages'))
|
||||
|
||||
from .packages import requests
|
||||
|
||||
|
||||
log = logging.getLogger('WakaTime')
|
||||
|
||||
|
||||
class SessionCache(object):
|
||||
DB_FILE = os.path.join(os.path.expanduser('~'), '.wakatime.db')
|
||||
|
||||
def connect(self):
|
||||
conn = sqlite3.connect(self.DB_FILE)
|
||||
c = conn.cursor()
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS session (
|
||||
value BLOB)
|
||||
''')
|
||||
return (conn, c)
|
||||
|
||||
|
||||
def save(self, session):
|
||||
"""Saves a requests.Session object for the next heartbeat process.
|
||||
"""
|
||||
|
||||
if not HAS_SQL:
|
||||
return
|
||||
try:
|
||||
conn, c = self.connect()
|
||||
c.execute('DELETE FROM session')
|
||||
values = {
|
||||
'value': pickle.dumps(session),
|
||||
}
|
||||
c.execute('INSERT INTO session VALUES (:value)', values)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except:
|
||||
log.error(traceback.format_exc())
|
||||
|
||||
|
||||
def get(self):
|
||||
"""Returns a requests.Session object.
|
||||
|
||||
Gets Session from sqlite3 cache or creates a new Session.
|
||||
"""
|
||||
|
||||
if not HAS_SQL:
|
||||
return requests.session()
|
||||
|
||||
try:
|
||||
conn, c = self.connect()
|
||||
except:
|
||||
log.error(traceback.format_exc())
|
||||
return requests.session()
|
||||
|
||||
session = None
|
||||
try:
|
||||
c.execute('BEGIN IMMEDIATE')
|
||||
c.execute('SELECT value FROM session LIMIT 1')
|
||||
row = c.fetchone()
|
||||
if row is not None:
|
||||
session = pickle.loads(row[0])
|
||||
except:
|
||||
log.error(traceback.format_exc())
|
||||
|
||||
try:
|
||||
conn.close()
|
||||
except:
|
||||
log.error(traceback.format_exc())
|
||||
|
||||
return session if session is not None else requests.session()
|
||||
|
||||
|
||||
def delete(self):
|
||||
"""Clears all cached Session objects.
|
||||
"""
|
||||
|
||||
if not HAS_SQL:
|
||||
return
|
||||
try:
|
||||
conn, c = self.connect()
|
||||
c.execute('DELETE FROM session')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except:
|
||||
log.error(traceback.format_exc())
|
@ -86,12 +86,14 @@ def number_lines_in_file(file_name):
|
||||
return lines
|
||||
|
||||
|
||||
def get_file_stats(file_name, notfile=False):
|
||||
def get_file_stats(file_name, notfile=False, lineno=None, cursorpos=None):
|
||||
if notfile:
|
||||
stats = {
|
||||
'language': None,
|
||||
'dependencies': [],
|
||||
'lines': None,
|
||||
'lineno': lineno,
|
||||
'cursorpos': cursorpos,
|
||||
}
|
||||
else:
|
||||
language, lexer = guess_language(file_name)
|
||||
@ -101,5 +103,7 @@ def get_file_stats(file_name, notfile=False):
|
||||
'language': language,
|
||||
'dependencies': dependencies,
|
||||
'lines': number_lines_in_file(file_name),
|
||||
'lineno': lineno,
|
||||
'cursorpos': cursorpos,
|
||||
}
|
||||
return stats
|
||||
|
Reference in New Issue
Block a user