diff --git a/.travis.yml b/.travis.yml index c40859c..8863201 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python -sudo: required -dist: xenial + +python: + - 2.7 addons: apt: @@ -10,52 +11,26 @@ addons: - python2.4 - python2.5 - python2.6 - - python3.2 - - python3.3 + - pypy -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: 2.7 - env: TOXENV=py32 - - python: 2.7 - env: TOXENV=py33 - - python: 3.4 - env: TOXENV=py34 - - python: 3.5 - 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; +env: + - TOXENV=py24 + - TOXENV=py25 + - TOXENV=py26 + - TOXENV=py27 + - TOXENV=py32 + - TOXENV=py33 + - TOXENV=py34 + - TOXENV=py35 + - TOXENV=pypy + - TOXENV=flake8 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 "(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; - - + - 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 7a98f31..0043b5c 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/travis/sivel/speedtest-cli.svg +.. image:: https://img.shields.io/pypi/dm/speedtest-cli.svg :target: https://pypi.python.org/pypi/speedtest-cli/ - :alt: Travis + :alt: Downloads .. image:: https://img.shields.io/pypi/l/speedtest-cli.svg :target: https://pypi.python.org/pypi/speedtest-cli/ :alt: License @@ -17,7 +17,7 @@ speedtest.net Versions -------- -speedtest-cli works with Python 2.4-3.7 +speedtest-cli works with Python 2.4-3.5 .. image:: https://img.shields.io/pypi/pyversions/speedtest-cli.svg :target: https://pypi.python.org/pypi/speedtest-cli/ @@ -51,8 +51,7 @@ or :: git clone https://github.com/sivel/speedtest-cli.git - cd speedtest-cli - python setup.py install + python speedtest-cli/setup.py install Just download (Like the way it used to be) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -75,10 +74,9 @@ Usage :: $ speedtest-cli -h - 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] + 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] @@ -90,8 +88,6 @@ Usage -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 @@ -110,10 +106,7 @@ Usage affected by --bytes --list Display a list of speedtest.net servers sorted by distance - --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 + --server SERVER Specify a server ID to test against --mini MINI URL of the Speedtest Mini server --source SOURCE Source IP address to bind to --timeout TIMEOUT HTTP timeout in seconds. Default 10 diff --git a/setup.py b/setup.py index f3d21ad..00a8054 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2012 Matt Martz +# Copyright 2012-2016 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'], + py_modules=['speedtest', 'speedtest_cli'], entry_points={ 'console_scripts': [ 'speedtest=speedtest:main', @@ -90,10 +90,5 @@ setup( 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', ] ) diff --git a/speedtest-cli.1 b/speedtest-cli.1 index 367c977..9e1befe 100644 --- a/speedtest-cli.1 +++ b/speedtest-cli.1 @@ -1,4 +1,4 @@ -.TH "speedtest-cli" 1 "2018-01-05" "speedtest-cli" +.TH "speedtest-cli" 1 "2014-04-23" "speedtest-cli" .SH NAME speedtest\-cli \- Command line interface for testing internet bandwidth using speedtest.net .SH SYNOPSIS @@ -23,29 +23,14 @@ 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\-\-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 +Display values in bytes instead of bits. Does not affect the image generated by \-\-share .RE \fB\-\-share\fR .RS -Generate and provide a URL to the speedtest.net share results image, not displayed with \-\-csv +Generate and provide a URL to the speedtest.net share results image .RE \fB\-\-simple\fR @@ -58,12 +43,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 @@ -80,12 +65,7 @@ Display a list of speedtest.net servers sorted by distance \fB\-\-server SERVER\fR .RS -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 +Specify a server ID to test against .RE \fB\-\-mini MINI\fR @@ -108,11 +88,6 @@ 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 diff --git a/speedtest.py b/speedtest.py index 186b529..cb4a374 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2012 Matt Martz +# Copyright 2012-2016 Matt Martz # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -15,18 +15,18 @@ # License for the specific language governing permissions and limitations # under the License. -import csv -import datetime -import errno -import math import os -import platform import re +import csv +import sys +import math +import errno import signal import socket -import sys -import threading import timeit +import datetime +import platform +import threading import xml.parsers.expat try: @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.1.4b1' +__version__ = '1.0.7' class FakeShutdownEvent(object): @@ -49,16 +49,16 @@ class FakeShutdownEvent(object): "Dummy method to always return false""" return False - is_set = isSet - # Some global variables we use +USER_AGENT = None +SOURCE = None +SHUTDOWN_EVENT = FakeShutdownEvent() +SCHEME = 'http' 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) -PY310PLUS = sys.version_info[:2] >= (3, 10) + +# Used for bound_interface +SOCKET_SOCKET = socket.socket # Begin import game to handle Python 2 and Python 3 try: @@ -70,31 +70,23 @@ except ImportError: json = None try: - import xml.etree.ElementTree as ET + import xml.etree.cElementTree as ET +except ImportError: try: - from xml.etree.ElementTree import _Element as ET_Element + import xml.etree.ElementTree as ET except ImportError: - pass -except ImportError: - from xml.dom import minidom as DOM - from xml.parsers.expat import ExpatError - ET = None + from xml.dom import minidom as DOM + ET = None try: - from urllib2 import (urlopen, Request, HTTPError, URLError, - AbstractHTTPHandler, ProxyHandler, - HTTPDefaultErrorHandler, HTTPRedirectHandler, - HTTPErrorProcessor, OpenerDirector) + from urllib2 import urlopen, Request, HTTPError, URLError except ImportError: - from urllib.request import (urlopen, Request, HTTPError, URLError, - AbstractHTTPHandler, ProxyHandler, - HTTPDefaultErrorHandler, HTTPRedirectHandler, - HTTPErrorProcessor, OpenerDirector) + from urllib.request import urlopen, Request, HTTPError, URLError try: - from httplib import HTTPConnection, BadStatusLine + from httplib import HTTPConnection except ImportError: - from http.client import HTTPConnection, BadStatusLine + from http.client import HTTPConnection try: from httplib import HTTPSConnection @@ -104,11 +96,6 @@ except ImportError: except ImportError: HTTPSConnection = None -try: - from httplib import FakeSocket -except ImportError: - FakeSocket = None - try: from Queue import Queue except ImportError: @@ -137,13 +124,11 @@ 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 @@ -161,31 +146,24 @@ except ImportError: import builtins from io import TextIOWrapper, FileIO - class _Py3Utf8Output(TextIOWrapper): + class _Py3Utf8Stdout(TextIOWrapper): """UTF-8 encoded wrapper around stdout for py3, to override ASCII stdout """ - def __init__(self, f, **kwargs): - buf = FileIO(f.fileno(), 'w') - super(_Py3Utf8Output, self).__init__( + def __init__(self, **kwargs): + buf = FileIO(sys.stdout.fileno(), 'w') + super(_Py3Utf8Stdout, self).__init__( buf, encoding='utf8', errors='strict' ) def write(self, s): - super(_Py3Utf8Output, self).write(s) + super(_Py3Utf8Stdout, self).write(s) self.flush() _py3_print = getattr(builtins, 'print') - 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 + _py3_utf8_stdout = _Py3Utf8Stdout() def to_utf8(v): """No-op encode to utf-8 for py3""" @@ -193,10 +171,7 @@ except ImportError: def print_(*args, **kwargs): """Wrapper function for py3 to print, with a utf-8 encoded stdout""" - if kwargs.get('file') == sys.stderr: - kwargs['file'] = _py3_utf8_stderr - else: - kwargs['file'] = kwargs.get('file', _py3_utf8_stdout) + kwargs['file'] = _py3_utf8_stdout _py3_print(*args, **kwargs) else: del __builtin__ @@ -213,7 +188,7 @@ else: Taken from https://pypi.python.org/pypi/six/ - Modified to set encoding to UTF-8 always, and to flush after write + Modified to set encoding to UTF-8 always """ fp = kwargs.pop("file", sys.stdout) if fp is None: @@ -232,7 +207,6 @@ 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: @@ -269,6 +243,7 @@ else: write(arg) write(end) + # Exception "constants" to support Python 2 through Python 3 try: import ssl @@ -277,30 +252,10 @@ try: except AttributeError: CERT_ERROR = tuple() - HTTP_ERRORS = ( - (HTTPError, URLError, socket.error, ssl.SSLError, BadStatusLine) + - CERT_ERROR - ) + HTTP_ERRORS = ((HTTPError, URLError, socket.error, ssl.SSLError) + + CERT_ERROR) except ImportError: - ssl = None - HTTP_ERRORS = (HTTPError, URLError, socket.error, BadStatusLine) - -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 - - -def event_is_set(event): - try: - return event.is_set() - except AttributeError: - return event.isSet() + HTTP_ERRORS = (HTTPError, URLError, socket.error) class SpeedtestException(Exception): @@ -316,11 +271,7 @@ class SpeedtestHTTPError(SpeedtestException): class SpeedtestConfigError(SpeedtestException): - """Configuration XML is invalid""" - - -class SpeedtestServersError(SpeedtestException): - """Servers XML is invalid""" + """Configuration provided is invalid""" class ConfigRetrievalError(SpeedtestHTTPError): @@ -369,253 +320,6 @@ 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. - - 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. - - Largely vendored from Python 2.7, modified to work with Python 2.4 - """ - - 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): - """Custom HTTPConnection to support source_address across - Python 2.4 - Python 3 - """ - def __init__(self, *args, **kwargs): - source_address = kwargs.pop('source_address', None) - timeout = kwargs.pop('timeout', 10) - - self._tunnel_host = None - - HTTPConnection.__init__(self, *args, **kwargs) - - self.source_address = source_address - self.timeout = timeout - - def connect(self): - """Connect to the host and port specified in __init__.""" - 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 self._tunnel_host: - self._tunnel() - - -if HTTPSConnection: - class SpeedtestHTTPSConnection(HTTPSConnection): - """Custom HTTPSConnection to support source_address across - Python 2.4 - Python 3 - """ - default_port = 443 - - def __init__(self, *args, **kwargs): - source_address = kwargs.pop('source_address', None) - timeout = kwargs.pop('timeout', 10) - - self._tunnel_host = None - - HTTPSConnection.__init__(self, *args, **kwargs) - - self.timeout = timeout - self.source_address = source_address - - def connect(self): - "Connect to a host on a given (SSL) port." - 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 self._tunnel_host: - self._tunnel() - - if ssl: - try: - kwargs = {} - if hasattr(ssl, 'SSLContext'): - if self._tunnel_host: - kwargs['server_hostname'] = self._tunnel_host - else: - kwargs['server_hostname'] = self.host - self.sock = self._context.wrap_socket(self.sock, **kwargs) - except AttributeError: - self.sock = ssl.wrap_socket(self.sock) - try: - 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): - """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, - 'timeout': timeout - }) - if context: - kwargs['context'] = context - return connection(host, **kwargs) - return inner - - -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 - 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): - """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) - 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, - self.timeout, - context=self._context, - ), - req - ) - - https_request = AbstractHTTPHandler.do_request_ - - -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` - """ - - 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 - - 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. @@ -653,6 +357,14 @@ 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""" @@ -675,32 +387,37 @@ 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.platform(), - platform.architecture()[0]), + '(%s; U; %s; en-us)' % (platform.system(), platform.architecture()[0]), 'Python/%s' % platform.python_version(), '(KHTML, like Gecko)', 'speedtest-cli/%s' % __version__ ) - user_agent = ' '.join(ua_tuple) - printer('User-Agent: %s' % 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='0', secure=False): +def build_request(url, data=None, headers=None, bump=''): """Build a urllib2 request object This function automatically adds a User-Agent header to all requests """ + if not USER_AGENT: + build_user_agent() + if not headers: headers = {} if url[0] == ':': - scheme = ('http', 'https')[bool(secure)] - schemed_url = '%s%s' % (scheme, url) + schemed_url = '%s%s' % (SCHEME, url) else: schemed_url = url @@ -715,6 +432,7 @@ def build_request(url, data=None, headers=None, bump='0', secure=False): bump) headers.update({ + 'User-Agent': USER_AGENT, 'Cache-Control': 'no-cache', }) @@ -724,21 +442,14 @@ def build_request(url, data=None, headers=None, bump='0', secure=False): return Request(final_url, data=data, headers=headers) -def catch_request(request, opener=None): +def catch_request(request): """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 = _open(request) - if request.get_full_url() != uh.geturl(): - printer('Redirected to %s' % uh.geturl(), debug=True) + uh = urlopen(request) return uh, False except HTTP_ERRORS: e = get_exception() @@ -773,19 +484,18 @@ def get_attributes_by_tag_name(dom, tag_name): return dict(list(elem.attributes.items())) -def print_dots(shutdown_event): +def print_dots(current, total, start=False, end=False): """Built in callback function used by Thread classes for printing status """ - def inner(current, total, start=False, end=False): - if event_is_set(shutdown_event): - return - sys.stdout.write('.') - if current + 1 == total and end is True: - sys.stdout.write('\n') - sys.stdout.flush() - return inner + if SHUTDOWN_EVENT.isSet(): + return + + sys.stdout.write('.') + if current + 1 == total and end is True: + sys.stdout.write('\n') + sys.stdout.flush() def do_nothing(*args, **kwargs): @@ -795,29 +505,19 @@ def do_nothing(*args, **kwargs): class HTTPDownloader(threading.Thread): """Thread class for retrieving a URL""" - def __init__(self, i, request, start, timeout, opener=None, - shutdown_event=None): + def __init__(self, i, request, start, timeout): 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 - - 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 event_is_set(self._shutdown_event) and + f = urlopen(self.request) + while (not SHUTDOWN_EVENT.isSet() and (timeit.default_timer() - self.starttime) <= self.timeout): self.result.append(len(f.read(10240))) @@ -826,8 +526,6 @@ class HTTPDownloader(threading.Thread): f.close() except IOError: pass - except HTTP_ERRORS: - pass class HTTPUploaderData(object): @@ -835,16 +533,11 @@ class HTTPUploaderData(object): has been reached """ - def __init__(self, length, start, timeout, shutdown_event=None): + def __init__(self, length, start, timeout): 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] @@ -853,17 +546,11 @@ class HTTPUploaderData(object): chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' multiplier = int(round(int(self.length) / 36.0)) IO = BytesIO or StringIO - 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' - ) + self._data = IO( + ('content1=%s' % + (chars * multiplier)[0:int(self.length) - 9] + ).encode() + ) @property def data(self): @@ -873,7 +560,7 @@ class HTTPUploaderData(object): def read(self, n=10240): if ((timeit.default_timer() - self.start) <= self.timeout and - not event_is_set(self._shutdown_event)): + not SHUTDOWN_EVENT.isSet()): chunk = self.data.read(n) self.total.append(len(chunk)) return chunk @@ -887,40 +574,29 @@ class HTTPUploaderData(object): class HTTPUploader(threading.Thread): """Thread class for putting a URL""" - def __init__(self, i, request, start, size, timeout, opener=None, - shutdown_event=None): + def __init__(self, i, request, start, size, timeout): threading.Thread.__init__(self) self.request = request self.request.data.start = self.starttime = start self.size = size - self.result = 0 + self.result = None self.timeout = timeout self.i = i - if opener: - self._opener = opener.open - 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 event_is_set(self._shutdown_event)): + not SHUTDOWN_EVENT.isSet()): try: - f = self._opener(request) + f = urlopen(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 = self._opener(request) + f = urlopen(request) f.read(11) f.close() self.result = sum(self.request.data.total) @@ -928,8 +604,6 @@ class HTTPUploader(threading.Thread): self.result = 0 except (IOError, SpeedtestUploadTimeout): self.result = sum(self.request.data.total) - except HTTP_ERRORS: - self.result = 0 class SpeedtestResults(object): @@ -945,8 +619,7 @@ class SpeedtestResults(object): to get a share results image link. """ - def __init__(self, download=0, upload=0, ping=0, server=None, client=None, - opener=None, secure=False): + def __init__(self, download=0, upload=0, ping=0, server=None): self.download = download self.upload = upload self.ping = ping @@ -954,20 +627,11 @@ 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 self.bytes_sent = 0 - if opener: - self._opener = opener - else: - self._opener = build_opener() - - self._secure = secure - def __repr__(self): return repr(self.dict()) @@ -1009,8 +673,8 @@ 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, secure=self._secure) - f, e = catch_request(request, opener=self._opener) + headers=headers) + f, e = catch_request(request) if e: raise ShareResultsConnectFailure(e) @@ -1044,20 +708,8 @@ class SpeedtestResults(object): 'bytes_sent': self.bytes_sent, 'bytes_received': self.bytes_received, 'share': self._share, - '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""" @@ -1067,7 +719,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'], self._share or '', self.client['ip']] + data['upload']] writer.writerow([to_utf8(v) for v in row]) return out.getvalue() @@ -1086,40 +738,17 @@ class SpeedtestResults(object): class Speedtest(object): """Class for performing standard speedtest.net testing operations""" - def __init__(self, config=None, source_address=None, timeout=10, - secure=False, shutdown_event=None): + def __init__(self, config=None): self.config = {} - - self._source_address = source_address - self._timeout = timeout - self._opener = build_opener(source_address, timeout) - - 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) self.servers = {} self.closest = [] - self._best = {} + self.best = {} - self.results = SpeedtestResults( - client=self.config['client'], - opener=self._opener, - secure=secure, - ) - - @property - def best(self): - if not self._best: - self.get_best_server() - return self._best + self.results = SpeedtestResults() def get_config(self): """Download the speedtest.net configuration and return only the data @@ -1130,20 +759,17 @@ class Speedtest(object): if gzip: headers['Accept-Encoding'] = 'gzip' request = build_request('://www.speedtest.net/speedtest-config.php', - headers=headers, secure=self._secure) - uh, e = catch_request(request, opener=self._opener) + headers=headers) + uh, e = catch_request(request) if e: raise ConfigRetrievalError(e) - configxml_list = [] + configxml = [] stream = get_response_stream(uh) while 1: - try: - configxml_list.append(stream.read(1024)) - except (OSError, EOFError): - raise ConfigRetrievalError(get_exception()) - if len(configxml_list[-1]) == 0: + configxml.append(stream.read(1024)) + if len(configxml[-1]) == 0: break stream.close() uh.close() @@ -1151,18 +777,10 @@ class Speedtest(object): if int(uh.code) != 200: return None - configxml = ''.encode().join(configxml_list) - - printer('Config XML:\n%s' % configxml, debug=True) + printer(''.encode().join(configxml), debug=True) try: - try: - root = ET.fromstring(configxml) - except ET.ParseError: - e = get_exception() - raise SpeedtestConfigError( - 'Malformed speedtest.net configuration: %s' % e - ) + root = ET.fromstring(''.encode().join(configxml)) server_config = root.find('server-config').attrib download = root.find('download').attrib upload = root.find('upload').attrib @@ -1170,22 +788,16 @@ class Speedtest(object): client = root.find('client').attrib except AttributeError: - try: - root = DOM.parseString(configxml) - except ExpatError: - e = get_exception() - raise SpeedtestConfigError( - 'Malformed speedtest.net configuration: %s' % e - ) + root = DOM.parseString(''.join(configxml)) 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') # times = get_attributes_by_tag_name(root, 'times') client = get_attributes_by_tag_name(root, 'client') - ignore_servers = [ - int(i) for i in server_config['ignoreids'].split(',') if i - ] + ignore_servers = list( + map(int, server_config['ignoreids'].split(',')) + ) ratio = int(upload['ratio']) upload_max = int(upload['maxchunkcount']) @@ -1225,38 +837,27 @@ class Speedtest(object): 'upload_max': upload_count * size_count }) - 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')) - ) + self.lat_lon = (float(client['lat']), float(client['lon'])) - printer('Config:\n%r' % self.config, debug=True) + printer(self.config, debug=True) return self.config - def get_servers(self, servers=None, exclude=None): + def get_servers(self, servers=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 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 - ) + 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) urls = [ '://www.speedtest.net/speedtest-servers-static.php', @@ -1272,26 +873,21 @@ class Speedtest(object): errors = [] for url in urls: try: - request = build_request( - '%s?threads=%s' % (url, - self.config['threads']['download']), - headers=headers, - secure=self._secure - ) - uh, e = catch_request(request, opener=self._opener) + request = build_request('%s?threads=%s' % + (url, + self.config['threads']['download']), + headers=headers) + uh, e = catch_request(request) if e: errors.append('%s' % e) raise ServersRetrievalError() stream = get_response_stream(uh) - serversxml_list = [] + serversxml = [] while 1: - try: - serversxml_list.append(stream.read(1024)) - except (OSError, EOFError): - raise ServersRetrievalError(get_exception()) - if len(serversxml_list[-1]) == 0: + serversxml.append(stream.read(1024)) + if len(serversxml[-1]) == 0: break stream.close() @@ -1300,28 +896,14 @@ class Speedtest(object): if int(uh.code) != 200: raise ServersRetrievalError() - serversxml = ''.encode().join(serversxml_list) - - printer('Servers XML:\n%s' % serversxml, debug=True) + printer(''.encode().join(serversxml), debug=True) try: try: - try: - root = ET.fromstring(serversxml) - except ET.ParseError: - e = get_exception() - raise SpeedtestServersError( - 'Malformed speedtest.net server list: %s' % e - ) - elements = etree_iter(root, 'server') + root = ET.fromstring(''.encode().join(serversxml)) + elements = root.getiterator('server') except AttributeError: - try: - root = DOM.parseString(serversxml) - except ExpatError: - e = get_exception() - raise SpeedtestServersError( - 'Malformed speedtest.net server list: %s' % e - ) + root = DOM.parseString(''.join(serversxml)) elements = root.getElementsByTagName('server') except (SyntaxError, xml.parsers.expat.ExpatError): raise ServersRetrievalError() @@ -1335,15 +917,14 @@ class Speedtest(object): if servers and int(attrib.get('id')) not in servers: continue - if (int(attrib.get('id')) in self.config['ignore_servers'] - or int(attrib.get('id')) in exclude): + if int(attrib.get('id')) in self.config['ignore_servers']: continue try: d = distance(self.lat_lon, (float(attrib.get('lat')), float(attrib.get('lon')))) - except Exception: + except: continue attrib['d'] = d @@ -1353,12 +934,14 @@ class Speedtest(object): except KeyError: self.servers[d] = [attrib] + printer(''.encode().join(serversxml), debug=True) + break except ServersRetrievalError: continue - if (servers or exclude) and not self.servers: + if servers and not self.servers: raise NoMatchedServers() return self.servers @@ -1377,7 +960,7 @@ class Speedtest(object): url = server request = build_request(url) - uh, e = catch_request(request, opener=self._opener) + uh, e = catch_request(request) if e: raise SpeedtestMiniConnectFailure('Failed to connect to %s' % server) @@ -1390,10 +973,8 @@ class Speedtest(object): if not extension: for ext in ['php', 'asp', 'aspx', 'jsp']: try: - f = self._opener.open( - '%s/speedtest/upload.%s' % (url, ext) - ) - except Exception: + f = urlopen('%s/speedtest/upload.%s' % (url, ext)) + except: pass else: data = f.read().strip().decode() @@ -1434,7 +1015,7 @@ class Speedtest(object): continue break - printer('Closest Servers:\n%r' % self.closest, debug=True) + printer(self.closest, debug=True) return self.closest def get_best_server(self, servers=None): @@ -1447,44 +1028,26 @@ 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 = [] url = os.path.dirname(server['url']) - 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) + urlparts = urlparse('%s/latency.txt' % url) + printer('%s %s/latency.txt' % ('GET', url), debug=True) + for _ in range(0, 3): try: if urlparts[0] == 'https': - h = SpeedtestHTTPSConnection( - urlparts[1], - source_address=source_address_tuple - ) + h = HTTPSConnection(urlparts[1]) else: - h = SpeedtestHTTPConnection( - urlparts[1], - source_address=source_address_tuple - ) - headers = {'User-Agent': user_agent} - path = '%s?%s' % (urlparts[2], urlparts[4]) + h = HTTPConnection(urlparts[1]) + headers = {'User-Agent': USER_AGENT} start = timeit.default_timer() - h.request("GET", path, headers=headers) + h.request("GET", urlparts[2], headers=headers) r = h.getresponse() total = (timeit.default_timer() - start) except HTTP_ERRORS: e = get_exception() - printer('ERROR: %r' % e, debug=True) + printer('%r' % e, debug=True) cum.append(3600) continue @@ -1509,16 +1072,12 @@ class Speedtest(object): self.results.ping = fastest self.results.server = best - self._best.update(best) - printer('Best Server:\n%r' % best, debug=True) + self.best.update(best) + printer(best, debug=True) return best - 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 - """ + def download(self, callback=do_nothing): + """Test download speed against speedtest.net""" urls = [] for size in self.config['sizes']['download']: @@ -1529,43 +1088,27 @@ class Speedtest(object): request_count = len(urls) requests = [] for i, url in enumerate(urls): - requests.append( - build_request(url, bump=i, secure=self._secure) - ) - - max_threads = threads or self.config['threads']['download'] - in_flight = {'threads': 0} + requests.append(build_request(url, bump=i)) def producer(q, requests, request_count): for i, request in enumerate(requests): - thread = HTTPDownloader( - i, - request, - start, - self.config['length']['download'], - opener=self._opener, - shutdown_event=self._shutdown_event - ) - while in_flight['threads'] >= max_threads: - timeit.time.sleep(0.001) + thread = HTTPDownloader(i, request, start, + self.config['length']['download']) thread.start() q.put(thread, True) - in_flight['threads'] += 1 callback(i, request_count, start=True) finished = [] def consumer(q, request_count): - _is_alive = thread_is_alive while len(finished) < request_count: thread = q.get(True) - while _is_alive(thread): - thread.join(timeout=0.001) - in_flight['threads'] -= 1 + while thread.isAlive(): + thread.join(timeout=0.1) finished.append(sum(thread.result)) callback(thread.i, request_count, end=True) - q = Queue(max_threads) + q = Queue(self.config['threads']['download']) prod_thread = threading.Thread(target=producer, args=(q, requests, request_count)) cons_thread = threading.Thread(target=consumer, @@ -1573,11 +1116,10 @@ class Speedtest(object): start = timeit.default_timer() prod_thread.start() cons_thread.start() - _is_alive = thread_is_alive - while _is_alive(prod_thread): - prod_thread.join(timeout=0.001) - while _is_alive(cons_thread): - cons_thread.join(timeout=0.001) + while prod_thread.isAlive(): + prod_thread.join(timeout=0.1) + while cons_thread.isAlive(): + cons_thread.join(timeout=0.1) stop = timeit.default_timer() self.results.bytes_received = sum(finished) @@ -1588,12 +1130,8 @@ class Speedtest(object): self.config['threads']['upload'] = 8 return self.results.download - 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 - """ + def upload(self, callback=do_nothing, pre_allocate=True): + """Test upload speed against speedtest.net""" sizes = [] @@ -1608,58 +1146,35 @@ 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'], - shutdown_event=self._shutdown_event - ) + data = HTTPUploaderData(size, 0, self.config['length']['upload']) if pre_allocate: data.pre_allocate() - - headers = {'Content-length': size} requests.append( ( - build_request(self.best['url'], data, secure=self._secure, - headers=headers), + build_request(self.best['url'], data), size ) ) - max_threads = threads or self.config['threads']['upload'] - in_flight = {'threads': 0} - 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, - shutdown_event=self._shutdown_event - ) - while in_flight['threads'] >= max_threads: - timeit.time.sleep(0.001) + thread = HTTPUploader(i, request[0], start, request[1], + self.config['length']['upload']) thread.start() q.put(thread, True) - in_flight['threads'] += 1 callback(i, request_count, start=True) finished = [] def consumer(q, request_count): - _is_alive = thread_is_alive while len(finished) < request_count: thread = q.get(True) - while _is_alive(thread): - thread.join(timeout=0.001) - in_flight['threads'] -= 1 + while thread.isAlive(): + thread.join(timeout=0.1) finished.append(thread.result) callback(thread.i, request_count, end=True) - q = Queue(threads or self.config['threads']['upload']) + q = Queue(self.config['threads']['upload']) prod_thread = threading.Thread(target=producer, args=(q, requests, request_count)) cons_thread = threading.Thread(target=consumer, @@ -1667,10 +1182,9 @@ class Speedtest(object): start = timeit.default_timer() prod_thread.start() cons_thread.start() - _is_alive = thread_is_alive - while _is_alive(prod_thread): + while prod_thread.isAlive(): prod_thread.join(timeout=0.1) - while _is_alive(cons_thread): + while cons_thread.isAlive(): cons_thread.join(timeout=0.1) stop = timeit.default_timer() @@ -1681,29 +1195,32 @@ class Speedtest(object): return self.results.upload -def ctrl_c(shutdown_event): +def ctrl_c(signum, frame): """Catch Ctrl-C key sequence and set a SHUTDOWN_EVENT for our threaded operations """ - def inner(signum, frame): - shutdown_event.set() - printer('\nCancelling...', error=True) - sys.exit(0) - return inner + + SHUTDOWN_EVENT.set() + print_('\nCancelling...') + sys.exit(0) def version(): """Print the version""" - printer('speedtest-cli %s' % __version__) - printer('Python %s' % sys.version.replace('\n', '')) + print_(__version__) sys.exit(0) def csv_header(delimiter=','): """Print the CSV Headers""" - printer(SpeedtestResults.csv_header(delimiter=delimiter)) + row = ['Server ID', 'Sponsor', 'Server Name', 'Timestamp', 'Distance', + 'Ping', 'Download', 'Upload'] + out = StringIO() + writer = csv.writer(out, delimiter=delimiter, lineterminator='') + writer.writerow([to_utf8(v) for v in row]) + print_(out.getvalue()) sys.exit(0) @@ -1729,10 +1246,6 @@ 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 ' @@ -1760,15 +1273,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', type=PARSER_TYPE_INT, action='append', - 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') + parser.add_argument('--server', help='Specify a server ID to test against', + type=PARSER_TYPE_INT) 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_FLOAT, + parser.add_argument('--timeout', default=10, type=PARSER_TYPE_INT, help='HTTP timeout in seconds. Default 10') parser.add_argument('--secure', action='store_true', help='Use HTTPS instead of HTTP when communicating ' @@ -1811,23 +1320,17 @@ def validate_optional_args(args): 'unavailable' % (info[0], arg)) -def printer(string, quiet=False, debug=False, error=False, **kwargs): - """Helper function print a string with various features""" +def printer(string, quiet=False, debug=False, **kwargs): + """Helper function to print a string only when not quiet""" if debug and not DEBUG: return if debug: - if sys.stdout.isatty(): - out = '\033[1;30mDEBUG: %s\033[0m' % string - else: - out = 'DEBUG: %s' % string + out = '\033[1;30mDEBUG: %s\033[0m' % string else: out = string - if error: - kwargs['file'] = sys.stderr - if not quiet: print_(out, **kwargs) @@ -1835,10 +1338,10 @@ def printer(string, quiet=False, debug=False, error=False, **kwargs): def shell(): """Run the full speedtest.net test""" - global DEBUG - shutdown_event = threading.Event() + global SHUTDOWN_EVENT, SOURCE, SCHEME, DEBUG + SHUTDOWN_EVENT = threading.Event() - signal.signal(signal.SIGINT, ctrl_c(shutdown_event)) + signal.signal(signal.SIGINT, ctrl_c) args = parse_args() @@ -1858,12 +1361,25 @@ 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' + debug = getattr(args, 'debug', False) if debug == 'SUPPRESSHELP': debug = False 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: @@ -1878,24 +1394,20 @@ def shell(): if quiet or debug: callback = do_nothing else: - callback = print_dots(shutdown_event) + callback = print_dots printer('Retrieving speedtest.net configuration...', quiet) try: - speedtest = Speedtest( - source_address=args.source, - timeout=args.timeout, - secure=args.secure - ) - except (ConfigRetrievalError,) + HTTP_ERRORS: - printer('Cannot retrieve speedtest configuration', error=True) + speedtest = Speedtest() + except (ConfigRetrievalError, HTTP_ERRORS): + printer('Cannot retrieve speedtest configuration') raise SpeedtestCLIError(get_exception()) if args.list: try: speedtest.get_servers() - except (ServersRetrievalError,) + HTTP_ERRORS: - printer('Cannot retrieve speedtest server list', error=True) + except (ServersRetrievalError, HTTP_ERRORS): + print_('Cannot retrieve speedtest server list') raise SpeedtestCLIError(get_exception()) for _, servers in sorted(speedtest.servers.items()): @@ -1903,38 +1415,35 @@ def shell(): line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) ' '[%(d)0.2f km]' % server) try: - printer(line) + print_(line) except IOError: e = get_exception() if e.errno != errno.EPIPE: 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=args.server, exclude=args.exclude) + speedtest.get_servers(servers) except NoMatchedServers: - raise SpeedtestCLIError( - 'No matched servers: %s' % - ', '.join('%s' % s for s in args.server) - ) - except (ServersRetrievalError,) + HTTP_ERRORS: - printer('Cannot retrieve speedtest server list', error=True) + raise SpeedtestCLIError('No matched servers: %s' % 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' % ', '.join('%s' % s for s in args.server) - ) + raise SpeedtestCLIError('%s is an invalid server type, must ' + 'be an int' % args.server) - 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) + 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)) @@ -1947,48 +1456,38 @@ def shell(): if args.download: printer('Testing download speed', quiet, end=('', '\n')[bool(debug)]) - speedtest.download( - callback=callback, - threads=(None, 1)[args.single] - ) + speedtest.download(callback=callback) printer('Download: %0.2f M%s/s' % ((results.download / 1000.0 / 1000.0) / args.units[1], args.units[0]), quiet) else: - printer('Skipping download test', quiet) + printer('Skipping download test') if args.upload: printer('Testing upload speed', quiet, end=('', '\n')[bool(debug)]) - speedtest.upload( - callback=callback, - pre_allocate=args.pre_allocate, - threads=(None, 1)[args.single] - ) + speedtest.upload(callback=callback, pre_allocate=args.pre_allocate) printer('Upload: %0.2f M%s/s' % ((results.upload / 1000.0 / 1000.0) / args.units[1], args.units[0]), quiet) else: - printer('Skipping upload test', quiet) - - printer('Results:\n%r' % results.dict(), debug=True) - - if not args.simple and args.share: - results.share() + printer('Skipping upload test') if args.simple: - 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])) + 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])) elif args.csv: - printer(results.csv(delimiter=args.csv_delimiter)) + print_(results.csv(delimiter=args.csv_delimiter)) elif args.json: - printer(results.json()) + if args.share: + results.share() + print_(results.json()) if args.share and not machine_format: printer('Share results: %s' % results.share()) @@ -1998,15 +1497,11 @@ def main(): try: shell() except KeyboardInterrupt: - printer('\nCancelling...', error=True) + print_('\nCancelling...') except (SpeedtestException, SystemExit): e = get_exception() - # Ignore a successful exit, or argparse exit - if getattr(e, 'code', 1) not in (0, 2): - msg = '%s' % e - if not msg: - msg = '%r' % e - raise SystemExit('ERROR: %s' % msg) + if getattr(e, 'code', 1) != 0: + raise SystemExit('ERROR: %s' % e) if __name__ == '__main__': diff --git a/tests/scripts/source.py b/speedtest_cli.py similarity index 53% rename from tests/scripts/source.py rename to speedtest_cli.py index 357f4c6..1d0fb29 100644 --- a/tests/scripts/source.py +++ b/speedtest_cli.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2018 Matt Martz +# Copyright 2012-2016 Matt Martz # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -15,23 +15,20 @@ # License for the specific language governing permissions and limitations # under the License. -import sys -import subprocess +import warnings -cmd = [sys.executable, 'speedtest.py', '--source', '127.0.0.1'] +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') -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() - ) +if __name__ == '__main__': + raise SystemExit(DEPRECATED_MSG) +else: + try: + from speedtest import * + except ImportError: + raise SystemExit(DEPRECATED_MSG) + else: + warnings.warn(DEPRECATED_MSG, UserWarning) diff --git a/tox.ini b/tox.ini index 8a63b5b..477fa1c 100644 --- a/tox.ini +++ b/tox.ini @@ -6,8 +6,6 @@ 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 @@ -21,5 +19,3 @@ commands = pypy -V pypy -m compileall speedtest.py pypy speedtest.py - pypy speedtest.py --source 172.17.0.1 - pypy tests/scripts/source.py