From 10b3b09f02bfd9636af4a10b84abcd3c26035949 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 May 2017 10:56:31 -0500 Subject: [PATCH 01/67] Don't override socket.socket for binding, eliminiate globals SOURCE and USER_AGENT --- speedtest.py | 298 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 237 insertions(+), 61 deletions(-) diff --git a/speedtest.py b/speedtest.py index cb4a374..115b2f8 100755 --- a/speedtest.py +++ b/speedtest.py @@ -51,14 +51,10 @@ class FakeShutdownEvent(object): # Some global variables we use -USER_AGENT = None -SOURCE = None SHUTDOWN_EVENT = FakeShutdownEvent() SCHEME = 'http' DEBUG = False - -# Used for bound_interface -SOCKET_SOCKET = socket.socket +_GLOBAL_DEFAULT_TIMEOUT = object() # Begin import game to handle Python 2 and Python 3 try: @@ -79,9 +75,15 @@ except ImportError: ET = None try: - from urllib2 import urlopen, Request, HTTPError, URLError + from urllib2 import (urlopen, Request, HTTPError, URLError, + AbstractHTTPHandler, ProxyHandler, + HTTPDefaultErrorHandler, HTTPRedirectHandler, + HTTPErrorProcessor, OpenerDirector) except ImportError: - from urllib.request import urlopen, Request, HTTPError, URLError + from urllib.request import (urlopen, Request, HTTPError, URLError, + AbstractHTTPHandler, ProxyHandler, + HTTPDefaultErrorHandler, HTTPRedirectHandler, + HTTPErrorProcessor, OpenerDirector) try: from httplib import HTTPConnection @@ -320,6 +322,165 @@ class SpeedtestBestServerFailure(SpeedtestException): """Unable to determine best server""" +def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, + source_address=None): + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + err = None + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket.socket(af, socktype, proto) + if timeout is not _GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(float(timeout)) + if source_address: + sock.bind(source_address) + sock.connect(sa) + return sock + + except socket.error: + err = get_exception() + if sock is not None: + sock.close() + + if err is not None: + raise err + else: + raise socket.error("getaddrinfo returns an empty list") + + +class SpeedtestHTTPConnection(HTTPConnection): + def __init__(self, *args, **kwargs): + source_address = kwargs.pop('source_address', None) + context = kwargs.pop('context', None) + timeout = kwargs.pop('timeout', 10) + + HTTPConnection.__init__(self, *args, **kwargs) + + self.source_address = source_address + self._context = context + self.timeout = timeout + + try: + self._create_connection = socket.create_connection + except AttributeError: + self._create_connection = create_connection + + def connect(self): + """Connect to the host and port specified in __init__.""" + self.sock = self._create_connection( + (self.host, self.port), + self.timeout, + self.source_address + ) + + +if HTTPSConnection: + class SpeedtestHTTPSConnection(HTTPSConnection, + SpeedtestHTTPConnection): + def connect(self): + "Connect to a host on a given (SSL) port." + + SpeedtestHTTPConnection.connect(self) + + kwargs = {} + if hasattr(ssl, 'SSLContext'): + kwargs['server_hostname'] = self.host + + self.sock = self._context.wrap_socket(self.sock, **kwargs) + + +def _build_connection(connection, source_address, timeout, context=None): + def inner(host, **kwargs): + kwargs.update({ + 'source_address': source_address, + 'timeout': timeout + }) + if context: + kwargs['context'] = context + return connection(host, **kwargs) + return inner + + +class SpeedtestHTTPHandler(AbstractHTTPHandler): + def __init__(self, debuglevel=0, source_address=None, timeout=10): + AbstractHTTPHandler.__init__(self, debuglevel) + self.source_address = source_address + self.timeout = timeout + + def http_open(self, req): + return self.do_open( + _build_connection( + SpeedtestHTTPConnection, + self.source_address, + self.timeout + ), + req + ) + + http_request = AbstractHTTPHandler.do_request_ + + +class SpeedtestHTTPSHandler(AbstractHTTPHandler): + def __init__(self, debuglevel=0, context=None, source_address=None, + timeout=10): + AbstractHTTPHandler.__init__(self, debuglevel) + self._context = context + self.source_address = source_address + self.timeout = timeout + + def https_open(self, req): + return self.do_open( + _build_connection( + SpeedtestHTTPSConnection, + self.source_address, + timeout, + context=self._context, + ), + req + ) + + https_request = AbstractHTTPHandler.do_request_ + + +def build_opener(source_address=None, timeout=10): + if source_address: + source_address_tuple = (source_address, 0) + else: + source_address_tuple = None + + handlers = [ + ProxyHandler(), + SpeedtestHTTPHandler(source_address=source_address_tuple, + timeout=timeout), + SpeedtestHTTPSHandler(source_address=source_address_tuple, + timeout=timeout), + HTTPDefaultErrorHandler(), + HTTPRedirectHandler(), + HTTPErrorProcessor() + ] + + opener = OpenerDirector() + opener.addheaders = [('User-agent', build_user_agent())] + + for handler in handlers: + opener.add_handler(handler) + + return opener + + class GzipDecodedResponse(GZIP_BASE): """A file-like object to decode a response encoded with the gzip method, as described in RFC 1952. @@ -357,14 +518,6 @@ def get_exception(): return sys.exc_info()[1] -def bound_socket(*args, **kwargs): - """Bind socket to a specified source IP address""" - - sock = SOCKET_SOCKET(*args, **kwargs) - sock.bind((SOURCE, 0)) - return sock - - def distance(origin, destination): """Determine distance between 2 sets of [lat,lon] in km""" @@ -387,10 +540,6 @@ def distance(origin, destination): def build_user_agent(): """Build a Mozilla/5.0 compatible User-Agent string""" - global USER_AGENT - if USER_AGENT: - return USER_AGENT - ua_tuple = ( 'Mozilla/5.0', '(%s; U; %s; en-us)' % (platform.system(), platform.architecture()[0]), @@ -398,9 +547,9 @@ def build_user_agent(): '(KHTML, like Gecko)', 'speedtest-cli/%s' % __version__ ) - USER_AGENT = ' '.join(ua_tuple) - printer(USER_AGENT, debug=True) - return USER_AGENT + user_agent = ' '.join(ua_tuple) + printer(user_agent, debug=True) + return user_agent def build_request(url, data=None, headers=None, bump=''): @@ -410,9 +559,6 @@ def build_request(url, data=None, headers=None, bump=''): """ - if not USER_AGENT: - build_user_agent() - if not headers: headers = {} @@ -432,7 +578,6 @@ def build_request(url, data=None, headers=None, bump=''): bump) headers.update({ - 'User-Agent': USER_AGENT, 'Cache-Control': 'no-cache', }) @@ -442,14 +587,19 @@ def build_request(url, data=None, headers=None, bump=''): return Request(final_url, data=data, headers=headers) -def catch_request(request): +def catch_request(request, opener=None): """Helper function to catch common exceptions encountered when establishing a connection with a HTTP/HTTPS request """ + if opener: + _open = opener.open + else: + _open = urlopen + try: - uh = urlopen(request) + uh = _open(request) return uh, False except HTTP_ERRORS: e = get_exception() @@ -505,18 +655,22 @@ def do_nothing(*args, **kwargs): class HTTPDownloader(threading.Thread): """Thread class for retrieving a URL""" - def __init__(self, i, request, start, timeout): + def __init__(self, i, request, start, timeout, opener=None): threading.Thread.__init__(self) self.request = request self.result = [0] self.starttime = start self.timeout = timeout self.i = i + if opener: + self._opener = opener.open + else: + self._opener = urlopen def run(self): try: if (timeit.default_timer() - self.starttime) <= self.timeout: - f = urlopen(self.request) + f = self._opener(self.request) while (not SHUTDOWN_EVENT.isSet() and (timeit.default_timer() - self.starttime) <= self.timeout): @@ -574,7 +728,7 @@ class HTTPUploaderData(object): class HTTPUploader(threading.Thread): """Thread class for putting a URL""" - def __init__(self, i, request, start, size, timeout): + def __init__(self, i, request, start, size, timeout, opener=None): threading.Thread.__init__(self) self.request = request self.request.data.start = self.starttime = start @@ -583,20 +737,25 @@ class HTTPUploader(threading.Thread): self.timeout = timeout self.i = i + if opener: + self._opener = opener.open + else: + self._opener = urlopen + def run(self): request = self.request try: if ((timeit.default_timer() - self.starttime) <= self.timeout and not SHUTDOWN_EVENT.isSet()): try: - f = urlopen(request) + f = self._opener(request) except TypeError: # PY24 expects a string or buffer # This also causes issues with Ctrl-C, but we will concede # for the moment that Ctrl-C on PY24 isn't immediate request = build_request(self.request.get_full_url(), data=request.data.read(self.size)) - f = urlopen(request) + f = self._opener(request) f.read(11) f.close() self.result = sum(self.request.data.total) @@ -619,7 +778,7 @@ class SpeedtestResults(object): to get a share results image link. """ - def __init__(self, download=0, upload=0, ping=0, server=None): + def __init__(self, download=0, upload=0, ping=0, server=None, opener=None): self.download = download self.upload = upload self.ping = ping @@ -632,6 +791,11 @@ class SpeedtestResults(object): self.bytes_received = 0 self.bytes_sent = 0 + if opener: + self._opener = opener + else: + self._opener = build_opener() + def __repr__(self): return repr(self.dict()) @@ -674,7 +838,7 @@ class SpeedtestResults(object): request = build_request('://www.speedtest.net/api/api.php', data='&'.join(api_data).encode(), headers=headers) - f, e = catch_request(request) + f, e = catch_request(request, opener=_self.opener) if e: raise ShareResultsConnectFailure(e) @@ -738,8 +902,13 @@ class SpeedtestResults(object): class Speedtest(object): """Class for performing standard speedtest.net testing operations""" - def __init__(self, config=None): + def __init__(self, config=None, source_address=None, timeout=10): self.config = {} + + self._source_address = source_address + self._timeout = timeout + self._opener = build_opener(source_address, timeout) + self.get_config() if config is not None: self.config.update(config) @@ -748,7 +917,7 @@ class Speedtest(object): self.closest = [] self.best = {} - self.results = SpeedtestResults() + self.results = SpeedtestResults(opener=self._opener) def get_config(self): """Download the speedtest.net configuration and return only the data @@ -760,7 +929,7 @@ class Speedtest(object): headers['Accept-Encoding'] = 'gzip' request = build_request('://www.speedtest.net/speedtest-config.php', headers=headers) - uh, e = catch_request(request) + uh, e = catch_request(request, opener=self._opener) if e: raise ConfigRetrievalError(e) configxml = [] @@ -877,7 +1046,7 @@ class Speedtest(object): (url, self.config['threads']['download']), headers=headers) - uh, e = catch_request(request) + uh, e = catch_request(request, opener=self._opener) if e: errors.append('%s' % e) raise ServersRetrievalError() @@ -960,7 +1129,7 @@ class Speedtest(object): url = server request = build_request(url) - uh, e = catch_request(request) + uh, e = catch_request(request, opener=self._opener) if e: raise SpeedtestMiniConnectFailure('Failed to connect to %s' % server) @@ -973,7 +1142,9 @@ class Speedtest(object): if not extension: for ext in ['php', 'asp', 'aspx', 'jsp']: try: - f = urlopen('%s/speedtest/upload.%s' % (url, ext)) + f = self._opener.open( + '%s/speedtest/upload.%s' % (url, ext) + ) except: pass else: @@ -1028,6 +1199,13 @@ class Speedtest(object): servers = self.get_closest_servers() servers = self.closest + if self._source_address: + source_address_tuple = (self._source_address, 0) + else: + source_address_tuple = None + + user_agent = build_user_agent() + results = {} for server in servers: cum = [] @@ -1037,10 +1215,16 @@ class Speedtest(object): for _ in range(0, 3): try: if urlparts[0] == 'https': - h = HTTPSConnection(urlparts[1]) + h = SpeedtestHTTPSConnection( + urlparts[1], + source_address=source_address_tuple + ) else: - h = HTTPConnection(urlparts[1]) - headers = {'User-Agent': USER_AGENT} + h = SpeedtestHTTPConnection( + urlparts[1], + source_address=source_address_tuple + ) + headers = {'User-Agent': user_agent} start = timeit.default_timer() h.request("GET", urlparts[2], headers=headers) r = h.getresponse() @@ -1093,7 +1277,8 @@ class Speedtest(object): def producer(q, requests, request_count): for i, request in enumerate(requests): thread = HTTPDownloader(i, request, start, - self.config['length']['download']) + self.config['length']['download'], + opener=self._opener) thread.start() q.put(thread, True) callback(i, request_count, start=True) @@ -1159,7 +1344,8 @@ class Speedtest(object): def producer(q, requests, request_count): for i, request in enumerate(requests[:request_count]): thread = HTTPUploader(i, request[0], start, request[1], - self.config['length']['upload']) + self.config['length']['upload'], + opener=self._opener) thread.start() q.put(thread, True) callback(i, request_count, start=True) @@ -1338,7 +1524,7 @@ def printer(string, quiet=False, debug=False, **kwargs): def shell(): """Run the full speedtest.net test""" - global SHUTDOWN_EVENT, SOURCE, SCHEME, DEBUG + global SHUTDOWN_EVENT, SCHEME, DEBUG SHUTDOWN_EVENT = threading.Event() signal.signal(signal.SIGINT, ctrl_c) @@ -1361,13 +1547,6 @@ def shell(): validate_optional_args(args) - socket.setdefaulttimeout(args.timeout) - - # If specified bind to a specific IP address - if args.source: - SOURCE = args.source - socket.socket = bound_socket - if args.secure: SCHEME = 'https' @@ -1377,9 +1556,6 @@ def shell(): if debug: DEBUG = True - # Pre-cache the user agent string - build_user_agent() - if args.simple or args.csv or args.json: quiet = True else: @@ -1398,15 +1574,15 @@ def shell(): printer('Retrieving speedtest.net configuration...', quiet) try: - speedtest = Speedtest() - except (ConfigRetrievalError, HTTP_ERRORS): + speedtest = Speedtest(source_address=args.source, timeout=args.timeout) + except (ConfigRetrievalError,) + HTTP_ERRORS: printer('Cannot retrieve speedtest configuration') raise SpeedtestCLIError(get_exception()) if args.list: try: speedtest.get_servers() - except (ServersRetrievalError, HTTP_ERRORS): + except (ServersRetrievalError,) + HTTP_ERRORS: print_('Cannot retrieve speedtest server list') raise SpeedtestCLIError(get_exception()) @@ -1436,7 +1612,7 @@ def shell(): speedtest.get_servers(servers) except NoMatchedServers: raise SpeedtestCLIError('No matched servers: %s' % args.server) - except (ServersRetrievalError, HTTP_ERRORS): + except (ServersRetrievalError,) + HTTP_ERRORS: print_('Cannot retrieve speedtest server list') raise SpeedtestCLIError(get_exception()) except InvalidServerIDType: From fe864f6dce881c4592a952df8db59745eff3fd7d Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 May 2017 11:08:22 -0500 Subject: [PATCH 02/67] Use vendored create_connection when socket doesn't have it, or socket.create_connection is too old --- speedtest.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/speedtest.py b/speedtest.py index 115b2f8..ad7dbb7 100755 --- a/speedtest.py +++ b/speedtest.py @@ -373,18 +373,20 @@ class SpeedtestHTTPConnection(HTTPConnection): self._context = context self.timeout = timeout - try: - self._create_connection = socket.create_connection - except AttributeError: - self._create_connection = create_connection - def connect(self): """Connect to the host and port specified in __init__.""" - self.sock = self._create_connection( - (self.host, self.port), - self.timeout, - self.source_address - ) + try: + self.sock = socket.create_connection( + (self.host, self.port), + self.timeout, + self.source_address + ) + except (AttributeError, TypeError): + self.sock = create_connection( + (self.host, self.port), + self.timeout, + self.source_address + ) if HTTPSConnection: From b33c7533dfa2a0a742da471fa7787db98aa0c581 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 May 2017 11:08:32 -0500 Subject: [PATCH 03/67] flake8 fixes --- speedtest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/speedtest.py b/speedtest.py index ad7dbb7..0053237 100755 --- a/speedtest.py +++ b/speedtest.py @@ -448,7 +448,7 @@ class SpeedtestHTTPSHandler(AbstractHTTPHandler): _build_connection( SpeedtestHTTPSConnection, self.source_address, - timeout, + self.timeout, context=self._context, ), req @@ -840,7 +840,7 @@ class SpeedtestResults(object): request = build_request('://www.speedtest.net/api/api.php', data='&'.join(api_data).encode(), headers=headers) - f, e = catch_request(request, opener=_self.opener) + f, e = catch_request(request, opener=self._opener) if e: raise ShareResultsConnectFailure(e) From 2fe369fdf8795a8c5a979e6e119a921e5d33935f Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 May 2017 12:29:54 -0500 Subject: [PATCH 04/67] Remove SCHEME global --- speedtest.py | 50 +++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/speedtest.py b/speedtest.py index 0053237..878edb7 100755 --- a/speedtest.py +++ b/speedtest.py @@ -52,7 +52,6 @@ class FakeShutdownEvent(object): # Some global variables we use SHUTDOWN_EVENT = FakeShutdownEvent() -SCHEME = 'http' DEBUG = False _GLOBAL_DEFAULT_TIMEOUT = object() @@ -554,7 +553,7 @@ def build_user_agent(): return user_agent -def build_request(url, data=None, headers=None, bump=''): +def build_request(url, data=None, headers=None, bump='', secure=False): """Build a urllib2 request object This function automatically adds a User-Agent header to all requests @@ -565,7 +564,9 @@ def build_request(url, data=None, headers=None, bump=''): headers = {} if url[0] == ':': - schemed_url = '%s%s' % (SCHEME, url) + scheme = ('http', 'https')[bool(secure)] + print scheme + schemed_url = '%s%s' % (scheme, url) else: schemed_url = url @@ -780,7 +781,8 @@ class SpeedtestResults(object): to get a share results image link. """ - def __init__(self, download=0, upload=0, ping=0, server=None, opener=None): + def __init__(self, download=0, upload=0, ping=0, server=None, opener=None, + secure=False): self.download = download self.upload = upload self.ping = ping @@ -798,6 +800,8 @@ class SpeedtestResults(object): else: self._opener = build_opener() + self._secure = secure + def __repr__(self): return repr(self.dict()) @@ -839,7 +843,7 @@ class SpeedtestResults(object): headers = {'Referer': 'http://c.speedtest.net/flash/speedtest.swf'} request = build_request('://www.speedtest.net/api/api.php', data='&'.join(api_data).encode(), - headers=headers) + headers=headers, secure=self._secure) f, e = catch_request(request, opener=self._opener) if e: raise ShareResultsConnectFailure(e) @@ -904,13 +908,16 @@ class SpeedtestResults(object): class Speedtest(object): """Class for performing standard speedtest.net testing operations""" - def __init__(self, config=None, source_address=None, timeout=10): + def __init__(self, config=None, source_address=None, timeout=10, + secure=False): self.config = {} self._source_address = source_address self._timeout = timeout self._opener = build_opener(source_address, timeout) + self._secure = secure + self.get_config() if config is not None: self.config.update(config) @@ -919,7 +926,7 @@ class Speedtest(object): self.closest = [] self.best = {} - self.results = SpeedtestResults(opener=self._opener) + self.results = SpeedtestResults(opener=self._opener, secure=secure) def get_config(self): """Download the speedtest.net configuration and return only the data @@ -930,7 +937,7 @@ class Speedtest(object): if gzip: headers['Accept-Encoding'] = 'gzip' request = build_request('://www.speedtest.net/speedtest-config.php', - headers=headers) + headers=headers, secure=self._secure) uh, e = catch_request(request, opener=self._opener) if e: raise ConfigRetrievalError(e) @@ -1044,10 +1051,12 @@ class Speedtest(object): errors = [] for url in urls: try: - request = build_request('%s?threads=%s' % - (url, - self.config['threads']['download']), - headers=headers) + request = build_request( + '%s?threads=%s' % (url, + self.config['threads']['download']), + headers=headers, + secure=self._secure + ) uh, e = catch_request(request, opener=self._opener) if e: errors.append('%s' % e) @@ -1274,7 +1283,9 @@ class Speedtest(object): request_count = len(urls) requests = [] for i, url in enumerate(urls): - requests.append(build_request(url, bump=i)) + requests.append( + build_request(url, bump=i, secure=self._secure) + ) def producer(q, requests, request_count): for i, request in enumerate(requests): @@ -1338,7 +1349,7 @@ class Speedtest(object): data.pre_allocate() requests.append( ( - build_request(self.best['url'], data), + build_request(self.best['url'], data, secure=self._secure), size ) ) @@ -1526,7 +1537,7 @@ def printer(string, quiet=False, debug=False, **kwargs): def shell(): """Run the full speedtest.net test""" - global SHUTDOWN_EVENT, SCHEME, DEBUG + global SHUTDOWN_EVENT, DEBUG SHUTDOWN_EVENT = threading.Event() signal.signal(signal.SIGINT, ctrl_c) @@ -1549,9 +1560,6 @@ def shell(): validate_optional_args(args) - if args.secure: - SCHEME = 'https' - debug = getattr(args, 'debug', False) if debug == 'SUPPRESSHELP': debug = False @@ -1576,7 +1584,11 @@ def shell(): printer('Retrieving speedtest.net configuration...', quiet) try: - speedtest = Speedtest(source_address=args.source, timeout=args.timeout) + speedtest = Speedtest( + source_address=args.source, + timeout=args.timeout, + secure=args.secure + ) except (ConfigRetrievalError,) + HTTP_ERRORS: printer('Cannot retrieve speedtest configuration') raise SpeedtestCLIError(get_exception()) From 0e585cbf644a0b7d07b6a891dbfbf96ad2474501 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 May 2017 12:38:33 -0500 Subject: [PATCH 05/67] Docstrings and version bump --- speedtest.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 878edb7..a7eb132 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '1.0.7' +__version__ = '2.0.0a' class FakeShutdownEvent(object): @@ -333,6 +333,8 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, is used. If *source_address* is set it must be a tuple of (host, port) for the socket to bind as a source address before making the connection. An host of '' or port 0 tells the OS to use the default. + + Largely vendored from Python 2.7, modified to work with Python 2.4 """ host, port = address @@ -361,6 +363,9 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, class SpeedtestHTTPConnection(HTTPConnection): + """Custom HTTPConnection to support source_address across + Python 2.4 - Python 3 + """ def __init__(self, *args, **kwargs): source_address = kwargs.pop('source_address', None) context = kwargs.pop('context', None) @@ -391,6 +396,9 @@ class SpeedtestHTTPConnection(HTTPConnection): if HTTPSConnection: class SpeedtestHTTPSConnection(HTTPSConnection, SpeedtestHTTPConnection): + """Custom HTTPSConnection to support source_address across + Python 2.4 - Python 3 + """ def connect(self): "Connect to a host on a given (SSL) port." @@ -404,6 +412,12 @@ if HTTPSConnection: def _build_connection(connection, source_address, timeout, context=None): + """Cross Python 2.4 - Python 3 callable to build an ``HTTPConnection`` or + ``HTTPSConnection`` with the args we need + + Called from ``http(s)_open`` methods of ``SpeedtestHTTPHandler`` or + ``SpeedtestHTTPSHandler`` + """ def inner(host, **kwargs): kwargs.update({ 'source_address': source_address, @@ -416,6 +430,9 @@ def _build_connection(connection, source_address, timeout, context=None): class SpeedtestHTTPHandler(AbstractHTTPHandler): + """Custom ``HTTPHandler`` that can build a ``HTTPConnection`` with the + args we need for ``source_address`` and ``timeout`` + """ def __init__(self, debuglevel=0, source_address=None, timeout=10): AbstractHTTPHandler.__init__(self, debuglevel) self.source_address = source_address @@ -435,6 +452,9 @@ class SpeedtestHTTPHandler(AbstractHTTPHandler): class SpeedtestHTTPSHandler(AbstractHTTPHandler): + """Custom ``HTTPSHandler`` that can build a ``HTTPSConnection`` with the + args we need for ``source_address`` and ``timeout`` + """ def __init__(self, debuglevel=0, context=None, source_address=None, timeout=10): AbstractHTTPHandler.__init__(self, debuglevel) @@ -457,6 +477,12 @@ class SpeedtestHTTPSHandler(AbstractHTTPHandler): def build_opener(source_address=None, timeout=10): + """Function similar to ``urllib2.build_opener`` that will build + an ``OpenerDirector`` with the explicit handlers we want, + ``source_address`` for binding, ``timeout`` and our custom + `User-Agent` + """ + if source_address: source_address_tuple = (source_address, 0) else: From 2fe34ecf4ef6055677a2163fdbd8d9797ec2c3e1 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 May 2017 12:51:26 -0500 Subject: [PATCH 06/67] Remove debug print --- speedtest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index a7eb132..d20a80d 100755 --- a/speedtest.py +++ b/speedtest.py @@ -591,7 +591,6 @@ def build_request(url, data=None, headers=None, bump='', secure=False): if url[0] == ':': scheme = ('http', 'https')[bool(secure)] - print scheme schemed_url = '%s%s' % (scheme, url) else: schemed_url = url From 6556be190aa0d2bf45f181b2101ec2c453d796ad Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 3 May 2017 10:44:46 -0500 Subject: [PATCH 07/67] Switch to using matrix for travis --- .travis.yml | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8863201..12b7fa3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,5 @@ language: python -python: - - 2.7 - addons: apt: sources: @@ -11,19 +8,31 @@ addons: - python2.4 - python2.5 - python2.6 - - pypy -env: - - TOXENV=py24 - - TOXENV=py25 - - TOXENV=py26 - - TOXENV=py27 - - TOXENV=py32 - - TOXENV=py33 - - TOXENV=py34 - - TOXENV=py35 - - TOXENV=pypy - - TOXENV=flake8 +matrix: + include: + - python: 2.7 + env: TOXENV=flake8 + - python: 2.7 + env: TOXENV=py24 + - python: 2.7 + env: TOXENV=py25 + - python: 2.7 + env: TOXENV=py26 + - python: 2.7 + env: TOXENV=py27 + - python: 3.2 + env: TOXENV=py32 + - python: 3.3 + env: TOXENV=py33 + - python: 3.4 + env: TOXENV=py34 + - python: 3.5 + env: TOXENV=py35 + - python: 3.6 + env: TOXENV=py36 + - python: pypy + env: TOXENV=pypy install: - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") != 0 ]]; then pip install virtualenv==1.7.2 tox==1.3; fi; From 65316773461efc5fea2d555da2bf0985528e9d25 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 3 May 2017 10:56:54 -0500 Subject: [PATCH 08/67] Test failing --source --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 477fa1c..e49332d 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ commands = {envpython} -V {envpython} -m compileall speedtest.py {envpython} speedtest.py + sh -c "{envpython} speedtest.py --source 127.0.0.1 || (( $? == 1 ))" [testenv:flake8] basepython=python @@ -19,3 +20,4 @@ commands = pypy -V pypy -m compileall speedtest.py pypy speedtest.py + sh -c "pypy speedtest.py --source 127.0.0.1 || (( $? == 1 ))" From f2a97baf1e0700a8f70cd851843ea81a6997e897 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 3 May 2017 11:02:35 -0500 Subject: [PATCH 09/67] Revert "Test failing --source" This reverts commit be7d7f6a1c2448cc60342142a405945817817115. --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index e49332d..477fa1c 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,6 @@ commands = {envpython} -V {envpython} -m compileall speedtest.py {envpython} speedtest.py - sh -c "{envpython} speedtest.py --source 127.0.0.1 || (( $? == 1 ))" [testenv:flake8] basepython=python @@ -20,4 +19,3 @@ commands = pypy -V pypy -m compileall speedtest.py pypy speedtest.py - sh -c "pypy speedtest.py --source 127.0.0.1 || (( $? == 1 ))" From 8854d82049babba9c5216e74723c87a5be996c43 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 3 May 2017 17:17:00 -0500 Subject: [PATCH 10/67] More and better debugging --- speedtest.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/speedtest.py b/speedtest.py index d20a80d..6419078 100755 --- a/speedtest.py +++ b/speedtest.py @@ -483,8 +483,12 @@ def build_opener(source_address=None, timeout=10): `User-Agent` """ + printer('Timeout set to %d' % timeout, debug=True) + if source_address: source_address_tuple = (source_address, 0) + printer('Binding to source address: %r' % (source_address_tuple,), + debug=True) else: source_address_tuple = None @@ -575,11 +579,11 @@ def build_user_agent(): 'speedtest-cli/%s' % __version__ ) user_agent = ' '.join(ua_tuple) - printer(user_agent, debug=True) + printer('User-Agent: %s' % user_agent, debug=True) return user_agent -def build_request(url, data=None, headers=None, bump='', secure=False): +def build_request(url, data=None, headers=None, bump='0', secure=False): """Build a urllib2 request object This function automatically adds a User-Agent header to all requests @@ -980,7 +984,7 @@ class Speedtest(object): if int(uh.code) != 200: return None - printer(''.encode().join(configxml), debug=True) + printer('Config XML:\n%s' % ''.encode().join(configxml), debug=True) try: root = ET.fromstring(''.encode().join(configxml)) @@ -1042,7 +1046,7 @@ class Speedtest(object): self.lat_lon = (float(client['lat']), float(client['lon'])) - printer(self.config, debug=True) + printer('Config:\n%r' % self.config, debug=True) return self.config @@ -1101,7 +1105,8 @@ class Speedtest(object): if int(uh.code) != 200: raise ServersRetrievalError() - printer(''.encode().join(serversxml), debug=True) + printer('Servers XML:\n%s' % ''.encode().join(serversxml), + debug=True) try: try: @@ -1139,8 +1144,6 @@ class Speedtest(object): except KeyError: self.servers[d] = [attrib] - printer(''.encode().join(serversxml), debug=True) - break except ServersRetrievalError: @@ -1222,7 +1225,7 @@ class Speedtest(object): continue break - printer(self.closest, debug=True) + printer('Closest Servers:\n%r' % self.closest, debug=True) return self.closest def get_best_server(self, servers=None): @@ -1246,9 +1249,13 @@ class Speedtest(object): for server in servers: cum = [] url = os.path.dirname(server['url']) - urlparts = urlparse('%s/latency.txt' % url) - printer('%s %s/latency.txt' % ('GET', url), debug=True) - for _ in range(0, 3): + stamp = int(timeit.time.time() * 1000) + latency_url = '%s/latency.txt?x=%s' % (url, stamp) + for i in range(0, 3): + this_latency_url = '%s.%s' % (latency_url, i) + printer('%s %s' % ('GET', this_latency_url), + debug=True) + urlparts = urlparse(latency_url) try: if urlparts[0] == 'https': h = SpeedtestHTTPSConnection( @@ -1261,13 +1268,14 @@ class Speedtest(object): source_address=source_address_tuple ) headers = {'User-Agent': user_agent} + path = '%s?%s' % (urlparts[2], urlparts[4]) start = timeit.default_timer() - h.request("GET", urlparts[2], headers=headers) + h.request("GET", path, headers=headers) r = h.getresponse() total = (timeit.default_timer() - start) except HTTP_ERRORS: e = get_exception() - printer('%r' % e, debug=True) + printer('ERROR: %r' % e, debug=True) cum.append(3600) continue @@ -1293,7 +1301,7 @@ class Speedtest(object): self.results.server = best self.best.update(best) - printer(best, debug=True) + printer('Best Server:\n%r' % best, debug=True) return best def download(self, callback=do_nothing): @@ -1690,6 +1698,8 @@ def shell(): else: printer('Skipping upload test') + printer('Results:\n%r' % results.dict(), debug=True) + if args.simple: print_('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' % (results.ping, From 3ebb9734a2e6211d60e1e52fe2f1fdd53bfedb46 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 4 May 2017 13:55:01 -0500 Subject: [PATCH 11/67] Indicate speedtest-cli supports python 3.6, and ensure py3.2 has an appropriate setuptools version --- .travis.yml | 7 ++++--- README.rst | 2 +- setup.py | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 12b7fa3..406754d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,11 +35,12 @@ matrix: env: TOXENV=pypy install: - - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") != 0 ]]; then pip install virtualenv==1.7.2 tox==1.3; fi; - - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") == 0 ]]; then pip install tox; fi; + - if [[ $(echo "$TOXENV" | egrep -c "py32") != 0 ]]; then pip install setuptools==17.1.1; fi + - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") != 0 ]]; then pip install virtualenv==1.7.2 tox==1.3; fi; + - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") == 0 ]]; then pip install tox; fi; script: - - tox + - tox notifications: email: diff --git a/README.rst b/README.rst index 0043b5c..5af9aad 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ speedtest.net Versions -------- -speedtest-cli works with Python 2.4-3.5 +speedtest-cli works with Python 2.4-3.6 .. image:: https://img.shields.io/pypi/pyversions/speedtest-cli.svg :target: https://pypi.python.org/pypi/speedtest-cli/ diff --git a/setup.py b/setup.py index 00a8054..f323f81 100644 --- a/setup.py +++ b/setup.py @@ -90,5 +90,6 @@ setup( 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ] ) From ca72d40033fffc2aa056b2c4f7103c6eb51e5a06 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 12 May 2017 13:01:59 -0500 Subject: [PATCH 12/67] Create a getter for Speedtest.best to raise an exception is get_best_server has not found a best server --- speedtest.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/speedtest.py b/speedtest.py index 6419078..a586b19 100755 --- a/speedtest.py +++ b/speedtest.py @@ -321,6 +321,10 @@ class SpeedtestBestServerFailure(SpeedtestException): """Unable to determine best server""" +class SpeedtestMissingBestServer(SpeedtestException): + """get_best_server not called or not able to determine best server""" + + def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None): """Connect to *address* and return the socket object. @@ -953,10 +957,19 @@ class Speedtest(object): self.servers = {} self.closest = [] - self.best = {} + self._best = {} self.results = SpeedtestResults(opener=self._opener, secure=secure) + @property + def best(self): + if not self._best: + raise SpeedtestMissingBestServer( + 'get_best_server not called or not able to determine best ' + 'server' + ) + return self._best + def get_config(self): """Download the speedtest.net configuration and return only the data we are interested in @@ -1300,7 +1313,7 @@ class Speedtest(object): self.results.ping = fastest self.results.server = best - self.best.update(best) + self._best.update(best) printer('Best Server:\n%r' % best, debug=True) return best From 6bfa5922c301402a5d11da1cc47e1d99a0367d7f Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 12 May 2017 13:02:35 -0500 Subject: [PATCH 13/67] Add option to exclude servers, and allow --server and --exclude to be specified multiple times --- speedtest.py | 49 +++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/speedtest.py b/speedtest.py index a586b19..9dc8737 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1063,21 +1063,26 @@ class Speedtest(object): return self.config - def get_servers(self, servers=None): + def get_servers(self, servers=None, exclude=None): """Retrieve a the list of speedtest.net servers, optionally filtered to servers matching those specified in the ``servers`` argument """ if servers is None: servers = [] + if exclude is None: + exclude = [] + self.servers.clear() - for i, s in enumerate(servers): - try: - servers[i] = int(s) - except ValueError: - raise InvalidServerIDType('%s is an invalid server type, must ' - 'be int' % s) + for server_list in (servers, exclude): + for i, s in enumerate(server_list): + try: + server_list[i] = int(s) + except ValueError: + raise InvalidServerIDType( + '%s is an invalid server type, must be int' % s + ) urls = [ '://www.speedtest.net/speedtest-servers-static.php', @@ -1140,7 +1145,8 @@ class Speedtest(object): if servers and int(attrib.get('id')) not in servers: continue - if int(attrib.get('id')) in self.config['ignore_servers']: + if (int(attrib.get('id')) in self.config['ignore_servers'] + or int(attrib.get('id')) in exclude): continue try: @@ -1162,7 +1168,7 @@ class Speedtest(object): except ServersRetrievalError: continue - if servers and not self.servers: + if (servers or exclude) and not self.servers: raise NoMatchedServers() return self.servers @@ -1518,8 +1524,11 @@ def parse_args(): parser.add_argument('--list', action='store_true', help='Display a list of speedtest.net servers ' 'sorted by distance') - parser.add_argument('--server', help='Specify a server ID to test against', - type=PARSER_TYPE_INT) + parser.add_argument('--server', type=PARSER_TYPE_INT, action='append', + help='Specify a server ID to test against') + parser.add_argument('--exclude', type=PARSER_TYPE_INT, action='append', + help='Exclude a server from selection. Can be ' + 'supplied multiple times') parser.add_argument('--mini', help='URL of the Speedtest Mini server') parser.add_argument('--source', help='Source IP address to bind to') parser.add_argument('--timeout', default=10, type=PARSER_TYPE_INT, @@ -1658,26 +1667,26 @@ def shell(): raise sys.exit(0) - # Set a filter of servers to retrieve - servers = [] - if args.server: - servers.append(args.server) - printer('Testing from %(isp)s (%(ip)s)...' % speedtest.config['client'], quiet) if not args.mini: printer('Retrieving speedtest.net server list...', quiet) try: - speedtest.get_servers(servers) + speedtest.get_servers(servers=args.server, exclude=args.exclude) except NoMatchedServers: - raise SpeedtestCLIError('No matched servers: %s' % args.server) + raise SpeedtestCLIError( + 'No matched servers: %s' % + ', '.join('%s' % s for s in args.server) + ) except (ServersRetrievalError,) + HTTP_ERRORS: print_('Cannot retrieve speedtest server list') raise SpeedtestCLIError(get_exception()) except InvalidServerIDType: - raise SpeedtestCLIError('%s is an invalid server type, must ' - 'be an int' % args.server) + raise SpeedtestCLIError( + '%s is an invalid server type, must ' + 'be an int' % ', '.join('%s' % s for s in args.server) + ) printer('Selecting best server based on ping...', quiet) speedtest.get_best_server() From f3a607feb20ed534de07f79262e98e937b9d24bc Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 12 May 2017 14:55:23 -0500 Subject: [PATCH 14/67] Allow timeout to be a float --- speedtest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 9dc8737..d467902 100755 --- a/speedtest.py +++ b/speedtest.py @@ -125,11 +125,13 @@ try: from argparse import SUPPRESS as ARG_SUPPRESS PARSER_TYPE_INT = int PARSER_TYPE_STR = str + PARSER_TYPE_FLOAT = float except ImportError: from optparse import OptionParser as ArgParser from optparse import SUPPRESS_HELP as ARG_SUPPRESS PARSER_TYPE_INT = 'int' PARSER_TYPE_STR = 'string' + PARSER_TYPE_FLOAT = 'float' try: from cStringIO import StringIO @@ -1531,7 +1533,7 @@ def parse_args(): 'supplied multiple times') parser.add_argument('--mini', help='URL of the Speedtest Mini server') parser.add_argument('--source', help='Source IP address to bind to') - parser.add_argument('--timeout', default=10, type=PARSER_TYPE_INT, + parser.add_argument('--timeout', default=10, type=PARSER_TYPE_FLOAT, help='HTTP timeout in seconds. Default 10') parser.add_argument('--secure', action='store_true', help='Use HTTPS instead of HTTP when communicating ' From d9642b2047913152fe4d5c3bee6f20216cd7f813 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 2 Jun 2017 09:56:45 -0500 Subject: [PATCH 15/67] Always flush in py2 print_ --- speedtest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index d467902..6ea6103 100755 --- a/speedtest.py +++ b/speedtest.py @@ -191,7 +191,7 @@ else: Taken from https://pypi.python.org/pypi/six/ - Modified to set encoding to UTF-8 always + Modified to set encoding to UTF-8 always, and to flush after write """ fp = kwargs.pop("file", sys.stdout) if fp is None: @@ -210,6 +210,7 @@ else: errors = "strict" data = data.encode(encoding, errors) fp.write(data) + fp.flush() want_unicode = False sep = kwargs.pop("sep", None) if sep is not None: From 16054cc3bc4cbb389b35f4b6ef74c2c7cf5a80aa Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 16 Oct 2017 09:26:28 -0500 Subject: [PATCH 16/67] Print errors to stderr --- speedtest.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/speedtest.py b/speedtest.py index 6ea6103..7fa6590 100755 --- a/speedtest.py +++ b/speedtest.py @@ -149,24 +149,25 @@ except ImportError: import builtins from io import TextIOWrapper, FileIO - class _Py3Utf8Stdout(TextIOWrapper): + class _Py3Utf8Output(TextIOWrapper): """UTF-8 encoded wrapper around stdout for py3, to override ASCII stdout """ - def __init__(self, **kwargs): - buf = FileIO(sys.stdout.fileno(), 'w') - super(_Py3Utf8Stdout, self).__init__( + def __init__(self, f, **kwargs): + buf = FileIO(f.fileno(), 'w') + super(_Py3Utf8Output, self).__init__( buf, encoding='utf8', errors='strict' ) def write(self, s): - super(_Py3Utf8Stdout, self).write(s) + super(_Py3Utf8Output, self).write(s) self.flush() _py3_print = getattr(builtins, 'print') - _py3_utf8_stdout = _Py3Utf8Stdout() + _py3_utf8_stdout = _Py3Utf8Output(sys.stdout) + _py3_utf8_stderr = _Py3Utf8Output(sys.stderr) def to_utf8(v): """No-op encode to utf-8 for py3""" @@ -174,7 +175,10 @@ except ImportError: def print_(*args, **kwargs): """Wrapper function for py3 to print, with a utf-8 encoded stdout""" - kwargs['file'] = _py3_utf8_stdout + if kwargs.get('file') == sys.stderr: + kwargs['file'] = _py3_utf8_stderr + else: + kwargs['file'] = kwargs.get('file', _py3_utf8_stdout) _py3_print(*args, **kwargs) else: del __builtin__ @@ -1577,7 +1581,7 @@ def validate_optional_args(args): 'unavailable' % (info[0], arg)) -def printer(string, quiet=False, debug=False, **kwargs): +def printer(string, quiet=False, debug=False, error=False, **kwargs): """Helper function to print a string only when not quiet""" if debug and not DEBUG: @@ -1588,6 +1592,9 @@ def printer(string, quiet=False, debug=False, **kwargs): else: out = string + if error: + kwargs['file'] = sys.stderr + if not quiet: print_(out, **kwargs) @@ -1648,14 +1655,14 @@ def shell(): secure=args.secure ) except (ConfigRetrievalError,) + HTTP_ERRORS: - printer('Cannot retrieve speedtest configuration') + printer('Cannot retrieve speedtest configuration', error=True) raise SpeedtestCLIError(get_exception()) if args.list: try: speedtest.get_servers() except (ServersRetrievalError,) + HTTP_ERRORS: - print_('Cannot retrieve speedtest server list') + printer('Cannot retrieve speedtest server list', error=True) raise SpeedtestCLIError(get_exception()) for _, servers in sorted(speedtest.servers.items()): @@ -1683,7 +1690,7 @@ def shell(): ', '.join('%s' % s for s in args.server) ) except (ServersRetrievalError,) + HTTP_ERRORS: - print_('Cannot retrieve speedtest server list') + printer('Cannot retrieve speedtest server list', error=True) raise SpeedtestCLIError(get_exception()) except InvalidServerIDType: raise SpeedtestCLIError( From 3cb44f563023c3db164eba6d905a1c6617324ae1 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 16 Oct 2017 09:26:47 -0500 Subject: [PATCH 17/67] Attempt to catch MemoryError if possible --- speedtest.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/speedtest.py b/speedtest.py index 7fa6590..bbc376b 100755 --- a/speedtest.py +++ b/speedtest.py @@ -743,11 +743,17 @@ class HTTPUploaderData(object): chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' multiplier = int(round(int(self.length) / 36.0)) IO = BytesIO or StringIO - self._data = IO( - ('content1=%s' % - (chars * multiplier)[0:int(self.length) - 9] - ).encode() - ) + try: + self._data = IO( + ('content1=%s' % + (chars * multiplier)[0:int(self.length) - 9] + ).encode() + ) + except MemoryError: + raise SpeedtestCLIError( + 'Insufficient memory to pre-allocate upload data. Please ' + 'use --no-pre-allocate' + ) @property def data(self): From 5a9f82a20a1de8e384c837662c5216b8e921a731 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 23 Nov 2017 09:43:10 -0600 Subject: [PATCH 18/67] Add additional information to machine parsable outputs --- speedtest.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/speedtest.py b/speedtest.py index bbc376b..0abe01d 100755 --- a/speedtest.py +++ b/speedtest.py @@ -827,8 +827,8 @@ class SpeedtestResults(object): to get a share results image link. """ - def __init__(self, download=0, upload=0, ping=0, server=None, opener=None, - secure=False): + def __init__(self, download=0, upload=0, ping=0, server=None, client=None, + opener=None, secure=False): self.download = download self.upload = upload self.ping = ping @@ -836,6 +836,8 @@ class SpeedtestResults(object): self.server = {} else: self.server = server + self.client = client or {} + self._share = None self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat() self.bytes_received = 0 @@ -924,6 +926,7 @@ class SpeedtestResults(object): 'bytes_sent': self.bytes_sent, 'bytes_received': self.bytes_received, 'share': self._share, + 'client': self.client, } def csv(self, delimiter=','): @@ -935,7 +938,7 @@ class SpeedtestResults(object): row = [data['server']['id'], data['server']['sponsor'], data['server']['name'], data['timestamp'], data['server']['d'], data['ping'], data['download'], - data['upload']] + data['upload'], self._share or '', self.client['ip']] writer.writerow([to_utf8(v) for v in row]) return out.getvalue() @@ -972,7 +975,11 @@ class Speedtest(object): self.closest = [] self._best = {} - self.results = SpeedtestResults(opener=self._opener, secure=secure) + self.results = SpeedtestResults( + client=self.config['client'], + opener=self._opener, + secure=secure, + ) @property def best(self): @@ -1480,7 +1487,7 @@ def csv_header(delimiter=','): """Print the CSV Headers""" row = ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Distance', - 'Ping', 'Download', 'Upload'] + 'Ping', 'Download', 'Upload', 'Share', 'IP Address'] out = StringIO() writer = csv.writer(out, delimiter=delimiter, lineterminator='') writer.writerow([to_utf8(v) for v in row]) From b27f69d1ad11d207515a4d3208784a576070654b Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 23 Nov 2017 09:43:59 -0600 Subject: [PATCH 19/67] Output a different message when only 1 server is provided --- speedtest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/speedtest.py b/speedtest.py index 0abe01d..886c839 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1545,7 +1545,8 @@ def parse_args(): help='Display a list of speedtest.net servers ' 'sorted by distance') parser.add_argument('--server', type=PARSER_TYPE_INT, action='append', - help='Specify a server ID to test against') + help='Specify a server ID to test against. Can be ' + 'supplied multiple times') parser.add_argument('--exclude', type=PARSER_TYPE_INT, action='append', help='Exclude a server from selection. Can be ' 'supplied multiple times') @@ -1711,7 +1712,10 @@ def shell(): 'be an int' % ', '.join('%s' % s for s in args.server) ) - printer('Selecting best server based on ping...', quiet) + if args.server and len(args.server) == 1: + printer('Retrieving information for the selected server...', quiet) + else: + printer('Selecting best server based on ping...', quiet) speedtest.get_best_server() elif args.mini: speedtest.get_best_server(speedtest.set_mini_server(args.mini)) From 5c061da8e000c6aeca0494d8f674aca2e1ba475c Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 23 Nov 2017 09:46:55 -0600 Subject: [PATCH 21/67] Move the majority of the csv_header functionality to SpeedtestResults --- speedtest.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/speedtest.py b/speedtest.py index 886c839..5eb3841 100755 --- a/speedtest.py +++ b/speedtest.py @@ -929,6 +929,17 @@ class SpeedtestResults(object): 'client': self.client, } + @staticmethod + def csv_header(delimiter=','): + """Return CSV Headers""" + + row = ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Distance', + 'Ping', 'Download', 'Upload', 'Share', 'IP Address'] + out = StringIO() + writer = csv.writer(out, delimiter=delimiter, lineterminator='') + writer.writerow([to_utf8(v) for v in row]) + return out.getvalue() + def csv(self, delimiter=','): """Return data in CSV format""" @@ -1486,12 +1497,7 @@ def version(): def csv_header(delimiter=','): """Print the CSV Headers""" - row = ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Distance', - 'Ping', 'Download', 'Upload', 'Share', 'IP Address'] - out = StringIO() - writer = csv.writer(out, delimiter=delimiter, lineterminator='') - writer.writerow([to_utf8(v) for v in row]) - print_(out.getvalue()) + print_(SpeedtestResults.csv_header(delimiter=delimiter)) sys.exit(0) From f70cc86222bf425c71b292bf116abd7d67e33ee9 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 23 Nov 2017 09:49:26 -0600 Subject: [PATCH 22/67] No bare except --- speedtest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/speedtest.py b/speedtest.py index 5eb3841..79c473e 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1184,7 +1184,7 @@ class Speedtest(object): d = distance(self.lat_lon, (float(attrib.get('lat')), float(attrib.get('lon')))) - except: + except Exception: continue attrib['d'] = d @@ -1234,7 +1234,7 @@ class Speedtest(object): f = self._opener.open( '%s/speedtest/upload.%s' % (url, ext) ) - except: + except Exception: pass else: data = f.read().strip().decode() From 5fbe593fc8f34ef393813746b4b6e861b1f29017 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 23 Nov 2017 10:18:16 -0600 Subject: [PATCH 23/67] Get travis working properly again --- .travis.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 406754d..20e5ecd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ addons: - python2.4 - python2.5 - python2.6 + - python3.2 + - python3.3 matrix: include: @@ -21,9 +23,9 @@ matrix: env: TOXENV=py26 - python: 2.7 env: TOXENV=py27 - - python: 3.2 + - python: 2.7 env: TOXENV=py32 - - python: 3.3 + - python: 2.7 env: TOXENV=py33 - python: 3.4 env: TOXENV=py34 @@ -34,8 +36,11 @@ matrix: - python: pypy env: TOXENV=pypy +before_install: + - if [[ $(echo "$TOXENV" | egrep -c "py35") != 0 ]]; then pyenv global system 3.5; fi; + install: - - if [[ $(echo "$TOXENV" | egrep -c "py32") != 0 ]]; then pip install setuptools==17.1.1; fi + - if [[ $(echo "$TOXENV" | egrep -c "py32") != 0 ]]; then pip install setuptools==17.1.1; fi; - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") != 0 ]]; then pip install virtualenv==1.7.2 tox==1.3; fi; - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") == 0 ]]; then pip install tox; fi; From e80ccc464790ded3a6bab77639d400372478ee47 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 23 Nov 2017 10:30:31 -0600 Subject: [PATCH 24/67] update README for usage changes --- README.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 5af9aad..d80188a 100644 --- a/README.rst +++ b/README.rst @@ -77,13 +77,14 @@ Usage usage: speedtest-cli [-h] [--no-download] [--no-upload] [--bytes] [--share] [--simple] [--csv] [--csv-delimiter CSV_DELIMITER] [--csv-header] [--json] [--list] [--server SERVER] - [--mini MINI] [--source SOURCE] [--timeout TIMEOUT] - [--secure] [--no-pre-allocate] [--version] - + [--exclude EXCLUDE] [--mini MINI] [--source SOURCE] + [--timeout TIMEOUT] [--secure] [--no-pre-allocate] + [--version] + Command line interface for testing internet bandwidth using speedtest.net. -------------------------------------------------------------------------- https://github.com/sivel/speedtest-cli - + optional arguments: -h, --help show this help message and exit --no-download Do not perform download test @@ -106,7 +107,10 @@ Usage affected by --bytes --list Display a list of speedtest.net servers sorted by distance - --server SERVER Specify a server ID to test against + --server SERVER Specify a server ID to test against. Can be supplied + multiple times + --exclude EXCLUDE Exclude a server from selection. Can be supplied + multiple times --mini MINI URL of the Speedtest Mini server --source SOURCE Source IP address to bind to --timeout TIMEOUT HTTP timeout in seconds. Default 10 From eab354603f5b92d7775e04866e8fa8a853937070 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 11 Dec 2017 09:35:41 -0600 Subject: [PATCH 25/67] Don't display ERROR: 2 when argparse exits with non-0 --- speedtest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 79c473e..da74eb2 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1780,7 +1780,8 @@ def main(): print_('\nCancelling...') except (SpeedtestException, SystemExit): e = get_exception() - if getattr(e, 'code', 1) != 0: + # Ignore a successful exit, or argparse exit + if getattr(e, 'code', 1) not in (0, 2): raise SystemExit('ERROR: %s' % e) From fa2e15ee08b0c55930bb7a251784ca7b2c529c1c Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 11 Dec 2017 09:36:03 -0600 Subject: [PATCH 26/67] Skipping test should be quiet --- speedtest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/speedtest.py b/speedtest.py index da74eb2..f45753c 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1740,7 +1740,7 @@ def shell(): args.units[0]), quiet) else: - printer('Skipping download test') + printer('Skipping download test', quiet) if args.upload: printer('Testing upload speed', quiet, @@ -1751,7 +1751,7 @@ def shell(): args.units[0]), quiet) else: - printer('Skipping upload test') + printer('Skipping upload test', quiet) printer('Results:\n%r' % results.dict(), debug=True) From 6381ba37421ec3a827351c9b5910b2af0329c299 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 Jan 2018 16:07:46 -0600 Subject: [PATCH 27/67] Eliminate SHUTDOWN_EVENT global --- speedtest.py | 103 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 32 deletions(-) diff --git a/speedtest.py b/speedtest.py index f45753c..a6ac664 100755 --- a/speedtest.py +++ b/speedtest.py @@ -51,7 +51,6 @@ class FakeShutdownEvent(object): # Some global variables we use -SHUTDOWN_EVENT = FakeShutdownEvent() DEBUG = False _GLOBAL_DEFAULT_TIMEOUT = object() @@ -677,18 +676,19 @@ def get_attributes_by_tag_name(dom, tag_name): return dict(list(elem.attributes.items())) -def print_dots(current, total, start=False, end=False): +def print_dots(shutdown_event): """Built in callback function used by Thread classes for printing status """ + def inner(current, total, start=False, end=False): + if shutdown_event.isSet(): + return - if SHUTDOWN_EVENT.isSet(): - return - - sys.stdout.write('.') - if current + 1 == total and end is True: - sys.stdout.write('\n') - sys.stdout.flush() + sys.stdout.write('.') + if current + 1 == total and end is True: + sys.stdout.write('\n') + sys.stdout.flush() + return inner def do_nothing(*args, **kwargs): @@ -698,7 +698,8 @@ def do_nothing(*args, **kwargs): class HTTPDownloader(threading.Thread): """Thread class for retrieving a URL""" - def __init__(self, i, request, start, timeout, opener=None): + def __init__(self, i, request, start, timeout, opener=None, + shutdown_event=None): threading.Thread.__init__(self) self.request = request self.result = [0] @@ -710,11 +711,16 @@ class HTTPDownloader(threading.Thread): else: self._opener = urlopen + if shutdown_event: + self._shutdown_event = shutdown_event + else: + self._shutdown_event = FakeShutdownEvent() + def run(self): try: if (timeit.default_timer() - self.starttime) <= self.timeout: f = self._opener(self.request) - while (not SHUTDOWN_EVENT.isSet() and + while (not self._shutdown_event.isSet() and (timeit.default_timer() - self.starttime) <= self.timeout): self.result.append(len(f.read(10240))) @@ -730,11 +736,16 @@ class HTTPUploaderData(object): has been reached """ - def __init__(self, length, start, timeout): + def __init__(self, length, start, timeout, shutdown_event=None): self.length = length self.start = start self.timeout = timeout + if shutdown_event: + self._shutdown_event = shutdown_event + else: + self._shutdown_event = FakeShutdownEvent() + self._data = None self.total = [0] @@ -763,7 +774,7 @@ class HTTPUploaderData(object): def read(self, n=10240): if ((timeit.default_timer() - self.start) <= self.timeout and - not SHUTDOWN_EVENT.isSet()): + not self._shutdown_event.isSet()): chunk = self.data.read(n) self.total.append(len(chunk)) return chunk @@ -777,7 +788,8 @@ class HTTPUploaderData(object): class HTTPUploader(threading.Thread): """Thread class for putting a URL""" - def __init__(self, i, request, start, size, timeout, opener=None): + def __init__(self, i, request, start, size, timeout, opener=None, + shutdown_event=None): threading.Thread.__init__(self) self.request = request self.request.data.start = self.starttime = start @@ -791,11 +803,16 @@ class HTTPUploader(threading.Thread): else: self._opener = urlopen + if shutdown_event: + self._shutdown_event = shutdown_event + else: + self._shutdown_event = FakeShutdownEvent() + def run(self): request = self.request try: if ((timeit.default_timer() - self.starttime) <= self.timeout and - not SHUTDOWN_EVENT.isSet()): + not self._shutdown_event.isSet()): try: f = self._opener(request) except TypeError: @@ -969,7 +986,7 @@ class Speedtest(object): """Class for performing standard speedtest.net testing operations""" def __init__(self, config=None, source_address=None, timeout=10, - secure=False): + secure=False, shutdown_event=None): self.config = {} self._source_address = source_address @@ -978,6 +995,11 @@ class Speedtest(object): self._secure = secure + if shutdown_event: + self._shutdown_event = shutdown_event + else: + self._shutdown_event = FakeShutdownEvent() + self.get_config() if config is not None: self.config.update(config) @@ -1372,9 +1394,14 @@ class Speedtest(object): def producer(q, requests, request_count): for i, request in enumerate(requests): - thread = HTTPDownloader(i, request, start, - self.config['length']['download'], - opener=self._opener) + thread = HTTPDownloader( + i, + request, + start, + self.config['length']['download'], + opener=self._opener, + shutdown_event=self._shutdown_event + ) thread.start() q.put(thread, True) callback(i, request_count, start=True) @@ -1427,7 +1454,12 @@ class Speedtest(object): for i, size in enumerate(sizes): # We set ``0`` for ``start`` and handle setting the actual # ``start`` in ``HTTPUploader`` to get better measurements - data = HTTPUploaderData(size, 0, self.config['length']['upload']) + data = HTTPUploaderData( + size, + 0, + self.config['length']['upload'], + shutdown_event=self._shutdown_event + ) if pre_allocate: data.pre_allocate() requests.append( @@ -1439,9 +1471,15 @@ class Speedtest(object): def producer(q, requests, request_count): for i, request in enumerate(requests[:request_count]): - thread = HTTPUploader(i, request[0], start, request[1], - self.config['length']['upload'], - opener=self._opener) + thread = HTTPUploader( + i, + request[0], + start, + request[1], + self.config['length']['upload'], + opener=self._opener, + shutdown_event=self._shutdown_event + ) thread.start() q.put(thread, True) callback(i, request_count, start=True) @@ -1477,14 +1515,15 @@ class Speedtest(object): return self.results.upload -def ctrl_c(signum, frame): +def ctrl_c(shutdown_event): """Catch Ctrl-C key sequence and set a SHUTDOWN_EVENT for our threaded operations """ - - SHUTDOWN_EVENT.set() - print_('\nCancelling...') - sys.exit(0) + def inner(signum, frame): + shutdown_event.set() + print_('\nCancelling...') + sys.exit(0) + return inner def version(): @@ -1622,10 +1661,10 @@ def printer(string, quiet=False, debug=False, error=False, **kwargs): def shell(): """Run the full speedtest.net test""" - global SHUTDOWN_EVENT, DEBUG - SHUTDOWN_EVENT = threading.Event() + global DEBUG + shutdown_event = threading.Event() - signal.signal(signal.SIGINT, ctrl_c) + signal.signal(signal.SIGINT, ctrl_c(shutdown_event)) args = parse_args() @@ -1665,7 +1704,7 @@ def shell(): if quiet or debug: callback = do_nothing else: - callback = print_dots + callback = print_dots(shutdown_event) printer('Retrieving speedtest.net configuration...', quiet) try: From 4737a69f105d101bac811e15b5db5e0e3e59bde0 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 Jan 2018 17:16:52 -0600 Subject: [PATCH 28/67] Add a few additional tests, specifically around --source --- speedtest.py | 2 +- tests/scripts/source.py | 37 +++++++++++++++++++++++++++++++++++++ tox.ini | 4 ++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/scripts/source.py diff --git a/speedtest.py b/speedtest.py index a6ac664..d0da04e 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2012-2016 Matt Martz +# Copyright 2012-2018 Matt Martz # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/tests/scripts/source.py b/tests/scripts/source.py new file mode 100644 index 0000000..357f4c6 --- /dev/null +++ b/tests/scripts/source.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright 2018 Matt Martz +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys +import subprocess + +cmd = [sys.executable, 'speedtest.py', '--source', '127.0.0.1'] + +p = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE +) + +stdout, stderr = p.communicate() + +if p.returncode != 1: + raise SystemExit('%s did not fail with exit code 1' % ' '.join(cmd)) + +if 'Invalid argument'.encode() not in stderr: + raise SystemExit( + '"Invalid argument" not found in stderr:\n%s' % stderr.decode() + ) diff --git a/tox.ini b/tox.ini index 477fa1c..8a63b5b 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,8 @@ commands = {envpython} -V {envpython} -m compileall speedtest.py {envpython} speedtest.py + {envpython} speedtest.py --source 172.17.0.1 + {envpython} tests/scripts/source.py [testenv:flake8] basepython=python @@ -19,3 +21,5 @@ commands = pypy -V pypy -m compileall speedtest.py pypy speedtest.py + pypy speedtest.py --source 172.17.0.1 + pypy tests/scripts/source.py From 9848481d0636373b2ba6e6c07cbb0b1d8dd742df Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 Jan 2018 18:22:16 -0600 Subject: [PATCH 29/67] Use the printer everywhere, leaving print_ to only be used within printer --- speedtest.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/speedtest.py b/speedtest.py index d0da04e..1f53b37 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1521,7 +1521,7 @@ def ctrl_c(shutdown_event): """ def inner(signum, frame): shutdown_event.set() - print_('\nCancelling...') + printer('\nCancelling...', error=True) sys.exit(0) return inner @@ -1529,14 +1529,14 @@ def ctrl_c(shutdown_event): def version(): """Print the version""" - print_(__version__) + printer(__version__) sys.exit(0) def csv_header(delimiter=','): """Print the CSV Headers""" - print_(SpeedtestResults.csv_header(delimiter=delimiter)) + printer(SpeedtestResults.csv_header(delimiter=delimiter)) sys.exit(0) @@ -1641,7 +1641,7 @@ def validate_optional_args(args): def printer(string, quiet=False, debug=False, error=False, **kwargs): - """Helper function to print a string only when not quiet""" + """Helper function print a string with various features""" if debug and not DEBUG: return @@ -1729,7 +1729,7 @@ def shell(): line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) ' '[%(d)0.2f km]' % server) try: - print_(line) + printer(line) except IOError: e = get_exception() if e.errno != errno.EPIPE: @@ -1795,18 +1795,18 @@ def shell(): printer('Results:\n%r' % results.dict(), debug=True) if args.simple: - print_('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' % - (results.ping, - (results.download / 1000.0 / 1000.0) / args.units[1], - args.units[0], - (results.upload / 1000.0 / 1000.0) / args.units[1], - args.units[0])) + printer('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' % + (results.ping, + (results.download / 1000.0 / 1000.0) / args.units[1], + args.units[0], + (results.upload / 1000.0 / 1000.0) / args.units[1], + args.units[0])) elif args.csv: - print_(results.csv(delimiter=args.csv_delimiter)) + printer(results.csv(delimiter=args.csv_delimiter)) elif args.json: if args.share: results.share() - print_(results.json()) + printer(results.json()) if args.share and not machine_format: printer('Share results: %s' % results.share()) @@ -1816,7 +1816,7 @@ def main(): try: shell() except KeyboardInterrupt: - print_('\nCancelling...') + printer('\nCancelling...', error=True) except (SpeedtestException, SystemExit): e = get_exception() # Ignore a successful exit, or argparse exit From c16ffd4ae7f3476bd037d0fa5742cd4c81ade7a7 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 2 Jan 2018 18:32:03 -0600 Subject: [PATCH 30/67] Catch OSError and EOFError while reading from gzip stream --- speedtest.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/speedtest.py b/speedtest.py index 1f53b37..19ca9b5 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1041,7 +1041,10 @@ class Speedtest(object): stream = get_response_stream(uh) while 1: - configxml.append(stream.read(1024)) + try: + configxml.append(stream.read(1024)) + except (OSError, EOFError): + raise ConfigRetrievalError(get_exception()) if len(configxml[-1]) == 0: break stream.close() @@ -1166,7 +1169,10 @@ class Speedtest(object): serversxml = [] while 1: - serversxml.append(stream.read(1024)) + try: + serversxml.append(stream.read(1024)) + except (OSError, EOFError): + raise ServersRetrievalError(get_exception()) if len(serversxml[-1]) == 0: break From 48a3d33ae478e218a5548a484b07e87413cc0c27 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 3 Jan 2018 09:16:51 -0600 Subject: [PATCH 31/67] Bump to beta --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 19ca9b5..a0e7a80 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.0.0a' +__version__ = '2.0.0b' class FakeShutdownEvent(object): From e1bab1ab55470384e953cd8c4cf18b03c7082a21 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 8 Jan 2018 16:57:26 -0600 Subject: [PATCH 32/67] Only add terminal colors with DEBUG if stdout is a tty --- speedtest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index a0e7a80..d7172e1 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1653,7 +1653,10 @@ def printer(string, quiet=False, debug=False, error=False, **kwargs): return if debug: - out = '\033[1;30mDEBUG: %s\033[0m' % string + if sys.stdout.isatty(): + out = '\033[1;30mDEBUG: %s\033[0m' % string + else: + out = 'DEBUG: %s' % string else: out = string From 2c847a1849e5c58bd21d3ea497133b0910bff1f8 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 26 Jan 2018 15:52:06 -0600 Subject: [PATCH 33/67] Add some guard code for places where sys.stdout and stderr are replaced with some other incompatible object --- speedtest.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/speedtest.py b/speedtest.py index d7172e1..a3d3004 100755 --- a/speedtest.py +++ b/speedtest.py @@ -165,8 +165,14 @@ except ImportError: self.flush() _py3_print = getattr(builtins, 'print') - _py3_utf8_stdout = _Py3Utf8Output(sys.stdout) - _py3_utf8_stderr = _Py3Utf8Output(sys.stderr) + try: + _py3_utf8_stdout = _Py3Utf8Output(sys.stdout) + _py3_utf8_stderr = _Py3Utf8Output(sys.stderr) + except OSError: + # sys.stdout/sys.stderr is not a compatible stdout/stderr object + # just use it and hope things go ok + _py3_utf8_stdout = sys.stdout + _py3_utf8_stderr = sys.stderr def to_utf8(v): """No-op encode to utf-8 for py3""" From 4f4c1dd8d19fc30bf17fb71f78e8415df94e6f09 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Feb 2018 16:16:51 -0600 Subject: [PATCH 34/67] Update man page --- speedtest-cli.1 | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/speedtest-cli.1 b/speedtest-cli.1 index 9e1befe..87558b4 100644 --- a/speedtest-cli.1 +++ b/speedtest-cli.1 @@ -1,4 +1,4 @@ -.TH "speedtest-cli" 1 "2014-04-23" "speedtest-cli" +.TH "speedtest-cli" 1 "2018-01-05" "speedtest-cli" .SH NAME speedtest\-cli \- Command line interface for testing internet bandwidth using speedtest.net .SH SYNOPSIS @@ -23,14 +23,24 @@ Displays usage for the tool. .B Options +\fB\-\-no\-download\fR +.RS +Do not perform download test +.RE + +\fB\-\-no\-upload\fR +.RS +Do not perform upload test +.RE + \fB\-\-bytes\fR .RS -Display values in bytes instead of bits. Does not affect the image generated by \-\-share +Display values in bytes instead of bits. Does not affect the image generated by \-\-share, nor output from \-\-json or \-\-csv .RE \fB\-\-share\fR .RS -Generate and provide a URL to the speedtest.net share results image +Generate and provide a URL to the speedtest.net share results image, not displayed with \-\-csv .RE \fB\-\-simple\fR @@ -43,12 +53,12 @@ Suppress verbose output, only show basic information Suppress verbose output, only show basic information in CSV format. Speeds listed in bit/s and not affected by \-\-bytes .RE -\fB\-\-csv-delimiter CSV_DELIMITER\fR +\fB\-\-csv\-delimiter CSV_DELIMITER\fR .RS Single character delimiter to use in CSV output. Default "," .RE -\fB\-\-csv-header\fR +\fB\-\-csv\-header\fR .RS Print CSV headers .RE @@ -65,7 +75,12 @@ Display a list of speedtest.net servers sorted by distance \fB\-\-server SERVER\fR .RS -Specify a server ID to test against +Specify a server ID to test against. Can be supplied multiple times +.RE + +\fB\-\-exclude EXCLUDE\fR +.RS +Exclude a server from selection. Can be supplied multiple times .RE \fB\-\-mini MINI\fR @@ -88,6 +103,11 @@ HTTP timeout in seconds. Default 10 Use HTTPS instead of HTTP when communicating with speedtest.net operated servers .RE +\fB\-\-no\-pre\-allocate\fR +.RS +Do not pre allocate upload data. Pre allocation is enabled by default to improve upload performance. To support systems with insufficient memory, use this option to avoid a MemoryError +.RE + \fB\-\-version\fR .RS Show the version number and exit From 831c07911398014703010b38bf9ae5f82a76be08 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Feb 2018 16:17:03 -0600 Subject: [PATCH 35/67] Bump for release --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index a3d3004..359224c 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.0.0b' +__version__ = '2.0.0' class FakeShutdownEvent(object): From ee2e647b9b4a14d9f82d3f0aa01e9f83fa47f50e Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Feb 2018 16:25:59 -0600 Subject: [PATCH 36/67] Remove deprecated speedtest_cli.py --- setup.py | 4 ++-- speedtest_cli.py | 34 ---------------------------------- 2 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 speedtest_cli.py diff --git a/setup.py b/setup.py index f323f81..32d16bb 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2012-2016 Matt Martz +# Copyright 2012-2018 Matt Martz # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -66,7 +66,7 @@ setup( author_email='matt@sivel.net', url='https://github.com/sivel/speedtest-cli', license='Apache License, Version 2.0', - py_modules=['speedtest', 'speedtest_cli'], + py_modules=['speedtest'], entry_points={ 'console_scripts': [ 'speedtest=speedtest:main', diff --git a/speedtest_cli.py b/speedtest_cli.py deleted file mode 100644 index 1d0fb29..0000000 --- a/speedtest_cli.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright 2012-2016 Matt Martz -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import warnings - -DEPRECATED_MSG = ('The file speedtest_cli.py has been deprecated in favor of ' - 'speedtest.py\nand is available for download at:\n\n' - 'https://raw.githubusercontent.com/sivel/speedtest-cli/' - 'master/speedtest.py') - - -if __name__ == '__main__': - raise SystemExit(DEPRECATED_MSG) -else: - try: - from speedtest import * - except ImportError: - raise SystemExit(DEPRECATED_MSG) - else: - warnings.warn(DEPRECATED_MSG, UserWarning) From 27a830119287e0cd456f9e04363494c8073bd5a9 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Feb 2018 16:33:01 -0600 Subject: [PATCH 37/67] Replace downloads badge with travis --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index d80188a..5ccde35 100644 --- a/README.rst +++ b/README.rst @@ -7,9 +7,9 @@ speedtest.net .. image:: https://img.shields.io/pypi/v/speedtest-cli.svg :target: https://pypi.python.org/pypi/speedtest-cli/ :alt: Latest Version -.. image:: https://img.shields.io/pypi/dm/speedtest-cli.svg +.. image:: https://img.shields.io/travis/sivel/speedtest-cli.svg :target: https://pypi.python.org/pypi/speedtest-cli/ - :alt: Downloads + :alt: Travis .. image:: https://img.shields.io/pypi/l/speedtest-cli.svg :target: https://pypi.python.org/pypi/speedtest-cli/ :alt: License From 0a7823db7acb895765f074261b01c6d1950b8a49 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 5 Feb 2018 16:33:07 -0600 Subject: [PATCH 38/67] Tested through 3.7 --- README.rst | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5ccde35..3e57422 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ speedtest.net Versions -------- -speedtest-cli works with Python 2.4-3.6 +speedtest-cli works with Python 2.4-3.7 .. image:: https://img.shields.io/pypi/pyversions/speedtest-cli.svg :target: https://pypi.python.org/pypi/speedtest-cli/ diff --git a/setup.py b/setup.py index 32d16bb..29e0f31 100644 --- a/setup.py +++ b/setup.py @@ -91,5 +91,6 @@ setup( 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', ] ) From 35c3ee20edbd19bb89a9b7b468b08a0a30566012 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 13 Feb 2018 16:21:57 -0600 Subject: [PATCH 39/67] Exit with nicer error if lat/lon is not valid --- speedtest.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 359224c..c92a791 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1119,7 +1119,13 @@ class Speedtest(object): 'upload_max': upload_count * size_count }) - self.lat_lon = (float(client['lat']), float(client['lon'])) + try: + self.lat_lon = (float(client['lat']), float(client['lon'])) + except ValueError: + raise SpeedtestConfigError( + 'Unknown location: lat=%r lon=%r' % + (client.get('lat'), client.get('lon')) + ) printer('Config:\n%r' % self.config, debug=True) From 8ff923b0fb03be02002b168143ffded7527138e4 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 13 Feb 2018 16:22:23 -0600 Subject: [PATCH 40/67] Bump to 2.0.1a --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index c92a791..d6e1219 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.0.0' +__version__ = '2.0.1a' class FakeShutdownEvent(object): From f8aa20ecdf5707eb2dfce1a811a6c19c5cf928bd Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 20 Feb 2018 14:59:08 -0600 Subject: [PATCH 41/67] Move results.share() to ensure csv and json have access to it. Fixes #483 --- speedtest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/speedtest.py b/speedtest.py index d6e1219..7ea05a5 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1815,6 +1815,9 @@ def shell(): printer('Results:\n%r' % results.dict(), debug=True) + if not args.simple and args.share: + results.share() + if args.simple: printer('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' % (results.ping, @@ -1825,8 +1828,6 @@ def shell(): elif args.csv: printer(results.csv(delimiter=args.csv_delimiter)) elif args.json: - if args.share: - results.share() printer(results.json()) if args.share and not machine_format: From 9c2977acfcfc782846fcecac2af39b9b50d43d48 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 9 Mar 2018 09:46:10 -0600 Subject: [PATCH 42/67] Gracefully handle XML parsing errors. Fixes #490 #491 --- speedtest.py | 60 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/speedtest.py b/speedtest.py index 7ea05a5..0fe067b 100755 --- a/speedtest.py +++ b/speedtest.py @@ -70,6 +70,7 @@ except ImportError: import xml.etree.ElementTree as ET except ImportError: from xml.dom import minidom as DOM + from xml.parsers.expat import ExpatError ET = None try: @@ -284,7 +285,11 @@ class SpeedtestHTTPError(SpeedtestException): class SpeedtestConfigError(SpeedtestException): - """Configuration provided is invalid""" + """Configuration XML is invalid""" + + +class SpeedtestServersError(SpeedtestException): + """Servers XML is invalid""" class ConfigRetrievalError(SpeedtestHTTPError): @@ -1042,16 +1047,16 @@ class Speedtest(object): uh, e = catch_request(request, opener=self._opener) if e: raise ConfigRetrievalError(e) - configxml = [] + configxml_list = [] stream = get_response_stream(uh) while 1: try: - configxml.append(stream.read(1024)) + configxml_list.append(stream.read(1024)) except (OSError, EOFError): raise ConfigRetrievalError(get_exception()) - if len(configxml[-1]) == 0: + if len(configxml_list[-1]) == 0: break stream.close() uh.close() @@ -1059,10 +1064,18 @@ class Speedtest(object): if int(uh.code) != 200: return None - printer('Config XML:\n%s' % ''.encode().join(configxml), debug=True) + configxml = ''.encode().join(configxml_list) + + printer('Config XML:\n%s' % configxml, debug=True) try: - root = ET.fromstring(''.encode().join(configxml)) + try: + root = ET.fromstring(configxml) + except ET.ParseError: + e = get_exception() + raise SpeedtestConfigError( + 'Malformed speedtest.net configuration: %s' % e + ) server_config = root.find('server-config').attrib download = root.find('download').attrib upload = root.find('upload').attrib @@ -1070,7 +1083,13 @@ class Speedtest(object): client = root.find('client').attrib except AttributeError: - root = DOM.parseString(''.join(configxml)) + try: + root = DOM.parseString(configxml) + except ExpatError: + e = get_exception() + raise SpeedtestConfigError( + 'Malformed speedtest.net configuration: %s' % e + ) server_config = get_attributes_by_tag_name(root, 'server-config') download = get_attributes_by_tag_name(root, 'download') upload = get_attributes_by_tag_name(root, 'upload') @@ -1179,13 +1198,13 @@ class Speedtest(object): stream = get_response_stream(uh) - serversxml = [] + serversxml_list = [] while 1: try: - serversxml.append(stream.read(1024)) + serversxml_list.append(stream.read(1024)) except (OSError, EOFError): raise ServersRetrievalError(get_exception()) - if len(serversxml[-1]) == 0: + if len(serversxml_list[-1]) == 0: break stream.close() @@ -1194,15 +1213,28 @@ class Speedtest(object): if int(uh.code) != 200: raise ServersRetrievalError() - printer('Servers XML:\n%s' % ''.encode().join(serversxml), - debug=True) + serversxml = ''.encode().join(serversxml_list) + + printer('Servers XML:\n%s' % serversxml, debug=True) try: try: - root = ET.fromstring(''.encode().join(serversxml)) + try: + root = ET.fromstring(serversxml) + except ET.ParseError: + e = get_exception() + raise SpeedtestServersError( + 'Malformed speedtest.net server list: %s' % e + ) elements = root.getiterator('server') except AttributeError: - root = DOM.parseString(''.join(serversxml)) + try: + root = DOM.parseString(serversxml) + except ExpatError: + e = get_exception() + raise SpeedtestServersError( + 'Malformed speedtest.net server list: %s' % e + ) elements = root.getElementsByTagName('server') except (SyntaxError, xml.parsers.expat.ExpatError): raise ServersRetrievalError() From 9e185e8f88db6f733f16b2bc41d6c5976394355e Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 22 May 2018 15:28:00 -0500 Subject: [PATCH 43/67] Properly handle older HTTPSConnection. Fixes #513 --- speedtest.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/speedtest.py b/speedtest.py index 0fe067b..d874adf 100755 --- a/speedtest.py +++ b/speedtest.py @@ -389,13 +389,11 @@ class SpeedtestHTTPConnection(HTTPConnection): """ def __init__(self, *args, **kwargs): source_address = kwargs.pop('source_address', None) - context = kwargs.pop('context', None) timeout = kwargs.pop('timeout', 10) HTTPConnection.__init__(self, *args, **kwargs) self.source_address = source_address - self._context = context self.timeout = timeout def connect(self): @@ -420,6 +418,17 @@ if HTTPSConnection: """Custom HTTPSConnection to support source_address across Python 2.4 - Python 3 """ + def __init__(self, *args, **kwargs): + source_address = kwargs.pop('source_address', None) + context = kwargs.pop('context', None) + timeout = kwargs.pop('timeout', 10) + + HTTPSConnection.__init__(self, *args, **kwargs) + + self.source_address = source_address + self._context = context + self.timeout = timeout + def connect(self): "Connect to a host on a given (SSL) port." @@ -428,8 +437,7 @@ if HTTPSConnection: kwargs = {} if hasattr(ssl, 'SSLContext'): kwargs['server_hostname'] = self.host - - self.sock = self._context.wrap_socket(self.sock, **kwargs) + self.sock = self._context.wrap_socket(self.sock, **kwargs) def _build_connection(connection, source_address, timeout, context=None): From 4ceae774016318d6a4f04b935c49d5f08043616d Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 22 May 2018 16:30:27 -0500 Subject: [PATCH 44/67] Handle virtualenv and tox versions for py2.6/3.3 --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 20e5ecd..0380352 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,7 +42,10 @@ before_install: install: - if [[ $(echo "$TOXENV" | egrep -c "py32") != 0 ]]; then pip install setuptools==17.1.1; fi; - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") != 0 ]]; then pip install virtualenv==1.7.2 tox==1.3; fi; - - if [[ $(echo "$TOXENV" | egrep -c "(py2[45]|py3[12])") == 0 ]]; then pip install tox; fi; + - if [[ $(echo "$TOXENV" | egrep -c "(py26|py33)") != 0 ]]; then pip install virtualenv==15.2.0 tox==2.9.1; fi; + - if [[ $(echo "$TOXENV" | egrep -c "(py2[456]|py3[123])") == 0 ]]; then pip install tox; fi; + + script: - tox From c7530bb143ade03772cc0733e531ee92cd819a8e Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 23 May 2018 15:26:20 -0500 Subject: [PATCH 45/67] Bump to 2.0.1 for release --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index d874adf..1608eb1 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.0.1a' +__version__ = '2.0.1' class FakeShutdownEvent(object): From 41e599f9c31e9e6f136324102f89368520bb9991 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 24 May 2018 09:37:39 -0500 Subject: [PATCH 46/67] Ensure we are utilizing the context created by HTTPSConnection, or falling back to ssl. Fixes #517 --- speedtest.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/speedtest.py b/speedtest.py index 1608eb1..4487a7e 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.0.1' +__version__ = '2.0.2a' class FakeShutdownEvent(object): @@ -85,9 +85,9 @@ except ImportError: HTTPErrorProcessor, OpenerDirector) try: - from httplib import HTTPConnection + from httplib import HTTPConnection, BadStatusLine except ImportError: - from http.client import HTTPConnection + from http.client import HTTPConnection, BadStatusLine try: from httplib import HTTPSConnection @@ -266,10 +266,13 @@ try: except AttributeError: CERT_ERROR = tuple() - HTTP_ERRORS = ((HTTPError, URLError, socket.error, ssl.SSLError) + - CERT_ERROR) + HTTP_ERRORS = ( + (HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) + + CERT_ERROR + ) except ImportError: - HTTP_ERRORS = (HTTPError, URLError, socket.error) + ssl = None + HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine) class SpeedtestException(Exception): @@ -420,14 +423,12 @@ if HTTPSConnection: """ def __init__(self, *args, **kwargs): source_address = kwargs.pop('source_address', None) - context = kwargs.pop('context', None) timeout = kwargs.pop('timeout', 10) HTTPSConnection.__init__(self, *args, **kwargs) - self.source_address = source_address - self._context = context self.timeout = timeout + self.source_address = source_address def connect(self): "Connect to a host on a given (SSL) port." @@ -435,9 +436,13 @@ if HTTPSConnection: SpeedtestHTTPConnection.connect(self) kwargs = {} - if hasattr(ssl, 'SSLContext'): - kwargs['server_hostname'] = self.host - self.sock = self._context.wrap_socket(self.sock, **kwargs) + if ssl: + if hasattr(ssl, 'SSLContext'): + kwargs['server_hostname'] = self.host + try: + self.sock = self._context.wrap_socket(self.sock, **kwargs) + except AttributeError: + self.sock = ssl.wrap_socket(self.sock, **kwargs) def _build_connection(connection, source_address, timeout, context=None): From 72ed585c6f9d19b39a80e0c9364592095d070f62 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 24 May 2018 11:06:29 -0500 Subject: [PATCH 47/67] Bump to v2.0.2 --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 4487a7e..7a99c99 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.0.2a' +__version__ = '2.0.2' class FakeShutdownEvent(object): From b2654de4107b236eb5417c1ebeeebbf82a391d71 Mon Sep 17 00:00:00 2001 From: Alex Ward Date: Mon, 3 Dec 2018 16:20:28 +0000 Subject: [PATCH 48/67] Automatically resolve .best property (#514) * automatically call get_best_server * add back SpeedtestBestServerFailer exception --- speedtest.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/speedtest.py b/speedtest.py index 7a99c99..276cf3f 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1041,10 +1041,7 @@ class Speedtest(object): @property def best(self): if not self._best: - raise SpeedtestMissingBestServer( - 'get_best_server not called or not able to determine best ' - 'server' - ) + self.get_best_server() return self._best def get_config(self): From a8a32650015997f7847f2de72a29ff7906f53d8a Mon Sep 17 00:00:00 2001 From: liuxu Date: Sat, 1 Dec 2018 23:58:38 +0800 Subject: [PATCH 49/67] Fix python3 upload problem In python3, if Content-length is not set,urllib.request.AbstractHTTPHandler::do_request_() will use "Transfer-encoding:chunked", which will cause HTTPUploader not to exit until timeout --- speedtest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 276cf3f..14d2dd6 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1522,9 +1522,11 @@ class Speedtest(object): ) if pre_allocate: data.pre_allocate() + + headers = {'Content-length': size} requests.append( ( - build_request(self.best['url'], data, secure=self._secure), + build_request(self.best['url'], data, secure=self._secure, headers=headers), size ) ) From 72bf53affa81d1fac97e4e3ca97ed98d62d4a203 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 3 Dec 2018 10:44:49 -0600 Subject: [PATCH 50/67] Fix linting error --- speedtest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 14d2dd6..d44ff5d 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1526,7 +1526,8 @@ class Speedtest(object): headers = {'Content-length': size} requests.append( ( - build_request(self.best['url'], data, secure=self._secure, headers=headers), + build_request(self.best['url'], data, secure=self._secure, + headers=headers), size ) ) From ddb8db0c9458a8ce7ee3c9e82758fd1f943099b1 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 2 Jan 2019 09:18:21 -0600 Subject: [PATCH 51/67] Fix install instructions with git clone. Fixes #566 --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3e57422..e7af2b7 100644 --- a/README.rst +++ b/README.rst @@ -51,7 +51,8 @@ or :: git clone https://github.com/sivel/speedtest-cli.git - python speedtest-cli/setup.py install + cd speedtest-cli + python setup.py install Just download (Like the way it used to be) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From ca2250f700e1e9c70d3c97fa5d091c69bbae7a9f Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 23 Jan 2019 11:33:30 -0600 Subject: [PATCH 52/67] Add functionality for single threaded testing. Fixes #571 --- speedtest.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/speedtest.py b/speedtest.py index d44ff5d..a18435a 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1436,8 +1436,12 @@ class Speedtest(object): printer('Best Server:\n%r' % best, debug=True) return best - def download(self, callback=do_nothing): - """Test download speed against speedtest.net""" + def download(self, callback=do_nothing, threads=None): + """Test download speed against speedtest.net + + A ``threads`` value of ``None`` will fall back to those dictated + by the speedtest.net configuration + """ urls = [] for size in self.config['sizes']['download']: @@ -1476,7 +1480,7 @@ class Speedtest(object): finished.append(sum(thread.result)) callback(thread.i, request_count, end=True) - q = Queue(self.config['threads']['download']) + q = Queue(threads or self.config['threads']['download']) prod_thread = threading.Thread(target=producer, args=(q, requests, request_count)) cons_thread = threading.Thread(target=consumer, @@ -1498,8 +1502,12 @@ class Speedtest(object): self.config['threads']['upload'] = 8 return self.results.download - def upload(self, callback=do_nothing, pre_allocate=True): - """Test upload speed against speedtest.net""" + def upload(self, callback=do_nothing, pre_allocate=True, threads=None): + """Test upload speed against speedtest.net + + A ``threads`` value of ``None`` will fall back to those dictated + by the speedtest.net configuration + """ sizes = [] @@ -1557,7 +1565,7 @@ class Speedtest(object): finished.append(thread.result) callback(thread.i, request_count, end=True) - q = Queue(self.config['threads']['upload']) + q = Queue(threads or self.config['threads']['upload']) prod_thread = threading.Thread(target=producer, args=(q, requests, request_count)) cons_thread = threading.Thread(target=consumer, @@ -1625,6 +1633,10 @@ def parse_args(): parser.add_argument('--no-upload', dest='upload', default=True, action='store_const', const=False, help='Do not perform upload test') + parser.add_argument('--single', default=False, action='store_true', + help='Only use a single connection instead of ' + 'multiple. This simulates a typical file ' + 'transfer.') parser.add_argument('--bytes', dest='units', action='store_const', const=('byte', 8), default=('bit', 1), help='Display values in bytes instead of bits. Does ' @@ -1839,7 +1851,10 @@ def shell(): if args.download: printer('Testing download speed', quiet, end=('', '\n')[bool(debug)]) - speedtest.download(callback=callback) + speedtest.download( + callback=callback, + threads=(None, 1)[args.single] + ) printer('Download: %0.2f M%s/s' % ((results.download / 1000.0 / 1000.0) / args.units[1], args.units[0]), @@ -1850,7 +1865,11 @@ def shell(): if args.upload: printer('Testing upload speed', quiet, end=('', '\n')[bool(debug)]) - speedtest.upload(callback=callback, pre_allocate=args.pre_allocate) + speedtest.upload( + callback=callback, + pre_allocate=args.pre_allocate, + threads=(None, 1)[args.single] + ) printer('Upload: %0.2f M%s/s' % ((results.upload / 1000.0 / 1000.0) / args.units[1], args.units[0]), From 9ac1091eae4d454354337314ef2b3594b75e9b81 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 23 Jan 2019 11:34:00 -0600 Subject: [PATCH 53/67] Add debug support to show if a URL request resulted in a redirect --- speedtest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/speedtest.py b/speedtest.py index a18435a..dcda09c 100755 --- a/speedtest.py +++ b/speedtest.py @@ -666,6 +666,8 @@ def catch_request(request, opener=None): try: uh = _open(request) + if request.get_full_url() != uh.geturl(): + printer('Redirected to %s' % uh.geturl(), debug=True) return uh, False except HTTP_ERRORS: e = get_exception() From b0b826c8703797eb2011ab70d2a2b6ad8f4a08e2 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 23 Jan 2019 11:34:23 -0600 Subject: [PATCH 54/67] Add the python version to the version output --- speedtest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index dcda09c..17ba7d9 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1602,7 +1602,8 @@ def ctrl_c(shutdown_event): def version(): """Print the version""" - printer(__version__) + printer('speedtest-cli %s' % __version__) + printer('Python %s' % sys.version.replace('\n', '')) sys.exit(0) From b43334f1ec7143e5ddf993a33d6a394bb4f2535d Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 19 Feb 2019 16:37:23 -0600 Subject: [PATCH 55/67] Switch from platform.system to platform.platform. Fixes #574 --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 17ba7d9..ff0eabf 100755 --- a/speedtest.py +++ b/speedtest.py @@ -607,7 +607,7 @@ def build_user_agent(): ua_tuple = ( 'Mozilla/5.0', - '(%s; U; %s; en-us)' % (platform.system(), platform.architecture()[0]), + '(%s; U; %s; en-us)' % (platform.platform(), platform.architecture()[0]), 'Python/%s' % platform.python_version(), '(KHTML, like Gecko)', 'speedtest-cli/%s' % __version__ From 217ce8eff1a1ce307b047d86793a4593890cfa22 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 19 Feb 2019 16:56:26 -0600 Subject: [PATCH 56/67] ssl.wrap_socket doesn't support server_hostname. See #572 --- speedtest.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/speedtest.py b/speedtest.py index ff0eabf..3f58e6f 100755 --- a/speedtest.py +++ b/speedtest.py @@ -435,14 +435,18 @@ if HTTPSConnection: SpeedtestHTTPConnection.connect(self) - kwargs = {} if ssl: - if hasattr(ssl, 'SSLContext'): - kwargs['server_hostname'] = self.host try: + kwargs = {} + if hasattr(ssl, 'SSLContext'): + kwargs['server_hostname'] = self.host self.sock = self._context.wrap_socket(self.sock, **kwargs) except AttributeError: - self.sock = ssl.wrap_socket(self.sock, **kwargs) + self.sock = ssl.wrap_socket(self.sock) + try: + self.sock.server_hostname = self.host + except AttributeError: + pass def _build_connection(connection, source_address, timeout, context=None): From 6cf43b2ff7fe67f63351b7e537b9944ea434d76e Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 19 Feb 2019 16:58:50 -0600 Subject: [PATCH 57/67] linting fix --- speedtest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 3f58e6f..464e2f7 100755 --- a/speedtest.py +++ b/speedtest.py @@ -611,7 +611,8 @@ def build_user_agent(): ua_tuple = ( 'Mozilla/5.0', - '(%s; U; %s; en-us)' % (platform.platform(), platform.architecture()[0]), + '(%s; U; %s; en-us)' % (platform.platform(), + platform.architecture()[0]), 'Python/%s' % platform.python_version(), '(KHTML, like Gecko)', 'speedtest-cli/%s' % __version__ From f356c7b02d7941a8f114caeefe63ae2747152cf2 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 19 Feb 2019 17:17:25 -0600 Subject: [PATCH 58/67] ensure ERROR doesn't print an empty string --- speedtest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 464e2f7..217ee39 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1915,7 +1915,10 @@ def main(): e = get_exception() # Ignore a successful exit, or argparse exit if getattr(e, 'code', 1) not in (0, 2): - raise SystemExit('ERROR: %s' % e) + msg = '%s' % e + if not msg: + msg = '%r' % e + raise SystemExit('ERROR: %s' % msg) if __name__ == '__main__': From fb0569946d5a1295f3bff59233c4834f78322e13 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 5 Mar 2019 10:55:57 -0600 Subject: [PATCH 59/67] Bump to 2.1.0 for upcoming release --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 217ee39..a011075 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.0.2' +__version__ = '2.1.0' class FakeShutdownEvent(object): From 69ddff1a11331d20ba5877abe6491082e90e1726 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 5 Mar 2019 11:44:19 -0600 Subject: [PATCH 60/67] Disable py2.4/2.5 tests for now --- .travis.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0380352..dc2c5fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,8 @@ addons: sources: - deadsnakes packages: - - python2.4 - - python2.5 + # - python2.4 + # - python2.5 - python2.6 - python3.2 - python3.3 @@ -15,10 +15,10 @@ matrix: include: - python: 2.7 env: TOXENV=flake8 - - python: 2.7 - env: TOXENV=py24 - - python: 2.7 - env: TOXENV=py25 + # - python: 2.7 + # env: TOXENV=py24 + # - python: 2.7 + # env: TOXENV=py25 - python: 2.7 env: TOXENV=py26 - python: 2.7 From 3109fcf407a948f1a867da122267cfe9ece7150f Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 11 Mar 2019 09:57:19 -0500 Subject: [PATCH 61/67] Update usage --- README.rst | 18 ++++++++++-------- speedtest-cli.1 | 5 +++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index e7af2b7..7a98f31 100644 --- a/README.rst +++ b/README.rst @@ -75,21 +75,23 @@ Usage :: $ speedtest-cli -h - usage: speedtest-cli [-h] [--no-download] [--no-upload] [--bytes] [--share] - [--simple] [--csv] [--csv-delimiter CSV_DELIMITER] - [--csv-header] [--json] [--list] [--server SERVER] - [--exclude EXCLUDE] [--mini MINI] [--source SOURCE] - [--timeout TIMEOUT] [--secure] [--no-pre-allocate] - [--version] - + usage: speedtest-cli [-h] [--no-download] [--no-upload] [--single] [--bytes] + [--share] [--simple] [--csv] + [--csv-delimiter CSV_DELIMITER] [--csv-header] [--json] + [--list] [--server SERVER] [--exclude EXCLUDE] + [--mini MINI] [--source SOURCE] [--timeout TIMEOUT] + [--secure] [--no-pre-allocate] [--version] + Command line interface for testing internet bandwidth using speedtest.net. -------------------------------------------------------------------------- https://github.com/sivel/speedtest-cli - + optional arguments: -h, --help show this help message and exit --no-download Do not perform download test --no-upload Do not perform upload test + --single Only use a single connection instead of multiple. This + simulates a typical file transfer. --bytes Display values in bytes instead of bits. Does not affect the image generated by --share, nor output from --json or --csv diff --git a/speedtest-cli.1 b/speedtest-cli.1 index 87558b4..367c977 100644 --- a/speedtest-cli.1 +++ b/speedtest-cli.1 @@ -33,6 +33,11 @@ Do not perform download test Do not perform upload test .RE +\fB\-\-single\fR +.RS +Only use a single connection instead of multiple. This simulates a typical file transfer. +.RE + \fB\-\-bytes\fR .RS Display values in bytes instead of bits. Does not affect the image generated by \-\-share, nor output from \-\-json or \-\-csv From 2d5a9ef364258621745edafb69c8e592982dac21 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Mon, 11 Mar 2019 10:03:12 -0500 Subject: [PATCH 62/67] Switch copyright from range, to date started --- setup.py | 2 +- speedtest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 29e0f31..a617be4 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2012-2018 Matt Martz +# Copyright 2012 Matt Martz # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/speedtest.py b/speedtest.py index a011075..be93123 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2012-2018 Matt Martz +# Copyright 2012 Matt Martz # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may From 9af203652b286ed320bc21ed2f5378317045ec77 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 12 Mar 2019 10:55:23 -0500 Subject: [PATCH 63/67] Python2.4/2.5 SSL support --- speedtest.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/speedtest.py b/speedtest.py index be93123..475a96d 100755 --- a/speedtest.py +++ b/speedtest.py @@ -97,6 +97,11 @@ except ImportError: except ImportError: HTTPSConnection = None +try: + from httplib import FakeSocket +except ImportError: + FakeSocket = None + try: from Queue import Queue except ImportError: @@ -447,6 +452,20 @@ if HTTPSConnection: self.sock.server_hostname = self.host except AttributeError: pass + elif FakeSocket: + # Python 2.4/2.5 support + try: + self.sock = FakeSocket(self.sock, socket.ssl(self.sock)) + except AttributeError: + raise SpeedtestException( + 'This version of Python does not support HTTPS/SSL ' + 'functionality' + ) + else: + raise SpeedtestException( + 'This version of Python does not support HTTPS/SSL ' + 'functionality' + ) def _build_connection(connection, source_address, timeout, context=None): From cdf60028659ad046b284a3a04792ddc1b760aaa6 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 12 Mar 2019 10:58:17 -0500 Subject: [PATCH 64/67] Bump to 2.1.1 --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 475a96d..e0d15e9 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.1.0' +__version__ = '2.1.1' class FakeShutdownEvent(object): From 681cdf20a5fe774ba3aefedd0cd88cbff5384439 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 12 Mar 2019 11:01:31 -0500 Subject: [PATCH 65/67] Re-enable python 2.4 and 2.5 testing --- .travis.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index dc2c5fc..0380352 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,8 @@ addons: sources: - deadsnakes packages: - # - python2.4 - # - python2.5 + - python2.4 + - python2.5 - python2.6 - python3.2 - python3.3 @@ -15,10 +15,10 @@ matrix: include: - python: 2.7 env: TOXENV=flake8 - # - python: 2.7 - # env: TOXENV=py24 - # - python: 2.7 - # env: TOXENV=py25 + - python: 2.7 + env: TOXENV=py24 + - python: 2.7 + env: TOXENV=py25 - python: 2.7 env: TOXENV=py26 - python: 2.7 From 81bba6070c9069f8770212f119232d0622ddb111 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 13 Mar 2019 15:56:00 -0500 Subject: [PATCH 66/67] Add support for py38 without deprecation warnings (#585) * Add support for py38 without deprecation warnings * Address Py2.5 issue * Add py3.7 and 3.8 * xenial * pypy trusty --- .travis.yml | 7 +++++++ speedtest.py | 44 +++++++++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0380352..c40859c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: python +sudo: required +dist: xenial addons: apt: @@ -33,8 +35,13 @@ matrix: env: TOXENV=py35 - python: 3.6 env: TOXENV=py36 + - python: 3.7 + env: TOXENV=py37 + - python: 3.8-dev + env: TOXENV=py38 - python: pypy env: TOXENV=pypy + dist: trusty before_install: - if [[ $(echo "$TOXENV" | egrep -c "py35") != 0 ]]; then pyenv global system 3.5; fi; diff --git a/speedtest.py b/speedtest.py index e0d15e9..17c52a2 100755 --- a/speedtest.py +++ b/speedtest.py @@ -53,6 +53,9 @@ class FakeShutdownEvent(object): # Some global variables we use DEBUG = False _GLOBAL_DEFAULT_TIMEOUT = object() +PY25PLUS = sys.version_info[:2] >= (2, 5) +PY26PLUS = sys.version_info[:2] >= (2, 6) +PY32PLUS = sys.version_info[:2] >= (3, 2) # Begin import game to handle Python 2 and Python 3 try: @@ -64,14 +67,15 @@ except ImportError: json = None try: - import xml.etree.cElementTree as ET -except ImportError: + import xml.etree.ElementTree as ET try: - import xml.etree.ElementTree as ET + from xml.etree.ElementTree import _Element as ET_Element except ImportError: - from xml.dom import minidom as DOM - from xml.parsers.expat import ExpatError - ET = None + pass +except ImportError: + from xml.dom import minidom as DOM + from xml.parsers.expat import ExpatError + ET = None try: from urllib2 import (urlopen, Request, HTTPError, URLError, @@ -262,6 +266,16 @@ else: write(arg) write(end) +if PY32PLUS: + etree_iter = ET.Element.iter +elif PY25PLUS: + etree_iter = ET_Element.getiterator + +if PY26PLUS: + thread_is_alive = threading.Thread.is_alive +else: + thread_is_alive = threading.Thread.isAlive + # Exception "constants" to support Python 2 through Python 3 try: @@ -1262,7 +1276,7 @@ class Speedtest(object): raise SpeedtestServersError( 'Malformed speedtest.net server list: %s' % e ) - elements = root.getiterator('server') + elements = etree_iter(root, 'server') except AttributeError: try: root = DOM.parseString(serversxml) @@ -1499,9 +1513,10 @@ class Speedtest(object): finished = [] def consumer(q, request_count): + _is_alive = thread_is_alive while len(finished) < request_count: thread = q.get(True) - while thread.isAlive(): + while _is_alive(thread): thread.join(timeout=0.1) finished.append(sum(thread.result)) callback(thread.i, request_count, end=True) @@ -1514,9 +1529,10 @@ class Speedtest(object): start = timeit.default_timer() prod_thread.start() cons_thread.start() - while prod_thread.isAlive(): + _is_alive = thread_is_alive + while _is_alive(prod_thread): prod_thread.join(timeout=0.1) - while cons_thread.isAlive(): + while _is_alive(cons_thread): cons_thread.join(timeout=0.1) stop = timeit.default_timer() @@ -1584,9 +1600,10 @@ class Speedtest(object): finished = [] def consumer(q, request_count): + _is_alive = thread_is_alive while len(finished) < request_count: thread = q.get(True) - while thread.isAlive(): + while _is_alive(thread): thread.join(timeout=0.1) finished.append(thread.result) callback(thread.i, request_count, end=True) @@ -1599,9 +1616,10 @@ class Speedtest(object): start = timeit.default_timer() prod_thread.start() cons_thread.start() - while prod_thread.isAlive(): + _is_alive = thread_is_alive + while _is_alive(prod_thread): prod_thread.join(timeout=0.1) - while cons_thread.isAlive(): + while _is_alive(cons_thread): cons_thread.join(timeout=0.1) stop = timeit.default_timer() From 2658bd50b448e98ce5f3688f66b91aaeba5a897a Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 13 Mar 2019 15:57:05 -0500 Subject: [PATCH 67/67] Bump devel version --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 17c52a2..7be8d65 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.1.1' +__version__ = '2.1.2a' class FakeShutdownEvent(object):