Skip to content

Commit

Permalink
Merge pull request #363 from pmdarrow/python3
Browse files Browse the repository at this point in the history
Python 3 support
  • Loading branch information
pmdarrow committed Jun 13, 2016
2 parents 32ce544 + 933151e commit 9e202a3
Show file tree
Hide file tree
Showing 22 changed files with 144 additions and 122 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ dist/**
.vagrant
build/
.coverage
.tox/
22 changes: 16 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
sudo: false
language: python
python:
- "2.6"
- "2.7"
# command to install dependencies
# Workaround for https://github.com/travis-ci/travis-ci/issues/4794
- 3.5
env:
- TOXENV=py26
- TOXENV=py27
- TOXENV=py33
- TOXENV=py34
- TOXENV=py35
addons:
apt:
packages:
- libevent-dev
install:
- sudo apt-get install -y libevent-dev
# command to run tests
script: python setup.py test
- pip install tox
script:
- tox
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
test:
python setup.py test
unit2 discover

release:
python setup.py sdist upload
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,5 @@ Open source licensed under the MIT license (see _LICENSE_ file for details).

## Supported Python Versions

Locust requires **Python 2.6+**. It is not currently compatible with Python 3.x.
Locust supports Python 2.6, 2.7 and 3.4.

4 changes: 2 additions & 2 deletions locust/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from core import HttpLocust, Locust, TaskSet, task
from exception import InterruptTaskSet, ResponseError, RescheduleTaskImmediately
from .core import HttpLocust, Locust, TaskSet, task
from .exception import InterruptTaskSet, ResponseError, RescheduleTaskImmediately

__version__ = "0.7.5"
9 changes: 5 additions & 4 deletions locust/clients.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import re
import time
from datetime import timedelta
from urlparse import urlparse, urlunparse
from six.moves.urllib.parse import urlparse, urlunparse
import six

import requests
from requests import Response, Request
from requests.auth import HTTPBasicAuth
from requests.exceptions import (RequestException, MissingSchema,
InvalidSchema, InvalidURL)

import events
from exception import CatchResponseError, ResponseError
from . import events
from .exception import CatchResponseError, ResponseError

absolute_http_url_regexp = re.compile(r"^https?://", re.I)

Expand Down Expand Up @@ -235,7 +236,7 @@ def failure(self, exc):
if response.content == "":
response.failure("No data")
"""
if isinstance(exc, basestring):
if isinstance(exc, six.string_types):
exc = CatchResponseError(exc)

events.request_failure.fire(
Expand Down
27 changes: 14 additions & 13 deletions locust/core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import gevent
from gevent import monkey, GreenletExit
import six
from six.moves import xrange

monkey.patch_all(thread=False)

Expand All @@ -10,10 +12,10 @@
import traceback
import logging

from clients import HttpSession
import events
from .clients import HttpSession
from . import events

from exception import LocustError, InterruptTaskSet, RescheduleTask, RescheduleTaskImmediately, StopLocust
from .exception import LocustError, InterruptTaskSet, RescheduleTask, RescheduleTaskImmediately, StopLocust

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -103,7 +105,7 @@ def run(self):
except StopLocust:
pass
except (RescheduleTask, RescheduleTaskImmediately) as e:
raise LocustError, LocustError("A task inside a Locust class' main TaskSet (`%s.task_set` of type `%s`) seems to have called interrupt() or raised an InterruptTaskSet exception. The interrupt() function is used to hand over execution to a parent TaskSet, and should never be called in the main TaskSet which a Locust class' task_set attribute points to." % (type(self).__name__, self.task_set.__name__)), sys.exc_info()[2]
six.reraise(LocustError, LocustError("A task inside a Locust class' main TaskSet (`%s.task_set` of type `%s`) seems to have called interrupt() or raised an InterruptTaskSet exception. The interrupt() function is used to hand over execution to a parent TaskSet, and should never be called in the main TaskSet which a Locust class' task_set attribute points to." % (type(self).__name__, self.task_set.__name__)), sys.exc_info()[2])


class HttpLocust(Locust):
Expand Down Expand Up @@ -146,7 +148,7 @@ def __new__(mcs, classname, bases, classDict):
if "tasks" in classDict and classDict["tasks"] is not None:
tasks = classDict["tasks"]
if isinstance(tasks, dict):
tasks = list(tasks.iteritems())
tasks = six.iteritems(tasks)

for task in tasks:
if isinstance(task, tuple):
Expand All @@ -156,7 +158,7 @@ def __new__(mcs, classname, bases, classDict):
else:
new_tasks.append(task)

for item in classDict.itervalues():
for item in six.itervalues(classDict):
if hasattr(item, "locust_task_weight"):
for i in xrange(0, item.locust_task_weight):
new_tasks.append(item)
Expand All @@ -165,6 +167,7 @@ def __new__(mcs, classname, bases, classDict):

return type.__new__(mcs, classname, bases, classDict)

@six.add_metaclass(TaskSetMeta)
class TaskSet(object):
"""
Class defining a set of tasks that a Locust user will execute.
Expand Down Expand Up @@ -221,8 +224,6 @@ class ForumPage(TaskSet):
instantiated. Useful for nested TaskSet classes.
"""

__metaclass__ = TaskSetMeta

def __init__(self, parent):
self._task_queue = []
self._time_start = time()
Expand Down Expand Up @@ -251,9 +252,9 @@ def run(self, *args, **kwargs):
self.on_start()
except InterruptTaskSet as e:
if e.reschedule:
raise RescheduleTaskImmediately, e, sys.exc_info()[2]
six.reraise(RescheduleTaskImmediately, RescheduleTaskImmediately(e.reschedule), sys.exc_info()[2])
else:
raise RescheduleTask, e, sys.exc_info()[2]
six.reraise(RescheduleTask, RescheduleTask(e.reschedule), sys.exc_info()[2])

while (True):
try:
Expand All @@ -273,9 +274,9 @@ def run(self, *args, **kwargs):
self.wait()
except InterruptTaskSet as e:
if e.reschedule:
raise RescheduleTaskImmediately, e, sys.exc_info()[2]
six.reraise(RescheduleTaskImmediately, RescheduleTaskImmediately(e.reschedule), sys.exc_info()[2])
else:
raise RescheduleTask, e, sys.exc_info()[2]
six.reraise(RescheduleTask, RescheduleTask(e.reschedule), sys.exc_info()[2])
except StopLocust:
raise
except GreenletExit:
Expand All @@ -294,7 +295,7 @@ def execute_next_task(self):

def execute_task(self, task, *args, **kwargs):
# check if the function is a method bound to the current locust, and if so, don't pass self as first argument
if hasattr(task, "im_self") and task.__self__ == self:
if hasattr(task, "__self__") and task.__self__ == self:
# task is a bound method on self
task(*args, **kwargs)
elif hasattr(task, "tasks") and issubclass(task, TaskSet):
Expand Down
13 changes: 7 additions & 6 deletions locust/inspectlocust.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import inspect
import six

from core import Locust, TaskSet
from log import console_logger
from .core import Locust, TaskSet
from .log import console_logger

def print_task_ratio(locusts, total=False, level=0, parent_ratio=1.0):
d = get_task_ratio_dict(locusts, total=total, parent_ratio=parent_ratio)
_print_task_ratio(d)

def _print_task_ratio(x, level=0):
for k, v in x.iteritems():
for k, v in six.iteritems(x):
padding = 2*" "*level
ratio = v.get('ratio', 1)
console_logger.info(" %-10s %-50s" % (padding + "%-6.1f" % (ratio*100), padding + k))
Expand All @@ -30,10 +31,10 @@ def get_task_ratio_dict(tasks, total=False, parent_ratio=1.0):
ratio[task] += task.weight if hasattr(task, 'weight') else 1

# get percentage
ratio_percent = dict((k, float(v) / divisor) for k, v in ratio.iteritems())
ratio_percent = dict((k, float(v) / divisor) for k, v in six.iteritems(ratio))

task_dict = {}
for locust, ratio in ratio_percent.iteritems():
for locust, ratio in six.iteritems(ratio_percent):
d = {"ratio":ratio}
if inspect.isclass(locust):
if issubclass(locust, Locust):
Expand All @@ -47,4 +48,4 @@ def get_task_ratio_dict(tasks, total=False, parent_ratio=1.0):

task_dict[locust.__name__] = d

return task_dict
return task_dict
20 changes: 10 additions & 10 deletions locust/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import locust
import runners
from . import runners

import gevent
import sys
Expand All @@ -10,13 +10,13 @@
import socket
from optparse import OptionParser

import web
from log import setup_logging, console_logger
from stats import stats_printer, print_percentile_stats, print_error_report, print_stats
from inspectlocust import print_task_ratio, get_task_ratio_dict
from core import Locust, HttpLocust
from runners import MasterLocustRunner, SlaveLocustRunner, LocalLocustRunner
import events
from . import web
from .log import setup_logging, console_logger
from .stats import stats_printer, print_percentile_stats, print_error_report, print_stats
from .inspectlocust import print_task_ratio, get_task_ratio_dict
from .core import Locust, HttpLocust
from .runners import MasterLocustRunner, SlaveLocustRunner, LocalLocustRunner
from . import events

_internals = [Locust, HttpLocust]
version = locust.__version__
Expand Down Expand Up @@ -338,7 +338,7 @@ def main():
logger = logging.getLogger(__name__)

if options.show_version:
print "Locust %s" % (version,)
print("Locust %s" % (version,))
sys.exit(0)

locustfile = find_locustfile(options.locustfile)
Expand Down Expand Up @@ -409,7 +409,7 @@ def main():
try:
runners.locust_runner = SlaveLocustRunner(locust_classes, options)
main_greenlet = runners.locust_runner.greenlet
except socket.error, e:
except socket.error as e:
logger.error("Failed to connect to the Locust master: %s", e)
sys.exit(-1)

Expand Down
4 changes: 2 additions & 2 deletions locust/rpc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import warnings

try:
import zmqrpc as rpc
from . import zmqrpc as rpc
except ImportError:
warnings.warn("WARNING: Using pure Python socket RPC implementation instead of zmq. If running in distributed mode, this could cause a performance decrease. We recommend you to install the pyzmq python package when running in distributed mode.")
import socketrpc as rpc
from . import socketrpc as rpc

from .protocol import Message
2 changes: 1 addition & 1 deletion locust/rpc/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ def serialize(self):

@classmethod
def unserialize(cls, data):
msg = cls(*msgpack.loads(data))
msg = cls(*msgpack.loads(data, encoding='utf-8'))
return msg
22 changes: 12 additions & 10 deletions locust/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import gevent
from gevent import GreenletExit
from gevent.pool import Group
import six
from six.moves import xrange

import events
from stats import global_stats
from . import events
from .stats import global_stats

from rpc import rpc, Message
from .rpc import rpc, Message

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -102,7 +104,7 @@ def hatch():
sleep_time = 1.0 / self.hatch_rate
while True:
if not bucket:
logger.info("All locusts hatched: %s" % ", ".join(["%s: %d" % (name, count) for name, count in occurence_count.iteritems()]))
logger.info("All locusts hatched: %s" % ", ".join(["%s: %d" % (name, count) for name, count in six.iteritems(occurence_count)]))
events.hatch_complete.fire(user_count=self.num_clients)
return

Expand Down Expand Up @@ -225,7 +227,7 @@ def __init__(self, *args, **kwargs):

class SlaveNodesDict(dict):
def get_by_state(self, state):
return [c for c in self.itervalues() if c.state == state]
return [c for c in six.itervalues(self) if c.state == state]

@property
def ready(self):
Expand Down Expand Up @@ -260,7 +262,7 @@ def on_quitting():

@property
def user_count(self):
return sum([c.user_count for c in self.clients.itervalues()])
return sum([c.user_count for c in six.itervalues(self.clients)])

def start_hatching(self, locust_count, hatch_rate):
num_slaves = len(self.clients.ready) + len(self.clients.running)
Expand All @@ -270,7 +272,7 @@ def start_hatching(self, locust_count, hatch_rate):
return

self.num_clients = locust_count
slave_num_clients = locust_count / (num_slaves or 1)
slave_num_clients = locust_count // (num_slaves or 1)
slave_hatch_rate = float(hatch_rate) / (num_slaves or 1)
remaining = locust_count % num_slaves

Expand All @@ -281,7 +283,7 @@ def start_hatching(self, locust_count, hatch_rate):
self.exceptions = {}
events.master_start_hatching.fire()

for client in self.clients.itervalues():
for client in six.itervalues(self.clients):
data = {
"hatch_rate":slave_hatch_rate,
"num_clients":slave_num_clients,
Expand All @@ -305,7 +307,7 @@ def stop(self):
events.master_stop_hatching.fire()

def quit(self):
for client in self.clients.itervalues():
for client in six.itervalues(self.clients):
self.server.send(Message("quit", None, None))
self.greenlet.kill(block=True)

Expand All @@ -332,7 +334,7 @@ def client_listener(self):
self.clients[msg.node_id].state = STATE_RUNNING
self.clients[msg.node_id].user_count = msg.data["count"]
if len(self.clients.hatching) == 0:
count = sum(c.user_count for c in self.clients.itervalues())
count = sum(c.user_count for c in six.itervalues(self.clients))
events.hatch_complete.fire(user_count=count)
elif msg.type == "quit":
if msg.node_id in self.clients:
Expand Down

0 comments on commit 9e202a3

Please sign in to comment.