Skip to content
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
language: python
python:
- "2.7"
#- "3.4"
- "3.5"

install:
- pip install coveralls
Expand All @@ -12,4 +12,4 @@ script:
- nosetests -v --with-coverage . smartdispatch/workers

after_success:
- coveralls
- coveralls
11 changes: 6 additions & 5 deletions scripts/smart-dispatch
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python2
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function

import os
import sys
Expand Down Expand Up @@ -178,11 +179,11 @@ def main():
pbs_filenames = job_generator.write_pbs_files(path_job_commands)

# Launch the jobs
print "## {nb_commands} command(s) will be executed in {nb_jobs} job(s) ##".format(nb_commands=nb_commands, nb_jobs=len(pbs_filenames))
print "Batch UID:\n{batch_uid}".format(batch_uid=jobname)
print("## {nb_commands} command(s) will be executed in {nb_jobs} job(s) ##".format(nb_commands=nb_commands, nb_jobs=len(pbs_filenames)))
print("Batch UID:\n{batch_uid}".format(batch_uid=jobname))
if not args.doNotLaunch:
launch_jobs(LAUNCHER if args.launcher is None else args.launcher, pbs_filenames, CLUSTER_NAME, path_job)
print "\nLogs, command, and jobs id related to this batch will be in:\n {smartdispatch_folder}".format(smartdispatch_folder=path_job)
print("\nLogs, command, and jobs id related to this batch will be in:\n {smartdispatch_folder}".format(smartdispatch_folder=path_job))


def parse_arguments():
Expand All @@ -198,7 +199,7 @@ def parse_arguments():
parser.add_argument('-c', '--coresPerCommand', type=int, required=False, help='How many cores a command needs.', default=1)
parser.add_argument('-g', '--gpusPerCommand', type=int, required=False, help='How many gpus a command needs.', default=1)
# parser.add_argument('-m', '--memPerCommand', type=float, required=False, help='How much memory a command needs (in Gb).')
parser.add_argument('-f', '--commandsFile', type=file, required=False, help='File containing commands to launch. Each command must be on a seperate line. (Replaces commandAndOptions)')
parser.add_argument('-f', '--commandsFile', type=open, required=False, help='File containing commands to launch. Each command must be on a seperate line. (Replaces commandAndOptions)')

parser.add_argument('-l', '--modules', type=str, required=False, help='List of additional modules to load.', nargs='+')
parser.add_argument('-x', '--doNotLaunch', action='store_true', help='Generate all the files without launching the job.')
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
license='LICENSE.txt',
description='An easy to use job launcher for supercomputers with PBS compatible job manager.',
long_description=open('README.md').read(),
install_requires=['psutil>=1'],
install_requires=['psutil>=1', 'future'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know about that library. Neat!

package_data={'smartdispatch': ['config/*.json']}
)
5 changes: 4 additions & 1 deletion smartdispatch/argument_template.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from builtins import map
from builtins import range
from builtins import object
import re
from collections import OrderedDict

Expand Down Expand Up @@ -35,7 +38,7 @@ def unfold(self, match):
start = int(groups[0])
end = int(groups[1])
step = 1 if groups[2] is None else int(groups[2])
return map(str, range(start, end, step))
return list(map(str, list(range(start, end, step))))


argument_templates = build_argument_templates_dictionnary()
1 change: 1 addition & 0 deletions smartdispatch/command_manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from builtins import object
import os
from .filelock import open_with_lock

Expand Down
2 changes: 2 additions & 0 deletions smartdispatch/job_generator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import absolute_import

from builtins import str
from builtins import object
import os
import re
from smartdispatch.pbs import PBS
Expand Down
10 changes: 6 additions & 4 deletions smartdispatch/pbs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from builtins import str
from builtins import object
import re
from collections import OrderedDict

Expand Down Expand Up @@ -54,7 +56,7 @@ def add_options(self, **options):
Declares a name for the job. It must consist of printable,
non white space characters with the first character alphabetic.
"""
for option_name, option_value in options.items():
for option_name, option_value in list(options.items()):
# If known option, validate it.
if option_name.strip('-') == 'N':
if len(option_name) > 64:
Expand All @@ -81,7 +83,7 @@ def add_resources(self, **resources):
*pmem*: pmem=[0-9]+(b|kb|mb|gb|tb)
Specifies the maximum amount of physical memory used by any single process of the job.
"""
for resource_name, resource_value in resources.items():
for resource_name, resource_value in list(resources.items()):
# If known ressource, validate it.
if resource_name == 'nodes':
if re.match(regex_resource_nodes, str(resource_value)) is None:
Expand Down Expand Up @@ -150,13 +152,13 @@ def __str__(self):
pbs = []
pbs += ["#!/bin/bash"]

for option_name, option_value in self.options.items():
for option_name, option_value in list(self.options.items()):
if option_value == "":
pbs += ["#PBS {0}".format(option_name)]
else:
pbs += ["#PBS {0} {1}".format(option_name, option_value)]

for resource_name, resource_value in self.resources.items():
for resource_name, resource_value in list(self.resources.items()):
pbs += ["#PBS -l {0}={1}".format(resource_name, resource_value)]

