From 9af203652b286ed320bc21ed2f5378317045ec77 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 12 Mar 2019 10:55:23 -0500 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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): From 7ebb9965ddaeb64ecb27efdf06e91d661c129301 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 21 Aug 2019 12:25:32 -0500 Subject: [PATCH 06/12] Ensure threads don't start before a position in the queue is available. Fixes #628 --- speedtest.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/speedtest.py b/speedtest.py index 7be8d65..b7a5124 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1496,6 +1496,9 @@ class Speedtest(object): build_request(url, bump=i, secure=self._secure) ) + max_threads = threads or self.config['threads']['download'] + in_flight = {'threads': 0} + def producer(q, requests, request_count): for i, request in enumerate(requests): thread = HTTPDownloader( @@ -1506,8 +1509,11 @@ class Speedtest(object): opener=self._opener, shutdown_event=self._shutdown_event ) + while in_flight['threads'] >= max_threads: + timeit.time.sleep(0.001) thread.start() q.put(thread, True) + in_flight['threads'] += 1 callback(i, request_count, start=True) finished = [] @@ -1517,11 +1523,12 @@ class Speedtest(object): while len(finished) < request_count: thread = q.get(True) while _is_alive(thread): - thread.join(timeout=0.1) + thread.join(timeout=0.001) + in_flight['threads'] -= 1 finished.append(sum(thread.result)) callback(thread.i, request_count, end=True) - q = Queue(threads or self.config['threads']['download']) + q = Queue(max_threads) prod_thread = threading.Thread(target=producer, args=(q, requests, request_count)) cons_thread = threading.Thread(target=consumer, @@ -1531,9 +1538,9 @@ class Speedtest(object): cons_thread.start() _is_alive = thread_is_alive while _is_alive(prod_thread): - prod_thread.join(timeout=0.1) + prod_thread.join(timeout=0.001) while _is_alive(cons_thread): - cons_thread.join(timeout=0.1) + cons_thread.join(timeout=0.001) stop = timeit.default_timer() self.results.bytes_received = sum(finished) @@ -1582,6 +1589,9 @@ class Speedtest(object): ) ) + 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( @@ -1593,8 +1603,11 @@ class Speedtest(object): opener=self._opener, shutdown_event=self._shutdown_event ) + while in_flight['threads'] >= max_threads: + timeit.time.sleep(0.001) thread.start() q.put(thread, True) + in_flight['threads'] += 1 callback(i, request_count, start=True) finished = [] @@ -1604,7 +1617,8 @@ class Speedtest(object): while len(finished) < request_count: thread = q.get(True) while _is_alive(thread): - thread.join(timeout=0.1) + thread.join(timeout=0.001) + in_flight['threads'] -= 1 finished.append(thread.result) callback(thread.i, request_count, end=True) From 266e53c25636e0dcee31b599c49113d4d7cf8298 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Fri, 5 Jul 2019 13:47:54 -0500 Subject: [PATCH 07/12] Fix proxy support. Fixes #610 --- speedtest.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/speedtest.py b/speedtest.py index b7a5124..eb131ee 100755 --- a/speedtest.py +++ b/speedtest.py @@ -413,6 +413,8 @@ class SpeedtestHTTPConnection(HTTPConnection): 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 @@ -433,17 +435,23 @@ class SpeedtestHTTPConnection(HTTPConnection): self.source_address ) + if self._tunnel_host: + self._tunnel() + if HTTPSConnection: - class SpeedtestHTTPSConnection(HTTPSConnection, - SpeedtestHTTPConnection): + 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 @@ -451,14 +459,30 @@ if HTTPSConnection: 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 + ) - SpeedtestHTTPConnection.connect(self) + if self._tunnel_host: + self._tunnel() if ssl: try: kwargs = {} if hasattr(ssl, 'SSLContext'): - kwargs['server_hostname'] = self.host + 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) From c58ad3367bf27f4b4a4d5b1bca29ebd574731c5d Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 22 Aug 2019 09:48:18 -0500 Subject: [PATCH 08/12] Bump release --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index eb131ee..92a2be0 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.1.2a' +__version__ = '2.1.2' class FakeShutdownEvent(object): From db46af8bcd69dd468e57752551e2e344b28dbc91 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Tue, 19 Jan 2021 17:04:47 -0600 Subject: [PATCH 09/12] Ensure we catch HTTP errors on upload/download. Fixes #752 --- speedtest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index 92a2be0..425f18c 100755 --- a/speedtest.py +++ b/speedtest.py @@ -817,6 +817,8 @@ class HTTPDownloader(threading.Thread): f.close() except IOError: pass + except HTTP_ERRORS: + pass class HTTPUploaderData(object): @@ -882,7 +884,7 @@ class HTTPUploader(threading.Thread): self.request = request self.request.data.start = self.starttime = start self.size = size - self.result = None + self.result = 0 self.timeout = timeout self.i = i @@ -917,6 +919,8 @@ 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): From cadc68b5aef20f28648072cf07a8f155639b81dd Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 8 Apr 2021 08:44:32 -0500 Subject: [PATCH 10/12] Handle case where ignoreids is empty or contains empty ids --- speedtest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/speedtest.py b/speedtest.py index 425f18c..a62a184 100755 --- a/speedtest.py +++ b/speedtest.py @@ -1174,9 +1174,9 @@ class Speedtest(object): # times = get_attributes_by_tag_name(root, 'times') client = get_attributes_by_tag_name(root, 'client') - ignore_servers = list( - map(int, server_config['ignoreids'].split(',')) - ) + ignore_servers = [ + int(i) for i in server_config['ignoreids'].split(',') if i + ] ratio = int(upload['ratio']) upload_max = int(upload['maxchunkcount']) From 42e96b13dda2afabbcec2622612d13495a415caa Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 8 Apr 2021 08:45:29 -0500 Subject: [PATCH 11/12] Bump to 2.1.3 --- speedtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speedtest.py b/speedtest.py index a62a184..a33296d 100755 --- a/speedtest.py +++ b/speedtest.py @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.1.2' +__version__ = '2.1.3' class FakeShutdownEvent(object): From 22210ca35228f0bbcef75a7c14587c4ecb875ab4 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Wed, 7 Jul 2021 14:50:15 -0500 Subject: [PATCH 12/12] Python 3.10 support --- setup.py | 3 +++ speedtest.py | 55 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/setup.py b/setup.py index a617be4..f3d21ad 100644 --- a/setup.py +++ b/setup.py @@ -92,5 +92,8 @@ setup( '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.py b/speedtest.py index a33296d..186b529 100755 --- a/speedtest.py +++ b/speedtest.py @@ -15,18 +15,18 @@ # License for the specific language governing permissions and limitations # under the License. -import os -import re import csv -import sys -import math +import datetime import errno +import math +import os +import platform +import re import signal import socket -import timeit -import datetime -import platform +import sys import threading +import timeit import xml.parsers.expat try: @@ -36,7 +36,7 @@ except ImportError: gzip = None GZIP_BASE = object -__version__ = '2.1.3' +__version__ = '2.1.4b1' class FakeShutdownEvent(object): @@ -49,6 +49,8 @@ class FakeShutdownEvent(object): "Dummy method to always return false""" return False + is_set = isSet + # Some global variables we use DEBUG = False @@ -56,6 +58,7 @@ _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) # Begin import game to handle Python 2 and Python 3 try: @@ -266,17 +269,6 @@ 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: import ssl @@ -293,6 +285,23 @@ 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() + class SpeedtestException(Exception): """Base exception for this module""" @@ -769,7 +778,7 @@ def print_dots(shutdown_event): status """ def inner(current, total, start=False, end=False): - if shutdown_event.isSet(): + if event_is_set(shutdown_event): return sys.stdout.write('.') @@ -808,7 +817,7 @@ class HTTPDownloader(threading.Thread): try: if (timeit.default_timer() - self.starttime) <= self.timeout: f = self._opener(self.request) - while (not self._shutdown_event.isSet() and + while (not event_is_set(self._shutdown_event) and (timeit.default_timer() - self.starttime) <= self.timeout): self.result.append(len(f.read(10240))) @@ -864,7 +873,7 @@ class HTTPUploaderData(object): def read(self, n=10240): if ((timeit.default_timer() - self.start) <= self.timeout and - not self._shutdown_event.isSet()): + not event_is_set(self._shutdown_event)): chunk = self.data.read(n) self.total.append(len(chunk)) return chunk @@ -902,7 +911,7 @@ class HTTPUploader(threading.Thread): request = self.request try: if ((timeit.default_timer() - self.starttime) <= self.timeout and - not self._shutdown_event.isSet()): + not event_is_set(self._shutdown_event)): try: f = self._opener(request) except TypeError: