diff --git a/.travis.yml b/.travis.yml index e049b27..c531075 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: python -python: - - "2.7" +env: + - TOXENV=py27 + - TOXENV=py33 install: - - pip install . + - pip install tox script: - - nosetests - - pyqi + - tox diff --git a/ChangeLog.md b/ChangeLog.md index 5a1a3d5..4561dce 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,6 +4,8 @@ pyqi ChangeLog pyqi 0.3.1-dev -------------- +* added an HDF5 implicit dataset extender +* native python 3 support * painless profiling: just set the environment variable PYQI_PROFILE_COMMAND pyqi 0.3.1 diff --git a/doc/index.rst b/doc/index.rst index 5a7bcea..de999ea 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -6,7 +6,7 @@ What is pyqi? pyqi (canonically pronounced *pie chee*) is a Python framework designed to support wrapping general *commands* in multiple types of *interfaces*, including at the command line, HTML, and API levels. -pyqi's only requirement is a working Python 2.7 installation. +pyqi's only requirement is a working Python 2.7 or 3.3 installation. Why should I care? ------------------ diff --git a/pyqi/commands/make_optparse.py b/pyqi/commands/make_optparse.py index b99930a..6c26323 100644 --- a/pyqi/commands/make_optparse.py +++ b/pyqi/commands/make_optparse.py @@ -10,7 +10,7 @@ from __future__ import division from operator import attrgetter -from pyqi.core.command import (Command, CommandIn, CommandOut, +from pyqi.core.command import (Command, CommandIn, CommandOut, ParameterCollection) from pyqi.commands.code_header_generator import CodeHeaderGenerator @@ -81,7 +81,7 @@ # # value will be made available to Handler. This name # # can be either an underscored or dashed version of the # # option name (e.g., 'output_fp' or 'output-fp') - # InputName='output-fp'), + # InputName='output-fp'), # # An example option that does not map to a CommandIn. # OptparseResult(Parameter=cmd_out_lookup('some_other_result'), @@ -112,7 +112,7 @@ class MakeOptparse(CodeHeaderGenerator): BriefDescription = "Consume a Command, stub out an optparse configuration" LongDescription = """Construct and stub out the basic optparse configuration for a given Command. This template provides comments and examples of what to fill in.""" - + CommandIns = ParameterCollection( CodeHeaderGenerator.CommandIns.Parameters + [ CommandIn(Name='command', DataType=Command, @@ -155,8 +155,9 @@ def run(self, **kwargs): action = 'store' data_type = cmdin.DataType - fmt = {'name':cmdin.Name, 'datatype':data_type, 'action':action, - 'required':str(cmdin.Required), + fmt = {'name':cmdin.Name, + 'datatype':getattr(data_type, '__name__', None), + 'action':action, 'required':str(cmdin.Required), 'help':cmdin.Description, 'default_block':default_block} cmdin_formatted.append(input_format % fmt) diff --git a/pyqi/core/command.py b/pyqi/core/command.py index b497a15..cf0dc83 100644 --- a/pyqi/core/command.py +++ b/pyqi/core/command.py @@ -135,9 +135,9 @@ def __call__(self, **kwargs): try: result = self.run(**kwargs) - except Exception: + except Exception as e: self._logger.fatal('Error executing command: %s' % self_str) - raise + raise e else: self._logger.info('Completed command: %s' % self_str) diff --git a/pyqi/core/hdf5.py b/pyqi/core/hdf5.py new file mode 100644 index 0000000..e233046 --- /dev/null +++ b/pyqi/core/hdf5.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +#----------------------------------------------------------------------------- +# Copyright (c) 2013, The BiPy Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- +from __future__ import division + +__credits__ = ["Daniel McDonald"] + +try: + import h5py +except ImportError: + raise ImportError("h5py is required for functionality in this module") + +VLENSTR = h5py.special_dtype(vlen=str) + +class AutoExtendHDF5(object): + """Allow for implicitly extendable datasets""" + def __init__(self, f): + self.f = f + self._known_datasets = [] + + def __del__(self): + for n in self._known_datasets: + self._finalize(n) + + def create_dataset(self, name, dtype): + """Create a tracked dataset that will automatically reshape""" + self.f.create_dataset(name, shape=(1,), maxshape=(None,), + chunks=True, dtype=dtype) + self.f[name].attrs['next_item'] = 0 # idx where next item can get written + self._known_datasets.append(name) + + def extend(self, name, data, growth_factor=1): + """Extend an automatically growable dataset""" + n_data_items = len(data) + next_item = self.f[name].attrs['next_item'] + + # resize as needed + if (next_item + n_data_items) >= self.f[name].size: + new_size = next_item + n_data_items + new_size += int(new_size * growth_factor) + self.f[name].resize((new_size,)) + + # store the data + start = next_item + end = next_item + len(data) + self.f[name][start:end] = data + self.f[name].attrs['next_item'] = end + + def _finalize(self, name): + """Resize a dataset to its correct size""" + actual_size = self.f[name].attrs.get('next_item', None) + + if actual_size is None: + return + + self.f[name].resize((actual_size,)) + del self.f[name].attrs['next_item'] diff --git a/pyqi/core/interface.py b/pyqi/core/interface.py index c2e1cd1..252ab1f 100644 --- a/pyqi/core/interface.py +++ b/pyqi/core/interface.py @@ -13,7 +13,6 @@ import importlib from sys import exit, stderr -from ConfigParser import SafeConfigParser from glob import glob from os.path import basename, dirname, expanduser, join from pyqi.core.exception import IncompetentDeveloperError @@ -193,13 +192,13 @@ class InterfaceInputOption(InterfaceOption): "str": str, "int": int, "float": float, - "long": long, + "long": int, # for python 3 compatibility as long is dropped "complex": complex, "tuple": tuple, "dict": dict, "list": list, "set": set, - "unicode": unicode, + "unicode": str, # for python 3 compatibility as all strings are unicode "frozenset": frozenset } @@ -298,9 +297,9 @@ def get_command_config(command_config_module, cmd, exit_on_failure=True): try: cmd_cfg = importlib.import_module('.'.join([command_config_module, python_cmd_name])) - except ImportError, e: + except ImportError as e: error_msg = str(e) - + if exit_on_failure: stderr.write("Unable to import the command configuration for " "%s:\n" % cmd) diff --git a/pyqi/core/interfaces/html/__init__.py b/pyqi/core/interfaces/html/__init__.py index eff4675..87c05f0 100644 --- a/pyqi/core/interfaces/html/__init__.py +++ b/pyqi/core/interfaces/html/__init__.py @@ -14,7 +14,15 @@ import os import types import os.path -from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +import sys + +from pyqi.util import get_version_string, is_py2 + +if is_py2(): + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +else: + from http.server import BaseHTTPRequestHandler, HTTPServer + from cgi import parse_header, parse_multipart, parse_qs, FieldStorage from copy import copy from glob import glob @@ -24,7 +32,6 @@ from pyqi.core.factory import general_factory from pyqi.core.exception import IncompetentDeveloperError from pyqi.core.command import Parameter -from pyqi.util import get_version_string class HTMLResult(InterfaceOutputOption): """Base class for results for an HTML config file""" @@ -36,7 +43,7 @@ def __init__(self, MIMEType=None, **kwargs): class HTMLDownload(HTMLResult): """Result class for downloading a file from the server""" - def __init__(self, FileExtension=None, FilenameLookup=None, DefaultFilename=None, + def __init__(self, FileExtension=None, FilenameLookup=None, DefaultFilename=None, MIMEType='application/octet-stream', **kwargs): super(HTMLDownload, self).__init__(MIMEType=MIMEType, **kwargs) self.FileExtension = FileExtension @@ -56,7 +63,6 @@ class HTMLInputOption(InterfaceInputOption): bool: lambda x: x.value == "True", int: lambda x: int(x.value), float: lambda x: float(x.value), - long: lambda x: long(x.value), complex: lambda x: complex(x.value), "upload_file": lambda x: x.file, "multiple_choice": lambda x: x.value @@ -76,17 +82,17 @@ def get_html(self, prefix, value=""): """Return the HTML needed for user input given a default value""" if (not value) and (self.Default is not None): value = self.Default - + input_name = prefix + self.Name string_input = lambda: '' % (input_name, value) number_input = lambda: '' % (input_name, value) - #html input files cannot have default values. + #html input files cannot have default values. #If the html interface worked as a data service, this would be possible as submit would be ajax. upload_input = lambda: '' % input_name mchoice_input = lambda: ''.join( - [ ('(%s)' - % (choice, input_name, choice, 'checked="true"' if value == choice else '')) + [ ('(%s)' + % (choice, input_name, choice, 'checked="true"' if value == choice else '')) for choice in self.Choices ] ) @@ -96,7 +102,6 @@ def get_html(self, prefix, value=""): bool: mchoice_input, int: number_input, float: number_input, - long: number_input, complex: string_input, "multiple_choice": mchoice_input, "upload_file": upload_input @@ -110,7 +115,7 @@ def get_html(self, prefix, value=""): self.Help, ' ' ]) - + def _validate_option(self): if self.Type not in self._type_handlers: raise IncompetentDeveloperError("Unsupported Type in HTMLInputOption: %s" % self.Type) @@ -130,7 +135,7 @@ def _validate_option(self): class HTMLInterface(Interface): """An HTML interface""" #Relative mapping wasn't working on a collegue's MacBook when pyqi was run outside of it's directory - #Until I understand why that was the case and how to fix it, I am putting the style css here. + #Until I understand why that was the case and how to fix it, I am putting the style css here. #This is not a permanent solution. css_style = '\n'.join([ 'html, body {', @@ -189,7 +194,7 @@ def __init__(self, input_prefix="pyqi_", **kwargs): self._html_input_prefix = input_prefix self._html_interface_input = {} super(HTMLInterface, self).__init__(**kwargs) - + #Override def __call__(self, in_, *args, **kwargs): self._the_in_validator(in_) @@ -199,14 +204,14 @@ def __call__(self, in_, *args, **kwargs): 'type': 'error', 'errors': errors } - else: + else: cmd_result = self.CmdInstance(**cmd_input) - self._the_out_validator(cmd_result) + self._the_out_validator(cmd_result) return self._output_handler(cmd_result) def _validate_inputs_outputs(self, inputs, outputs): - super(HTMLInterface, self)._validate_inputs_outputs(inputs, outputs) - + super(HTMLInterface, self)._validate_inputs_outputs(inputs, outputs) + if len(outputs) > 1: raise IncompetentDeveloperError("There can be only one... output") @@ -241,7 +246,7 @@ def _input_handler(self, in_, *args, **kwargs): formatted_input = {} for key in in_: - mod_key = key[ len(self._html_input_prefix): ] + mod_key = key[ len(self._html_input_prefix): ] formatted_input[mod_key] = in_[key] if not formatted_input[mod_key].value: formatted_input[mod_key] = None @@ -319,11 +324,11 @@ def _output_handler(self, results): if output.InputName is None: handled_results = output.Handler(rk, results[rk]) else: - handled_results = output.Handler(rk, results[rk], + handled_results = output.Handler(rk, results[rk], self._html_interface_input[output.InputName]) else: handled_results = results[rk] - + if isinstance(output, HTMLDownload): return self._output_download_handler(output, handled_results) @@ -413,7 +418,7 @@ def route(self, path, output_writer): self.send_header('Content-type', 'text/html') self.end_headers() output_writer(self.wfile.write) - + self.wfile.close() self._unrouted = False; @@ -426,7 +431,7 @@ def command_route(self, command): self.send_header('Content-type', 'text/html') self.end_headers() cmd_obj.command_page_writer(self.wfile.write, [], {}) - + self.wfile.close() self._unrouted = False @@ -441,14 +446,14 @@ def post_route(self, command, postvars): 'type':'error', 'errors':[e] } - + if result['type'] == 'error': self.send_response(400) self.send_header('Content-type', 'text/html') self.end_headers() cmd_obj.command_page_writer(self.wfile.write, result['errors'], postvars) - elif result['type'] == 'page': + elif result['type'] == 'page': self.send_response(200) self.send_header('Content-type', result['mime_type']) self.end_headers() @@ -460,7 +465,7 @@ def post_route(self, command, postvars): self.send_header('Content-disposition', 'attachment; filename='+result['filename']) self.end_headers() self.wfile.write(result['contents']) - + self.wfile.close() self._unrouted = False @@ -508,8 +513,8 @@ def do_POST(self): def start_server(port, module): """Start a server for the HTMLInterface on the specified port""" interface_server = HTTPServer(("", port), get_http_handler(module)) - print "-- Starting server at http://localhost:%d --" % port - print "To close the server, type 'ctrl-c' into this window." + print("-- Starting server at http://localhost:%d --" % port) + print("To close the server, type 'ctrl-c' into this window.") try: interface_server.serve_forever() diff --git a/pyqi/core/interfaces/optparse/__init__.py b/pyqi/core/interfaces/optparse/__init__.py index dd43c8c..85de1fc 100644 --- a/pyqi/core/interfaces/optparse/__init__.py +++ b/pyqi/core/interfaces/optparse/__init__.py @@ -13,7 +13,6 @@ "Jose Antonio Navas Molina"] import os -import types from copy import copy from glob import glob from os.path import abspath, exists, isdir, isfile, split @@ -436,7 +435,7 @@ def _check_multiple_choice(self): if self.mchoices is None: raise OptionError( "must supply a list of mchoices for type '%s'" % self.type, self) - elif type(self.mchoices) not in (types.TupleType, types.ListType): + elif type(self.mchoices) not in (tuple, list): raise OptionError( "choices must be a list of strings ('%s' supplied)" % str(type(self.mchoices)).split("'")[1], self) diff --git a/pyqi/core/interfaces/optparse/output_handler.py b/pyqi/core/interfaces/optparse/output_handler.py index 94d7075..fe0fcb2 100644 --- a/pyqi/core/interfaces/optparse/output_handler.py +++ b/pyqi/core/interfaces/optparse/output_handler.py @@ -65,14 +65,14 @@ def print_list_of_strings(result_key, data, option_value=None): ``result_key`` and ``option_value`` are ignored. """ for line in data: - print line + print(line) def print_string(result_key, data, option_value=None): """Print the string A newline will be printed before the data""" - print "" - print data + print("") + print(data) def write_or_print_string(result_key, data, option_value=None): """Write a string to a file. diff --git a/pyqi/util.py b/pyqi/util.py index b82bad6..7408492 100644 --- a/pyqi/util.py +++ b/pyqi/util.py @@ -15,21 +15,29 @@ import importlib from os import remove from os.path import split, splitext -import sys -from subprocess import Popen, PIPE, STDOUT +import sys +from subprocess import Popen, PIPE + from pyqi.core.log import StdErrLogger from pyqi.core.exception import MissingVersionInfoError +def is_py2(): + """Check if we're using Python 2""" + if sys.version_info.major == 2: + return True + else: + return False + def pyqi_system_call(cmd, shell=True, dry_run=False): """Call cmd and return (stdout, stderr, return_value). - cmd: can be either a string containing the command to be run, or a + cmd: can be either a string containing the command to be run, or a sequence of strings that are the tokens of the command. - shell: value passed directly to Popen (default: True). See Python's + shell: value passed directly to Popen (default: True). See Python's subprocess.Popen for a description of the shell parameter and how cmd is interpreted differently based on its value. dry_run: if True, print cmd and return ("", "", 0) (default: False) - + This function is ported from QIIME (http://www.qiime.org), previously named qiime_system_call. QIIME is a GPL project, but we obtained permission from the authors of this function to port it to pyqi (and keep it under @@ -48,7 +56,7 @@ def pyqi_system_call(cmd, shell=True, dry_run=False): universal_newlines=True, stdout=PIPE, stderr=PIPE) - # communicate pulls all stdout/stderr from the PIPEs to + # communicate pulls all stdout/stderr from the PIPEs to # avoid blocking -- don't remove this line! stdout, stderr = proc.communicate() return_value = proc.returncode @@ -69,8 +77,8 @@ def remove_files(list_of_filepaths, error_on_missing=True): missing.append(fp) if error_on_missing and missing: - raise OSError, "Some filepaths were not accessible: %s" % '\t'.join( - missing) + raise OSError("Some filepaths were not accessible: %s" % '\t'.join( + missing)) def old_to_new_command(driver_name, project_title, local_argv): """Deprecate an old-style script. diff --git a/scripts/pyqi b/scripts/pyqi index 2f3ebf3..16b5393 100755 --- a/scripts/pyqi +++ b/scripts/pyqi @@ -26,7 +26,6 @@ from pyqi.core.interface import get_command_names, get_command_config from pyqi.core.interfaces.optparse import optparse_main, optparse_factory from pyqi.util import get_version_string from os.path import basename -from string import ljust from os import environ ### we actually have some flexibility here to make the driver interface agnostic as well @@ -54,26 +53,25 @@ def usage(cmd_cfg_mod, command_names): desc_limit = TERM_WIDTH - (INDENT + max_cmd + INDENT) cmd_end = INDENT + max_cmd + INDENT - print "usage: %s []" % argv[0] - print - print "The currently available commands are:" + print("usage: %s []\n" % argv[0]) + print("The currently available commands are:") # format: # indent command indent description for c, desc in valid_cmds: - cmd_formatted = ljust(''.join([' ' * INDENT, c]), cmd_end) - print ''.join([cmd_formatted, desc[:desc_limit]]) + s = ''.join([' ' * INDENT, c]) + cmd_formatted = s.ljust(cmd_end) + print(''.join([cmd_formatted, desc[:desc_limit]])) if invalid_cmds: - print - print "The following commands could not be loaded:" + print("\nThe following commands could not be loaded:") for c, error_msg in invalid_cmds: - cmd_formatted = ljust(''.join([' ' * INDENT, c]), cmd_end) - print ''.join([cmd_formatted, 'Error: %s' % error_msg]) + s = ''.join([' ' * INDENT, c]) + cmd_formatted = s.ljust(cmd_end) + print(''.join([cmd_formatted, 'Error: %s' % error_msg])) - print - print "See '%s help ' for more information on a specific command." % argv[0] + print("\nSee '%s help ' for more information on a specific command." % argv[0]) exit(0) def get_cmd_obj(cmd_cfg_mod, cmd): diff --git a/setup.py b/setup.py index 0667db3..07769fd 100644 --- a/setup.py +++ b/setup.py @@ -21,14 +21,6 @@ from glob import glob import sys -# from https://wiki.python.org/moin/PortingPythonToPy3k -try: - # python 3.x - from distutils.command.build_py import build_py_2to3 as build_py -except ImportError: - # python 2.x - from distutils.command.build_py import build_py - # classes/classifiers code adapted from Celery: # https://github.com/celery/celery/blob/master/setup.py # @@ -41,6 +33,7 @@ Topic :: Software Development :: User Interfaces Programming Language :: Python Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3.3 Programming Language :: Python :: Implementation :: CPython Operating System :: OS Independent Operating System :: POSIX @@ -50,14 +43,13 @@ # Verify Python version ver = '.'.join(map(str, [sys.version_info.major, sys.version_info.minor])) -if ver not in ['2.7']: - sys.stderr.write("Only Python >=2.7 and <3.0 is supported.") +if ver not in ['2.7', '3.3']: + sys.stderr.write("Only Python 2.7 and 3.3 are supported.") sys.exit(1) long_description = """pyqi (canonically pronounced pie chee) is a Python framework designed to support wrapping general commands in multiple types of interfaces, including at the command line, HTML, and API levels.""" setup(name='pyqi', - cmdclass={'build_py':build_py}, version=__version__, license=__license__, description='pyqi: expose your interface', diff --git a/tests/test_commands/test_code_header_generator.py b/tests/test_commands/test_code_header_generator.py index b1c91e7..dc1cc38 100644 --- a/tests/test_commands/test_code_header_generator.py +++ b/tests/test_commands/test_code_header_generator.py @@ -24,7 +24,7 @@ def test_run(self): obs = self.cmd(author='bob', email='bob@bob.bob', license='very permissive license', copyright='what\'s that?', version='1.0') - self.assertEqual(obs.keys(), ['result']) + self.assertEqual(list(obs.keys()), ['result']) obs = obs['result'] self.assertEqual('\n'.join(obs), exp_header1) @@ -34,14 +34,14 @@ def test_run(self): license='very permissive license', copyright='what\'s that?', version='1.0', credits=['another person', 'another another person']) - self.assertEqual(obs.keys(), ['result']) + self.assertEqual(list(obs.keys()), ['result']) obs = obs['result'] self.assertEqual('\n'.join(obs), exp_header2) # With no arguments obs = self.cmd() - self.assertEqual(obs.keys(), ['result']) + self.assertEqual(list(obs.keys()), ['result']) obs = obs['result'] self.assertEqual('\n'.join(obs), exp_header3) diff --git a/tests/test_commands/test_make_bash_completion.py b/tests/test_commands/test_make_bash_completion.py index aad1c08..2305da2 100644 --- a/tests/test_commands/test_make_bash_completion.py +++ b/tests/test_commands/test_make_bash_completion.py @@ -75,7 +75,7 @@ def test_run(self): params = {'command_config_module':self.temp_module_name, 'driver_name':'pyqi'} obs = self.cmd(**params) - self.assertEqual(obs.keys(), ['result']) + self.assertEqual(list(obs.keys()), ['result']) self.assertEqual(obs['result'], outputandstuff) diff --git a/tests/test_commands/test_make_command.py b/tests/test_commands/test_make_command.py index 94a680c..25f083a 100644 --- a/tests/test_commands/test_make_command.py +++ b/tests/test_commands/test_make_command.py @@ -24,7 +24,7 @@ def test_run_command_code_generation(self): obs = self.cmd(name='Test', author='bob', email='bob@bob.bob', license='very permissive license', copyright='what\'s that?', version='1.0') - self.assertEqual(obs.keys(), ['result']) + self.assertEqual(list(obs.keys()), ['result']) obs = obs['result'] self.assertEqual(obs, exp_command_code1.splitlines()) @@ -35,7 +35,7 @@ def test_run_test_code_generation(self): license='very permissive license', copyright='what\'s that?', version='1.0', credits=['another person'], test_code=True) - self.assertEqual(obs.keys(), ['result']) + self.assertEqual(list(obs.keys()), ['result']) obs = obs['result'] self.assertEqual('\n'.join(obs), exp_test_code1) diff --git a/tests/test_commands/test_make_optparse.py b/tests/test_commands/test_make_optparse.py index d36dc2f..ae30658 100644 --- a/tests/test_commands/test_make_optparse.py +++ b/tests/test_commands/test_make_optparse.py @@ -22,7 +22,7 @@ def setUp(self): def test_run(self): exp = win_text - + pc = CommandIn(Name='DUN', Required=True, DataType=str, Description="") bool_param = CommandIn(Name='imabool', DataType=bool, Description='zero or one', Required=False) @@ -39,9 +39,9 @@ class stubby: 'copyright': 'what\'s that?', 'version': '1.0' }) - + self.assertEqual(obs['result'], exp.splitlines()) - + win_text = """#!/usr/bin/env python from __future__ import division @@ -105,7 +105,7 @@ class stubby: # Help='output filepath') OptparseOption(Parameter=cmd_in_lookup('DUN'), - Type=, + Type=str, Action='store', # default is 'store', change if desired Handler=None, # must be defined if desired ShortName=None, # must be defined if desired @@ -136,7 +136,7 @@ class stubby: # # value will be made available to Handler. This name # # can be either an underscored or dashed version of the # # option name (e.g., 'output_fp' or 'output-fp') - # InputName='output-fp'), + # InputName='output-fp'), # # An example option that does not map to a CommandIn. # OptparseResult(Parameter=cmd_out_lookup('some_other_result'), diff --git a/tests/test_core/test_hdf5.py b/tests/test_core/test_hdf5.py new file mode 100644 index 0000000..fe3f737 --- /dev/null +++ b/tests/test_core/test_hdf5.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +#----------------------------------------------------------------------------- +# Copyright (c) 2013, The BiPy Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- +from __future__ import division + +__credits__ = ["Daniel McDonald"] + +import unittest + +from unittest import TestCase, main +from os import remove +from numpy import array, hstack +from pyqi.core.hdf5 import AutoExtendHDF5 + +try: + import h5py + h5py_missing = False +except ImportError: + h5py_missing = True + +@unittest.skipIf(h5py_missing, "h5py is not present") +class HDF5AutoExtendTests(TestCase): + def setUp(self): + self.hdf5_file = h5py.File('_test_file.hdf5','w') + self.obj = AutoExtendHDF5(self.hdf5_file) + + def tearDown(self): + remove('_test_file.hdf5') + + def test_create_dataset(self): + """Create something""" + self.obj.create_dataset('test_group/test_ds', int) + self.assertEqual(array([0],int), self.obj.f['test_group/test_ds'][:]) + self.assertEqual(self.obj._known_datasets, ['test_group/test_ds']) + + def test_extend(self): + """Check the implicit resizing""" + name = 'test_group/test_ds' + fetch = lambda x: x[name][:x[name].attrs['next_item']] + size = lambda x: x[name].size + + self.obj.create_dataset(name, int) + ds1 = array([1,2,3,4]) + ds2 = array([5,10,20,50]) + ds3 = array([44,44,44,123,123]) + ds4 = array([1]) + + self.obj.extend(name, ds1) + self.assertTrue((fetch(self.obj.f) == ds1).all()) + self.assertEqual(self.obj.f[name].attrs['next_item'], 4) + self.assertEqual(size(self.obj.f), 8) + + self.obj.extend(name, ds2) + exp = hstack([ds1, ds2]) + obs = fetch(self.obj.f) + self.assertTrue((obs == exp).all()) + self.assertEqual(self.obj.f[name].attrs['next_item'], 8) + self.assertEqual(size(self.obj.f), 16) + + self.obj.extend(name, ds3) + exp = hstack([ds1, ds2, ds3]) + obs = fetch(self.obj.f) + self.assertTrue((obs == exp).all()) + self.assertEqual(self.obj.f[name].attrs['next_item'], 13) + self.assertEqual(size(self.obj.f), 16) + + self.obj.extend(name, ds4) + exp = hstack([ds1, ds2, ds3, ds4]) + obs = fetch(self.obj.f) + self.assertTrue((obs == exp).all()) + self.assertEqual(self.obj.f[name].attrs['next_item'], 14) + self.assertEqual(size(self.obj.f), 16) + + def test_finalize(self): + """Make sure we can finalize a dataset""" + name = 'test_group/test_ds' + fetch = lambda x: x[name][:] + size = lambda x: x[name].size + self.obj.create_dataset(name, int) + self.obj.extend(name, array([1,2,3])) + self.obj.extend(name, array([1,2,3])) + self.obj.extend(name, array([1,2,3])) + self.obj.extend(name, array([1,2,3])) + self.obj.extend(name, array([1,2,3])) + + self.assertEqual(size(self.obj.f), 24) + exp = array([1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,0,0,0,0,0,0,0,0,0]) + obs = fetch(self.obj.f) + self.assertTrue((obs == exp).all()) + + self.obj._finalize(name) + exp = array([1,2,3] * 5) + obs = fetch(self.obj.f) + self.assertTrue((obs == exp).all()) + + self.assertFalse('next_item' in self.obj.f[name].attrs) + +if __name__ == '__main__': + main() diff --git a/tests/test_core/test_interface.py b/tests/test_core/test_interface.py index 63e05c0..b0b292e 100644 --- a/tests/test_core/test_interface.py +++ b/tests/test_core/test_interface.py @@ -12,14 +12,17 @@ __credits__ = ["Greg Caporaso", "Daniel McDonald", "Doug Wendel", "Jai Ram Rideout"] +import sys + from unittest import TestCase, main from pyqi.core.interface import get_command_names, get_command_config +from pyqi.util import is_py2 import pyqi.interfaces.optparse.config.make_bash_completion class TopLevelTests(TestCase): def test_get_command_names(self): """Test that command names are returned from a config directory.""" - exp = ['make-bash-completion', 'make-command', 'make-optparse', + exp = ['make-bash-completion', 'make-command', 'make-optparse', 'make-release', 'serve-html-interface'] obs = get_command_names('pyqi.interfaces.optparse.config') self.assertEqual(obs, exp) @@ -40,8 +43,13 @@ def test_get_command_config(self): 'hopefully.nonexistent.python.module', 'umm', exit_on_failure=False) self.assertEqual(cmd_cfg, None) - self.assertEqual(error_msg, 'No module named hopefully.nonexistent.' - 'python.module.umm') + + py2_err = 'No module named hopefully.nonexistent.python.module.umm' + py3_err = "No module named 'hopefully'" + if is_py2(): + self.assertEqual(error_msg, py2_err) + else: + self.assertEqual(error_msg, py3_err) if __name__ == '__main__': diff --git a/tests/test_core/test_interfaces/test_html/test_input_handler.py b/tests/test_core/test_interfaces/test_html/test_input_handler.py index e22dafe..7891672 100644 --- a/tests/test_core/test_interfaces/test_html/test_input_handler.py +++ b/tests/test_core/test_interfaces/test_html/test_input_handler.py @@ -10,8 +10,14 @@ __credits__ = ["Evan Bolyen"] -from StringIO import StringIO +import sys from unittest import TestCase, main + +if sys.version_info.major == 2: + from StringIO import StringIO +else: + from io import StringIO + from pyqi.core.exception import IncompetentDeveloperError from pyqi.core.interfaces.html.input_handler import (load_file_lines, load_file_contents) @@ -34,8 +40,8 @@ def test_load_file_lines(self): # can't load a string, etc... self.assertRaises(IncompetentDeveloperError, load_file_lines, 'This is not a file') result = load_file_lines(self.file_like_object) - self.assertEqual(result, - ["This is line 1", + self.assertEqual(result, + ["This is line 1", "This is line 2", "This is line 3"]) diff --git a/tests/test_core/test_interfaces/test_optparse/test_init.py b/tests/test_core/test_interfaces/test_optparse/test_init.py index 9e85823..4ecffe0 100644 --- a/tests/test_core/test_interfaces/test_optparse/test_init.py +++ b/tests/test_core/test_interfaces/test_optparse/test_init.py @@ -100,7 +100,7 @@ def test_validate_inputs(self): def test_input_handler(self): obs = self.interface._input_handler(['--c','foo']) - self.assertEqual(obs.items(), [('c', 'foo')]) + self.assertEqual(list(obs.items()), [('c', 'foo')]) def test_build_usage_lines(self): obs = self.interface._build_usage_lines([]) @@ -177,8 +177,10 @@ def setUp(self): self._dirs_to_clean_up = [] def tearDown(self): - map(remove, self._paths_to_clean_up) - map(rmdir, self._dirs_to_clean_up) + for p in self._paths_to_clean_up: + remove(p) + for d in self._dirs_to_clean_up: + rmdir(d) def test_check_existing_filepath(self): # Check that returns the correct value when the file exists @@ -199,8 +201,8 @@ def test_check_existing_filepath(self): def test_check_existing_filepaths(self): # Check that returns a list with the paths, in the same order as # the input comma separated list - tmp_f1, tmp_path1 = mkstemp(prefix='pyqi_tmp_') - tmp_f2, tmp_path2 = mkstemp(prefix='pyqi_tmp_') + tmp_f1, tmp_path1 = mkstemp(prefix='pyqi_tmp_testf') + tmp_f2, tmp_path2 = mkstemp(prefix='pyqi_tmp_testf') self._paths_to_clean_up = [tmp_path1, tmp_path2] option = PyqiOption('-f', '--files_test', type='existing_filepaths') exp = [tmp_path1, tmp_path2] @@ -245,8 +247,8 @@ def test_check_existing_dirpath(self): def test_check_existing_dirpaths(self): # Check that returns a list with the paths, in the same order as the # input comma separated list - tmp_dirpath1 = mkdtemp(prefix='pyqi_tmp_') - tmp_dirpath2 = mkdtemp(prefix='pyqi_tmp_') + tmp_dirpath1 = mkdtemp(prefix='pyqi_tmp_testd_') + tmp_dirpath2 = mkdtemp(prefix='pyqi_tmp_testd_') self._dirs_to_clean_up = [tmp_dirpath1, tmp_dirpath2] option = PyqiOption('-d', '--dirs_test', type='existing_dirpaths') exp = [tmp_dirpath1, tmp_dirpath2] diff --git a/tests/test_core/test_interfaces/test_optparse/test_output_handler.py b/tests/test_core/test_interfaces/test_optparse/test_output_handler.py index c4c9a82..22a687c 100644 --- a/tests/test_core/test_interfaces/test_optparse/test_output_handler.py +++ b/tests/test_core/test_interfaces/test_optparse/test_output_handler.py @@ -13,7 +13,14 @@ import os import sys -from StringIO import StringIO + +from pyqi.util import is_py2 + +if is_py2(): + from StringIO import StringIO +else: + from io import StringIO + from shutil import rmtree from tempfile import mkdtemp from unittest import TestCase, main diff --git a/tests/test_util.py b/tests/test_util.py index 30462b5..f751196 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -17,6 +17,7 @@ from pyqi.util import get_version_string from pyqi.core.exception import MissingVersionInfoError + class UtilTests(TestCase): def test_get_version_string(self): """Test extracting a version string given a module string.""" diff --git a/tox.ini b/tox.ini index b1547c6..1b99b7a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,7 @@ [tox] -envlist = py27 +envlist = py27,py33 [testenv] -deps=nose -commands=nosetests +deps=nose +commands= + nosetests + pyqi