pbs += ["\n# Modules #"]
Expand Down
4 changes: 3 additions & 1 deletion smartdispatch/queue.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from smartdispatch import get_available_queues
from __future__ import absolute_import
from builtins import object
from .smartdispatch import get_available_queues


class Queue(object):
Expand Down
16 changes: 9 additions & 7 deletions smartdispatch/smartdispatch.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import absolute_import
from __future__ import absolute_import, print_function

from builtins import next
from builtins import map
import os
import re
import itertools
Expand Down Expand Up @@ -89,7 +91,7 @@ def unfold_command(command):
text = utils.encode_escaped_characters(command)

# Build the master regex with all argument's regex
regex = "(" + "|".join(["(?P<{0}>{1})".format(name, arg.regex) for name, arg in argument_templates.items()]) + ")"
regex = "(" + "|".join(["(?P<{0}>{1})".format(name, arg.regex) for name, arg in list(argument_templates.items())]) + ")"

pos = 0
arguments = []
Expand All @@ -98,12 +100,12 @@ def unfold_command(command):
arguments.append([text[pos:match.start()]])

# Unfold argument
argument_template_name, matched_text = next((k, v) for k, v in match.groupdict().items() if v is not None)
argument_template_name, matched_text = next((k, v) for k, v in list(match.groupdict().items()) if v is not None)
arguments.append(argument_templates[argument_template_name].unfold(matched_text))
pos = match.end()

arguments.append([text[pos:]]) # Add remaining unfolded arguments
arguments = [map(utils.decode_escaped_characters, argvalues) for argvalues in arguments]
arguments = [list(map(utils.decode_escaped_characters, argvalues)) for argvalues in arguments]
return ["".join(argvalues) for argvalues in itertools.product(*arguments)]


Expand Down Expand Up @@ -185,11 +187,11 @@ def launch_jobs(launcher, pbs_filenames, cluster_name, path_job): # pragma: no
for pbs_filename in pbs_filenames:
launcher_output = check_output('PBS_FILENAME={pbs_filename} {launcher} {pbs_filename}'.format(
launcher=launcher, pbs_filename=pbs_filename), shell=True)
jobs_id += [launcher_output.strip()]
jobs_id += [launcher_output.strip().decode()]

# On some clusters, SRMJID and PBS_JOBID don't match
if cluster_name in ['helios']:
launcher_output = check_output(['qstat', '-f']).split('Job Id: ')
launcher_output = check_output(['qstat', '-f']).decode().split('Job Id: ')
for job in launcher_output:
if re.search(r"SRMJID:{job_id}".format(job_id=jobs_id[-1]), job):
pbs_job_id = re.match(r"[0-9a-zA-Z.-]*", job).group()
Expand All @@ -198,4 +200,4 @@ def launch_jobs(launcher, pbs_filenames, cluster_name, path_job): # pragma: no
with open_with_lock(pjoin(path_job, "jobs_id.txt"), 'a') as jobs_id_file:
jobs_id_file.writelines(t.strftime("## %Y-%m-%d %H:%M:%S ##\n"))
jobs_id_file.writelines("\n".join(jobs_id) + "\n")
print "\nJobs id:\n{jobs_id}".format(jobs_id=" ".join(jobs_id))
print("\nJobs id:\n{jobs_id}".format(jobs_id=" ".join(jobs_id)))
2 changes: 1 addition & 1 deletion smartdispatch/tests/test_filelock.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def _test_open_with_lock(lock_func):
time.sleep(1)

stdout, stderr = process.communicate()
assert_equal(stdout, "")
assert_equal(stdout, b"")
assert_true("Traceback" not in stderr, msg="Unexpected error: '{}'".format(stderr))
assert_true("write-lock" in stderr, msg="Forcing a race condition, try increasing sleeping time above.")

Expand Down
2 changes: 2 additions & 0 deletions smartdispatch/tests/test_job_generator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from builtins import str
from builtins import object
from nose.tools import assert_true, assert_false, assert_equal, assert_raises

import os
Expand Down
3 changes: 2 additions & 1 deletion smartdispatch/tests/test_pbs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from builtins import str
from nose.tools import assert_true, assert_equal, assert_raises
from numpy.testing import assert_array_equal

Expand Down Expand Up @@ -29,7 +30,7 @@ def test_constructor(self):
def test_add_options(self):
# Default options
assert_equal(len(self.pbs.options), 2)
assert_true('-V' in self.pbs.options.keys())
assert_true('-V' in list(self.pbs.options.keys()))
assert_equal(self.pbs.options['-q'], self.queue_name)

self.pbs.add_options(A="option1")
Expand Down
2 changes: 1 addition & 1 deletion smartdispatch/tests/test_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def test_constructor(self):

# Test with missing information but referring to a known queue.
for cluster_name in self.known_clusters:
for queue_name, queue_infos in get_available_queues(cluster_name).items():
for queue_name, queue_infos in list(get_available_queues(cluster_name).items()):
queue = Queue(queue_name, cluster_name)
assert_equal(queue.name, queue_name)
assert_equal(queue.cluster_name, cluster_name)
Expand Down
4 changes: 3 additions & 1 deletion smartdispatch/tests/test_smartdispatch.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from future import standard_library
standard_library.install_aliases()
import os
import re
import shutil
import time as t
from os.path import join as pjoin
from StringIO import StringIO
from io import StringIO

import tempfile
from nose.tools import assert_true, assert_equal
Expand Down
6 changes: 4 additions & 2 deletions smartdispatch/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from builtins import zip
from builtins import range
import unittest

from smartdispatch import utils
Expand All @@ -21,11 +23,11 @@ def test_print_boxed_empty(self):


def test_chunks():
sequence = range(10)
sequence = list(range(10))

for n in range(1, 11):
expected = []
for start, end in zip(range(0, len(sequence), n), range(n, len(sequence) + n, n)):
for start, end in zip(list(range(0, len(sequence), n)), list(range(n, len(sequence) + n, n))):
expected.append(sequence[start:end])

assert_array_equal(list(utils.chunks(sequence, n)), expected, "n:{0}".format(n))
Expand Down
32 changes: 23 additions & 9 deletions smartdispatch/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
from __future__ import print_function

from builtins import str
from builtins import input
from builtins import map
from builtins import range

try:
_unicode = unicode
_utf8 = lambda x: _unicode(x, 'UTF-8')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have thought this sort of "hacks" isn't needed anymore thanks to the future library?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I did this before adding future as a dependency because I thought I could get around it without additional dependencies... until I got to the tests. I'll change it to use future.builtins.str instead.

except NameError:
_utf8 = str

import re
import hashlib
import unicodedata
import json
import codecs

from distutils.util import strtobool
from subprocess import Popen, PIPE
Expand All @@ -17,7 +31,7 @@ def jobname_generator(jobname, job_id):
Returns
-------
str
The cropped version of the string.
The cropped version of the string.
'''
# 64 - 1 since the total length including -1 should be less than 64
job_id = str(job_id)
Expand All @@ -30,13 +44,13 @@ def jobname_generator(jobname, job_id):

def print_boxed(string):
splitted_string = string.split('\n')
max_len = max(map(len, splitted_string))
max_len = max(list(map(len, splitted_string)))
box_line = u"\u2500" * (max_len + 2)

out = u"\u250c" + box_line + u"\u2510\n"
out += '\n'.join([u"\u2502 {} \u2502".format(line.ljust(max_len)) for line in splitted_string])
out += u"\n\u2514" + box_line + u"\u2518"
print out
print(out)


def yes_no_prompt(query, default=None):
Expand All @@ -47,7 +61,7 @@ def yes_no_prompt(query, default=None):

while True:
try:
answer = raw_input("{0}{1}".format(query, available_prompts[default]))
answer = eval(input("{0}{1}".format(query, available_prompts[default])))
return strtobool(answer)
except ValueError:
if answer == '' and default is not None:
Expand All @@ -56,13 +70,13 @@ def yes_no_prompt(query, default=None):

def chunks(sequence, n):
""" Yield successive n-sized chunks from sequence. """
for i in xrange(0, len(sequence), n):
for i in range(0, len(sequence), n):
yield sequence[i:i + n]


def generate_uid_from_string(value):
""" Create unique identifier from a string. """
return hashlib.sha256(value).hexdigest()
return hashlib.sha256(value.encode('UTF-8')).hexdigest()


def slugify(value):
Expand All @@ -75,15 +89,15 @@ def slugify(value):
---------
https://github.com/django/django/blob/1.7c3/django/utils/text.py#L436
"""
value = unicodedata.normalize('NFKD', unicode(value, "UTF-8")).encode('ascii', 'ignore').decode('ascii')
value = unicodedata.normalize('NFKD', _utf8(value)).encode('ascii', 'ignore').decode('ascii')
value = re.sub('[^\w\s-]', '', value).strip().lower()
return str(re.sub('[-\s]+', '_', value))


def encode_escaped_characters(text, escaping_character="\\"):
""" Escape the escaped character using its hex representation """
def hexify(match):
return "\\x{0}".format(match.group()[-1].encode("hex"))
return codecs.encode("\\x{0}".format(match.group()[-1], 'hex_codec'))

return re.sub(r"\\.", hexify, text)

Expand Down Expand Up @@ -117,7 +131,7 @@ def detect_cluster():
# If qstat is not available we assume that the cluster is unknown.
return None
# Get server name from status
server_name = output.split('\n')[2].split(' ')[0]
server_name = output.decode().split('\n')[2].split(' ')[0]
# Cleanup the name and return it
cluster_name = None
if server_name.split('.')[-1] == 'm':
Expand Down
2 changes: 1 addition & 1 deletion smartdispatch/workers/tests/test_base_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,6 @@ def test_lock(self):
time.sleep(1)

stdout, stderr = process.communicate()
assert_equal(stdout, "")
assert_equal(stdout, b"")
assert_true("write-lock" in stderr, msg="Forcing a race condition, try increasing sleeping time above.")
assert_true("Traceback" not in stderr) # Check that there are no errors.