From 8431b090ab8e9a7a8cf64dfd031e771cd6b740bb Mon Sep 17 00:00:00 2001 From: ocefpaf Date: Thu, 7 May 2015 18:02:45 -0300 Subject: [PATCH 1/3] Adding test_coding_standards.py + py.test. --- .travis.yml | 3 +- cf_units/__init__.py | 5 +- cf_units/cf_units.py | 28 ++- cf_units/tests/test_coding_standards.py | 309 ++++++++++++++++++++++++ cf_units/tests/unit/__init__.py | 19 ++ cf_units/tests/unit/unit/test_Unit.py | 82 +++++++ setup.py | 34 +-- 7 files changed, 457 insertions(+), 23 deletions(-) create mode 100644 cf_units/tests/test_coding_standards.py create mode 100644 cf_units/tests/unit/__init__.py create mode 100644 cf_units/tests/unit/unit/test_Unit.py diff --git a/.travis.yml b/.travis.yml index 3b7779b9..4b66e1e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,6 @@ install: - SITE_CFG=cf_units/etc/site.cfg - echo "[System]" >> $SITE_CFG - echo "udunits2_path = $PREFIX/lib/libudunits2.so" >> $SITE_CFG - - python setup.py --quiet install script: - nosetests --verbose --nocapture cf_units/tests/test_unit.py + - python setup.py test diff --git a/cf_units/__init__.py b/cf_units/__init__.py index af9114e7..12bb7f5a 100644 --- a/cf_units/__init__.py +++ b/cf_units/__init__.py @@ -15,9 +15,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with cf_units. If not, see . +from __future__ import (absolute_import, division, print_function) + __version__ = '0.1.0' from .cf_units import * -from .cf_units import (CALENDAR_STANDARD, CALENDARS, UT_NAMES, UT_DEFINITION, - FLOAT32, FLOAT64, julian_day2date, date2julian_day, - is_time, is_vertical) diff --git a/cf_units/cf_units.py b/cf_units/cf_units.py index 527702e3..00e99d25 100644 --- a/cf_units/cf_units.py +++ b/cf_units/cf_units.py @@ -42,8 +42,32 @@ from . import util -__all__ = ['Unit', 'date2num', 'decode_time', 'encode_clock', 'encode_date', - 'encode_time', 'num2date'] +__all__ = ['CALENDAR_STANDARD', + 'CALENDAR_GREGORIAN', + 'CALENDAR_PROLEPTIC_GREGORIAN', + 'CALENDAR_NO_LEAP', + 'CALENDAR_JULIAN', + 'CALENDAR_ALL_LEAP', + 'CALENDAR_365_DAY', + 'CALENDAR_366_DAY', + 'CALENDAR_360_DAY', + 'CALENDARS', + 'UT_NAMES', + 'UT_DEFINITION', + 'FLOAT32', + 'FLOAT64', + 'julian_day2date', + 'date2julian_day', + 'is_time', + 'is_vertical', + 'Unit', + 'date2num', + 'decode_time', + 'encode_clock', + 'encode_date', + 'encode_time', + 'num2date', + 'suppress_unit_warnings'] ######################################################################## diff --git a/cf_units/tests/test_coding_standards.py b/cf_units/tests/test_coding_standards.py new file mode 100644 index 00000000..a653c850 --- /dev/null +++ b/cf_units/tests/test_coding_standards.py @@ -0,0 +1,309 @@ +# (C) British Crown Copyright 2013 - 2015, Met Office +# +# This file is part of cf_units. +# +# cf_units is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cf_units is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with cf_units. If not, see . + +from __future__ import (absolute_import, division, print_function) + +from datetime import datetime +from fnmatch import fnmatch +from glob import glob +from itertools import chain +import os +import re +import subprocess +import unittest + +import pep8 + +import cf_units + + +LICENSE_TEMPLATE = """ +# (C) British Crown Copyright {YEARS}, Met Office +# +# This file is part of cf_units. +# +# cf_units is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cf_units is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with cf_units. If not, see .""".strip() + + +LICENSE_RE_PATTERN = re.escape(LICENSE_TEMPLATE).replace('\{YEARS\}', '(.*?)') +# Add shebang possibility to the LICENSE_RE_PATTERN +LICENSE_RE_PATTERN = r'(\#\!.*\n)?' + LICENSE_RE_PATTERN +LICENSE_RE = re.compile(LICENSE_RE_PATTERN, re.MULTILINE) + + +# Guess cf_units repo directory of cf_units - realpath is used to mitigate +# against Python finding the cf_units package via a symlink. +DIR = os.path.realpath(os.path.dirname(cf_units.__file__)) +REPO_DIR = os.path.dirname(os.path.dirname(DIR)) +DOCS_DIR = os.path.join(REPO_DIR, 'docs', 'cf_units') +DOCS_DIR = cf_units.config.get_option('Resources', 'doc_dir', default=DOCS_DIR) +exclusion = ['Makefile', 'build'] +DOCS_DIRS = glob(os.path.join(DOCS_DIR, '*')) +DOCS_DIRS = [DOC_DIR for DOC_DIR in DOCS_DIRS if os.path.basename(DOC_DIR) not + in exclusion] + + +class StandardReportWithExclusions(pep8.StandardReport): + expected_bad_files = [] + optional_bad_files = [] + expected_bad_files += optional_bad_files + matched_exclusions = set() + + def get_file_results(self): + # If the file had no errors, return self.file_errors (which will be 0) + if not self._deferred_print: + return self.file_errors + + # Iterate over all of the patterns, to find a possible exclusion. If we + # the filename is to be excluded, go ahead and remove the counts that + # self.error added. + for pattern in self.expected_bad_files: + if fnmatch(self.filename, pattern): + self.matched_exclusions.add(pattern) + # invert the error method's counters. + for _, _, code, _, _ in self._deferred_print: + self.counters[code] -= 1 + if self.counters[code] == 0: + self.counters.pop(code) + self.messages.pop(code) + self.file_errors -= 1 + self.total_errors -= 1 + return self.file_errors + + # Otherwise call the superclass' method to print the bad results. + return super(StandardReportWithExclusions, + self).get_file_results() + + +class TestCodeFormat(unittest.TestCase): + def test_pep8_conformance(self): + # + # Tests the cf_units codebase against the "pep8" tool. + # + # Users can add their own excluded files (should files exist in the + # local directory which is not in the repository) by adding a + # ".pep8_test_exclude.txt" file in the same directory as this test. + # The file should be a line separated list of filenames/directories + # as can be passed to the "pep8" tool's exclude list. + + # To get a list of bad files, rather than the specific errors, add + # "reporter=pep8.FileReport" to the StyleGuide constructor. + pep8style = pep8.StyleGuide(quiet=False, + reporter=StandardReportWithExclusions) + + # Allow users to add their own exclude list. + extra_exclude_file = os.path.join(os.path.dirname(__file__), + '.pep8_test_exclude.txt') + if os.path.exists(extra_exclude_file): + with open(extra_exclude_file, 'r') as fh: + extra_exclude = [line.strip() for line in fh if line.strip()] + pep8style.options.exclude.extend(extra_exclude) + + check_paths = [os.path.dirname(cf_units.__file__)] + if DOCS_DIRS: + check_paths.extend(DOCS_DIRS) + + result = pep8style.check_files(check_paths) + self.assertEqual(result.total_errors, 0, "Found code syntax " + "errors (and warnings).") + + reporter = pep8style.options.reporter + # If we've been using the exclusions reporter, check that we didn't + # exclude files unnecessarily. + if reporter is StandardReportWithExclusions: + unexpectedly_good = sorted(set(reporter.expected_bad_files) - + set(reporter.optional_bad_files) - + reporter.matched_exclusions) + + if unexpectedly_good: + self.fail('Some exclude patterns were unnecessary as the ' + 'files they pointed to either passed the PEP8 tests ' + 'or do not point to a file:\n ' + '{}'.format('\n '.join(unexpectedly_good))) + + +class TestLicenseHeaders(unittest.TestCase): + @staticmethod + def years_of_license_in_file(fh): + """ + Using :data:`LICENSE_RE` look for the years defined in the license + header of the given file handle. + + If the license cannot be found in the given fh, None will be returned, + else a tuple of (start_year, end_year) will be returned. + + """ + license_matches = LICENSE_RE.match(fh.read()) + if not license_matches: + # no license found in file. + return None + + years = license_matches.groups()[-1] + if len(years) == 4: + start_year = end_year = int(years) + elif len(years) == 11: + start_year, end_year = int(years[:4]), int(years[7:]) + else: + fname = getattr(fh, 'name', 'unknown filename') + raise ValueError("Unexpected year(s) string in {}'s copyright " + "notice: {!r}".format(fname, years)) + return (start_year, end_year) + + @staticmethod + def whatchanged_parse(whatchanged_output): + """ + Returns a generator of tuples of data parsed from + "git whatchanged --pretty='TIME:%at'". The tuples are of the form + ``(filename, last_commit_datetime)`` + + Sample input:: + + ['TIME:1366884020', '', + ':000000 100644 0000000... 5862ced... A\tcf_units/cf_units.py'] + + """ + dt = None + for line in whatchanged_output: + if not line.strip(): + continue + elif line.startswith('TIME:'): + dt = datetime.fromtimestamp(int(line[5:])) + else: + # Non blank, non date, line -> must be the lines + # containing the file info. + fname = ' '.join(line.split('\t')[1:]) + yield fname, dt + + @staticmethod + def last_change_by_fname(): + """ + Return a dictionary of all the files under git which maps to + the datetime of their last modification in the git history. + + .. note:: + + This function raises a ValueError if the repo root does + not have a ".git" folder. If git is not installed on the system, + or cannot be found by subprocess, an IOError may also be raised. + + """ + # Check the ".git" folder exists at the repo dir. + if not os.path.isdir(os.path.join(REPO_DIR, '.git')): + raise ValueError('{} is not a git repository.'.format(REPO_DIR)) + + # Call "git whatchanged" to get the details of all the files and when + # they were last changed. + output = subprocess.check_output(['git', 'whatchanged', + "--pretty=TIME:%ct"], + cwd=REPO_DIR) + output = output.split('\n') + res = {} + for fname, dt in TestLicenseHeaders.whatchanged_parse(output): + if fname not in res or dt > res[fname]: + res[fname] = dt + + return res + + def test_license_headers(self): + exclude_patterns = ('setup.py', + 'build/*', + 'dist/*') + + try: + last_change_by_fname = self.last_change_by_fname() + except ValueError: + # Caught the case where this is not a git repo. + return self.skipTest('cf_units installation did not look like a ' + 'git repo.') + + failed = False + for fname, last_change in sorted(last_change_by_fname.items()): + full_fname = os.path.join(REPO_DIR, fname) + if full_fname.endswith('.py') and os.path.isfile(full_fname) and \ + not any(fnmatch(fname, pat) for pat in exclude_patterns): + with open(full_fname) as fh: + years = TestLicenseHeaders.years_of_license_in_file(fh) + if years is None: + print('The file {} has no valid header license and ' + 'has not been excluded from the license header ' + 'test.'.format(fname)) + failed = True + elif last_change.year > years[1]: + print('The file header at {} is out of date. The last' + ' commit was in {}, but the copyright states it' + ' was {}.'.format(fname, last_change.year, + years[1])) + failed = True + + if failed: + raise ValueError('There were license header failures. See stdout.') + + +class TestFutureImports(unittest.TestCase): + excluded = () + + future_imports_pattern = re.compile( + r"^from __future__ import \(absolute_import,\s*division,\s*" + r"print_function(,\s*unicode_literals)?\)$", + flags=re.MULTILINE) + + def test_future_imports(self): + # Tests that every single Python file includes the appropriate + # __future__ import to enforce consistent behaviour. + check_paths = [os.path.dirname(cf_units.__file__)] + if DOCS_DIRS: + check_paths.extend(DOCS_DIRS) + + failed = False + for dirpath, _, files in chain.from_iterable(os.walk(path) + for path in check_paths): + for fname in files: + full_fname = os.path.join(dirpath, fname) + if not full_fname.endswith('.py'): + continue + if not os.path.isfile(full_fname): + continue + if any(fnmatch(full_fname, pat) for pat in self.excluded): + continue + + with open(full_fname, "r") as fh: + content = fh.read() + + if re.search(self.future_imports_pattern, content) is None: + print('The file {} has no valid __future__ imports ' + 'and has not been excluded from the imports ' + 'test.'.format(full_fname)) + failed = True + + if failed: + raise ValueError('There were __future__ import check failures. ' + 'See stdout.') + + +if __name__ == '__main__': + unittest.main() diff --git a/cf_units/tests/unit/__init__.py b/cf_units/tests/unit/__init__.py new file mode 100644 index 00000000..e18d93b1 --- /dev/null +++ b/cf_units/tests/unit/__init__.py @@ -0,0 +1,19 @@ +# (C) British Crown Copyright 2013 - 2014, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Unit tests for the :mod:`iris` package.""" + +from __future__ import (absolute_import, division, print_function) diff --git a/cf_units/tests/unit/unit/test_Unit.py b/cf_units/tests/unit/unit/test_Unit.py new file mode 100644 index 00000000..d1e23464 --- /dev/null +++ b/cf_units/tests/unit/unit/test_Unit.py @@ -0,0 +1,82 @@ +# (C) British Crown Copyright 2014, Met Office +# +# This file is part of cf_units. +# +# cf_units is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cf_units is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with cf_units. If not, see . +"""Unit tests for the `cf_units.Unit` class.""" + +from __future__ import (absolute_import, division, print_function) + +import unittest + +import numpy as np + +import cf_units +from cf_units import Unit + + +class Test___init__(unittest.TestCase): + + def test_capitalised_calendar(self): + calendar = 'GrEgoRian' + expected = cf_units.CALENDAR_GREGORIAN + u = Unit('hours since 1970-01-01 00:00:00', calendar=calendar) + self.assertEqual(u.calendar, expected) + + def test_not_basestring_calendar(self): + with self.assertRaises(TypeError): + u = Unit('hours since 1970-01-01 00:00:00', calendar=5) + + +class Test_convert(unittest.TestCase): + + class MyStr(str): + pass + + def test_gregorian_calendar_conversion_setup(self): + # Reproduces a situation where a unit's gregorian calendar would not + # match (using the `is` operator) to the literal string 'gregorian', + # causing an `is not` test to return a false negative. + cal_str = cf_units.CALENDAR_GREGORIAN + calendar = self.MyStr(cal_str) + self.assertIsNot(calendar, cal_str) + u1 = Unit('hours since 1970-01-01 00:00:00', calendar=calendar) + u2 = Unit('hours since 1969-11-30 00:00:00', calendar=calendar) + u1point = np.array([8.], dtype=np.float32) + expected = np.array([776.], dtype=np.float32) + result = u1.convert(u1point, u2) + return expected, result + + def test_gregorian_calendar_conversion_array(self): + expected, result = self.test_gregorian_calendar_conversion_setup() + np.testing.assert_array_equal(expected, result) + + def test_gregorian_calendar_conversion_dtype(self): + expected, result = self.test_gregorian_calendar_conversion_setup() + self.assertEqual(expected.dtype, result.dtype) + + def test_gregorian_calendar_conversion_shape(self): + expected, result = self.test_gregorian_calendar_conversion_setup() + self.assertEqual(expected.shape, result.shape) + + def test_non_gregorian_calendar_conversion_dtype(self): + data = np.arange(4, dtype=np.float32) + u1 = Unit('hours since 2000-01-01 00:00:00', calendar='360_day') + u2 = Unit('hours since 2000-01-02 00:00:00', calendar='360_day') + result = u1.convert(data, u2) + self.assertEqual(result.dtype, np.float32) + + +if __name__ == '__main__': + tests.main() diff --git a/setup.py b/setup.py index 39508815..2a6ec91f 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,24 @@ import os +import sys try: from setuptools import setup + from setuptools.command.test import test as TestCommand except ImportError: from distutils.core import setup +class PyTest(TestCommand): + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = ['--verbose'] + self.test_suite = True + + def run_tests(self): + import pytest + errno = pytest.main(self.test_args) + sys.exit(errno) + + def extract_version(): version = None fdir = os.path.dirname(__file__) @@ -13,7 +27,8 @@ def extract_version(): for line in fd: if (line.startswith('__version__')): _, version = line.split('=') - version = version.strip()[1:-1] # Remove quotation characters. + # Remove quotation characters. + version = version.strip()[1:-1] break return version @@ -48,18 +63,5 @@ def read(*parts): package_data={'cf_units': list(file_walk_relative('cf_units/etc', remove='cf_units/'))}, data_files=[('cf_units', ['COPYING', 'COPYING.LESSER'])], - tests_require=['nose'], - long_description=long_description, - classifiers=['Development Status :: 4 - Beta', - 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX', - 'Operating System :: POSIX :: AIX', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Topic :: Scientific/Engineering'], - description='Units of measure as required by the Climate and Forecast (CF) metadata conventions', -) + tests_require=['pytest'], + cmdclass={'test': PyTest},) From f15787ef380aea0ff90ee69194f49fb32446ccc7 Mon Sep 17 00:00:00 2001 From: Richard Hattersley Date: Wed, 13 May 2015 09:00:59 +0100 Subject: [PATCH 2/3] Don't suppress warnings. --- cf_units/cf_units.py | 72 ++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/cf_units/cf_units.py b/cf_units/cf_units.py index 00e99d25..295b9823 100644 --- a/cf_units/cf_units.py +++ b/cf_units/cf_units.py @@ -67,7 +67,7 @@ 'encode_date', 'encode_time', 'num2date', - 'suppress_unit_warnings'] + 'suppress_errors'] ######################################################################## @@ -336,28 +336,40 @@ FLOAT64: _cv_convert_doubles} _numpy2ctypes = {np.float32: FLOAT32, np.float64: FLOAT64} _ctypes2numpy = {v: k for k, v in _numpy2ctypes.iteritems()} -# -# load the UDUNITS-2 xml-formatted unit-database -# -if not _ud_system: + + +@contextmanager +def suppress_errors(): + """ + Suppresses all error messages from UDUNITS-2. + + """ _func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_char_p, use_errno=True) _set_handler_type = ctypes.CFUNCTYPE(_func_type, _func_type) _ut_set_error_message_handler = _set_handler_type((_UT_HANDLER, _lib_ud)) _ut_ignore = _func_type((_UT_IGNORE, _lib_ud)) - # ignore standard UDUNITS-2 start-up preamble redirected to stderr stream _default_handler = _ut_set_error_message_handler(_ut_ignore) - # Load the unit-database from the default location (modified via - # the UDUNITS2_XML_PATH environment variable) and if that fails look - # relative to sys.prefix to support environments such as conda. - _ud_system = _ut_read_xml(None) - if _ud_system is None: - _alt_xml_path = os.path.join(sys.prefix, 'share', - 'udunits', 'udunits2.xml') - _ud_system = _ut_read_xml(_alt_xml_path) - # reinstate old error handler - _ut_set_error_message_handler(_default_handler) - del _func_type + try: + yield + finally: + _ut_set_error_message_handler(_default_handler) + + +# +# load the UDUNITS-2 xml-formatted unit-database +# +if not _ud_system: + # Ignore standard noisy UDUNITS-2 start-up. + with suppress_errors(): + # Load the unit-database from the default location (modified via + # the UDUNITS2_XML_PATH environment variable) and if that fails look + # relative to sys.prefix to support environments such as conda. + _ud_system = _ut_read_xml(None) + if _ud_system is None: + _alt_xml_path = os.path.join(sys.prefix, 'share', + 'udunits', 'udunits2.xml') + _ud_system = _ut_read_xml(_alt_xml_path) if not _ud_system: _status_msg = 'UNKNOWN' _error_msg = '' @@ -729,32 +741,6 @@ def num2date(time_value, unit, calendar): return cdftime.num2date(time_value) -def _handler(func): - """Set the error message handler.""" - - _ut_set_error_message_handler(func) - - -@contextmanager -def suppress_unit_warnings(): - """ - Suppresses all warnings raised because of invalid units in loaded data. - - """ - # Suppress any warning messages raised by UDUNITS2. - _func_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_char_p, - use_errno=True) - _set_handler_type = ctypes.CFUNCTYPE(_func_type, _func_type) - _ut_set_error_message_handler = _set_handler_type((_UT_HANDLER, _lib_ud)) - _ut_ignore = _func_type((_UT_IGNORE, _lib_ud)) - _default_handler = _ut_set_error_message_handler(_ut_ignore) - with warnings.catch_warnings(): - # Also suppress invalid units warnings from the cf_units loader code. - warnings.filterwarnings("ignore", message=".*invalid units") - yield - _ut_set_error_message_handler(_default_handler) - - ######################################################################## # # unit wrapper class for unidata/ucar UDUNITS-2 From 7bd72281ae2ec575bb3fbf786d2a8ae053fd56c8 Mon Sep 17 00:00:00 2001 From: Richard Hattersley Date: Wed, 13 May 2015 14:17:52 +0100 Subject: [PATCH 3/3] Remove _handler calls --- cf_units/tests/test_unit.py | 66 ++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/cf_units/tests/test_unit.py b/cf_units/tests/test_unit.py index a6150f75..9c0cbfab 100644 --- a/cf_units/tests/test_unit.py +++ b/cf_units/tests/test_unit.py @@ -34,15 +34,7 @@ Unit = unit.Unit -class TestUnit(unittest.TestCase): - def setUp(self): - unit._handler(unit._ut_ignore) - - def tearDown(self): - unit._handler(unit._default_handler) - - -class TestCreation(TestUnit): +class TestCreation(unittest.TestCase): # # test: unit creation # @@ -75,7 +67,7 @@ def test_unknown_calendar(self): u = Unit('hours since 1970-01-01 00:00:00', calendar='wibble') -class TestModulus(TestUnit): +class TestModulus(unittest.TestCase): # # test: modulus property # @@ -92,7 +84,7 @@ def test_modulus_pass_2(self): self.assertEqual(u.modulus, None) -class TestConvertible(TestUnit): +class TestConvertible(unittest.TestCase): # # test: convertible method # @@ -125,7 +117,7 @@ def test_convertible_fail_3(self): self.assertFalse(v.is_convertible(u)) -class TestDimensionless(TestUnit): +class TestDimensionless(unittest.TestCase): # # test: dimensionless property # @@ -146,7 +138,7 @@ def test_dimensionless_fail_2(self): self.assertFalse(u.is_dimensionless()) -class TestFormat(TestUnit): +class TestFormat(unittest.TestCase): # # test: format method # @@ -175,7 +167,7 @@ def test_format_pass_5(self): self.assertEqual(u.format(), 'no_unit') -class TestName(TestUnit): +class TestName(unittest.TestCase): # # test: name property # @@ -192,7 +184,7 @@ def test_name_pass_2(self): self.assertEqual(u.name, 'no_unit') -class TestSymbol(TestUnit): +class TestSymbol(unittest.TestCase): # # test: symbol property # @@ -209,7 +201,7 @@ def test_symbol_pass_2(self): self.assertEqual(u.symbol, unit._NO_UNIT_SYMBOL) -class TestDefinition(TestUnit): +class TestDefinition(unittest.TestCase): # # test: definition property # @@ -226,7 +218,7 @@ def test_definition_pass_2(self): self.assertEqual(u.definition, unit._NO_UNIT_SYMBOL) -class TestOffset(TestUnit): +class TestOffset(unittest.TestCase): # # test: offset method # @@ -255,7 +247,7 @@ def test_offset_pass_2(self): self.assertEqual(u + 1000L, "m @ 1000") -class TestOffsetByTime(TestUnit): +class TestOffsetByTime(unittest.TestCase): # # test: offset_by_time method # @@ -283,7 +275,7 @@ def test_offset_by_time_pass_0(self): self.assertEqual(v, "(3600 s) @ 20070115T120600.00000000 UTC") -class TestInvert(TestUnit): +class TestInvert(unittest.TestCase): # # test: invert method # @@ -301,7 +293,7 @@ def test_invert_pass_0(self): self.assertEqual(u.invert().invert(), "N") -class TestRoot(TestUnit): +class TestRoot(unittest.TestCase): # # test: root method # @@ -330,7 +322,7 @@ def test_root_pass_0(self): self.assertEqual(u.root(2), "V") -class TestLog(TestUnit): +class TestLog(unittest.TestCase): # # test: log method # @@ -351,7 +343,7 @@ def test_log_pass_0(self): self.assertEqual(u.log(10), "lg(re 100 Pa)") -class TestMultiply(TestUnit): +class TestMultiply(unittest.TestCase): def test_multiply_fail_0(self): u = Unit("amp") self.assertRaises(ValueError, operator.mul, u, "naughty") @@ -392,7 +384,7 @@ def test_multiply_pass_3(self): self.assertEqual((u * v).format(), "W") -class TestDivide(TestUnit): +class TestDivide(unittest.TestCase): def test_divide_fail_0(self): u = Unit("watts") self.assertRaises(ValueError, operator.div, u, "naughty") @@ -433,7 +425,7 @@ def test_divide_pass_3(self): self.assertEqual((u / v).format(), "A") -class TestPower(TestUnit): +class TestPower(unittest.TestCase): def test_power(self): u = Unit("amp") self.assertRaises(TypeError, operator.pow, u, "naughty") @@ -464,7 +456,7 @@ def test_power_nounit(self): self.assertRaises(ValueError, operator.pow, u, 2) -class TestCopy(TestUnit): +class TestCopy(unittest.TestCase): # # test: copy method # @@ -481,7 +473,7 @@ def test_copy_pass_2(self): self.assertTrue(copy.copy(u).is_no_unit()) -class TestStringify(TestUnit): +class TestStringify(unittest.TestCase): # # test: __str__ method # @@ -503,7 +495,7 @@ def test_repr_pass_1(self): self.assertEqual(repr(u), comp) -class TestRichComparison(TestUnit): +class TestRichComparison(unittest.TestCase): # # test: __eq__ method # @@ -551,7 +543,7 @@ def test_ne_cross_category(self): self.assertTrue(u != n) -class TestOrdering(TestUnit): +class TestOrdering(unittest.TestCase): def test_order(self): m = Unit("meter") u = Unit('unknown') @@ -560,7 +552,7 @@ def test_order(self): self.assertEqual(sorted(start), [m, n, u]) -class TestTimeEncoding(TestUnit): +class TestTimeEncoding(unittest.TestCase): # # test: encode_time module function # @@ -592,7 +584,7 @@ def test_decode_time_pass_0(self): (2006, 1, 15, 12, 6, 0)) -class TestConvert(TestUnit): +class TestConvert(unittest.TestCase): # # test: convert method # @@ -707,7 +699,7 @@ def test_convert_time_fail_0(self): u1.convert(u1point, u2) -class TestNumsAndDates(TestUnit): +class TestNumsAndDates(unittest.TestCase): # # test: num2date method # @@ -726,7 +718,7 @@ def test_date2num_pass_0(self): self.assertEqual(str(u.num2date(u.date2num(d))), "2010-11-02 13:00:00") -class TestUnknown(TestUnit): +class TestUnknown(unittest.TestCase): # # test: unknown units # @@ -751,7 +743,7 @@ def test_unknown_unit_fail_2(self): self.assertFalse(u.is_unknown()) -class TestNoUnit(TestUnit): +class TestNoUnit(unittest.TestCase): # # test: no unit # @@ -772,7 +764,7 @@ def test_no_unit_pass_3(self): self.assertTrue(u.is_no_unit()) -class TestTimeReference(TestUnit): +class TestTimeReference(unittest.TestCase): # # test: time reference # @@ -785,7 +777,7 @@ def test_time_reference_fail_0(self): self.assertFalse(u.is_time_reference()) -class TestTitle(TestUnit): +class TestTitle(unittest.TestCase): # # test: title # @@ -798,7 +790,7 @@ def test_title_pass_1(self): self.assertEqual(u.title(10), '1970-01-01 10:00:00') -class TestImmutable(TestUnit): +class TestImmutable(unittest.TestCase): def _set_attr(self, unit, name): setattr(unit, name, -999) raise ValueError("'Unit' attribute '%s' is mutable!" % name) @@ -824,7 +816,7 @@ def test_hash(self): self.assertEqual(len(h), 2) -class TestInPlace(TestUnit): +class TestInPlace(unittest.TestCase): def test1(self): # Check conversions do not change original object