diff --git a/LICENSE.txt b/LICENSE.txt index 7d09c3f..723aecf 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,10 +1,10 @@ -Copyright (c) 2010, Marty Alchin -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of the author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. - +Copyright (c) 2010, Marty Alchin +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of the author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index dcc355d..8808262 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include LICENSE.txt -include contents.csv -recursive-include tests *.py +include LICENSE.txt +include contents.csv +recursive-include tests *.py diff --git a/setup.py b/setup.py index 5da1dca..250fdd3 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,18 @@ -from distutils.core import setup - -setup(name='Sheets', - version='0.6', - author='Marty Alchin', - author_email='marty@martyalchin.com', - url='https://github.com/gulopine/sheets/', - packages=['sheets'], - license='BSD', - description='A declarative class framework for working with CSV files', - classifiers=[ - 'License :: OSI Approved :: BSD License', - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries :: Application Frameworks', - 'Programming Language :: Python :: 3.1', - ] +from distutils.core import setup + +setup(name='Sheets', + version='0.6', + author='Marty Alchin', + author_email='marty@martyalchin.com', + url='https://github.com/gulopine/sheets/', + packages=['sheets'], + license='BSD', + description='A declarative class framework for working with CSV files', + classifiers=[ + 'License :: OSI Approved :: BSD License', + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Libraries :: Application Frameworks', + 'Programming Language :: Python :: 3.1', + ] ) \ No newline at end of file diff --git a/sheets/__init__.py b/sheets/__init__.py index b221b6b..1a3b7d8 100644 --- a/sheets/__init__.py +++ b/sheets/__init__.py @@ -1,3 +1,3 @@ -from sheets.base import * -from sheets.options import * -from sheets.columns import * +from sheets.base import * +from sheets.options import * +from sheets.columns import * diff --git a/sheets/base.py b/sheets/base.py index c3ab343..6c918e7 100644 --- a/sheets/base.py +++ b/sheets/base.py @@ -1,117 +1,113 @@ -import csv -from collections import OrderedDict - -from sheets import options - -__all__ = ['Row', 'Reader', 'Writer'] - -class RowMeta(type): - def __init__(cls, name, bases, attrs): - if 'Dialect' in attrs: - # Filter out Python's own additions to the namespace - items = attrs.pop('Dialect').__dict__.items() - items = dict((k, v) for (k, v) in items if not k.startswith('__')) - else: - # No options were explicitly defined - items = {} - cls._dialect = options.Dialect(**items) - - for key, attr in attrs.items(): - if hasattr(attr, 'attach_to_class'): - attr.attach_to_class(cls, key, cls._dialect) - - @classmethod - def __prepare__(cls, name, bases): - return OrderedDict() - - -class Row(metaclass=RowMeta): - # Not yet written about - - def __init__(self, *args, **kwargs): - column_names = [column.name for column in self._dialect.columns] - - # First, make sure the arguments make sense - if len(args) > len(column_names): - msg = "__init__() takes at most %d arguments (%d given)" - raise TypeError(msg % (len(column_names), len(args))) - - for name in kwargs: - if name not in column_names: - raise TypeError("Got unknown keyword argument '%s'" % name) - - for i, name in enumerate(column_names[:len(args)]): - if name in kwargs: - msg = "__init__() got multiple values for keyword argument '%s'" - raise TypeError(msg % name) - kwargs[name] = args[i] - - # Now populate the actual values on the object - for column in self._dialect.columns: - try: - value = column.to_python(kwargs[column.name]) - except KeyError: - # No value was provided - value = None - setattr(self, column.name, value) - - errors = () - - def is_valid(self): - valid = True - self.errors = [] - for column in self._dialect.columns: - value = getattr(self, column.name) - try: - column.validate(value) - except ValueError as e: - self.errors.append(e) - valid = False - return valid - - @classmethod - def reader(cls, file): - return Reader(cls, file) - - @classmethod - def writer(cls, file): - return Writer(cls, file) - - -class Reader: - def __init__(self, row_cls, file): - self.row_cls = row_cls - self.csv_reader = csv.reader(file, **row_cls._dialect.csv_dialect) - self.skip_header_row = row_cls._dialect.has_header_row - - def __iter__(self): - return self - - def __next__(self): - # Skip the first row if it's a header - if self.skip_header_row: - self.csv_reader.__next__() - self.skip_header_row = False - - return self.row_cls(*self.csv_reader.__next__()) - - -class Writer: - def __init__(self, row_cls, file): - self.columns = row_cls._dialect.columns - self._writer = csv.writer(file, row_cls._dialect.csv_dialect) - self.needs_header_row = row_cls._dialect.has_header_row - - def writerow(self, row): - if self.needs_header_row: - values = [column.title.title() for column in self.columns] - self._writer.writerow(values) - self.needs_header_row = False - values = [getattr(row, column.name) for column in self.columns] - self._writer.writerow(values) - - def writerows(self, rows): - for row in rows: - self.writerow(row) - - +import csv + +from sheets import options + +__all__ = ['Row', 'Reader', 'Writer'] + + +class RowMeta(type): + def __init__(cls, name, bases, attrs): + if 'Dialect' in attrs: + # Filter out Python's own additions to the namespace + items = attrs.pop('Dialect').__dict__.items() + items = dict((k, v) for (k, v) in items if not k.startswith('__')) + else: + # No options were explicitly defined + items = {} + cls._dialect = options.Dialect(**items) + + for key, attr in attrs.items(): + if hasattr(attr, 'attach_to_class'): + attr.attach_to_class(cls, key, cls._dialect) + + +class Row(object): + __metaclass__ = RowMeta + # Not yet written about + + def __init__(self, *args, **kwargs): + column_names = [column.name for column in self._dialect.columns] + + # First, make sure the arguments make sense + if len(args) > len(column_names): + msg = "__init__() takes at most %d arguments (%d given)" + raise TypeError(msg % (len(column_names), len(args))) + + for name in kwargs: + if name not in column_names: + raise TypeError("Got unknown keyword argument '%s'" % name) + + for i, name in enumerate(column_names[:len(args)]): + if name in kwargs: + msg = ("__init__() got multiple values for keyword argument " + "'%s'") + raise TypeError(msg % name) + kwargs[name] = args[i] + + # Now populate the actual values on the object + for column in self._dialect.columns: + try: + value = column.to_python(kwargs[column.name]) + except KeyError: + # No value was provided + value = None + setattr(self, column.name, value) + + errors = () + + def is_valid(self): + valid = True + self.errors = [] + for column in self._dialect.columns: + value = getattr(self, column.name) + try: + column.validate(value) + except ValueError as e: + self.errors.append(e) + valid = False + return valid + + @classmethod + def reader(cls, file): + return Reader(cls, file) + + @classmethod + def writer(cls, file): + return Writer(cls, file) + + +class Reader(object): + def __init__(self, row_cls, file): + self.row_cls = row_cls + self.csv_reader = csv.reader(file, **row_cls._dialect.csv_dialect) + self.skip_header_row = row_cls._dialect.has_header_row + + def __iter__(self): + return self + + def __next__(self): + # Skip the first row if it's a header + if self.skip_header_row: + self.csv_reader.__next__() + self.skip_header_row = False + + return self.row_cls(*self.csv_reader.__next__()) + + +class Writer(object): + def __init__(self, row_cls, file): + self.columns = row_cls._dialect.columns + self._writer = csv.writer(file, row_cls._dialect.csv_dialect) + self.needs_header_row = row_cls._dialect.has_header_row + + def writerow(self, row): + if self.needs_header_row: + values = [column.title.title() for column in self.columns] + self._writer.writerow(values) + self.needs_header_row = False + values = [getattr(row, column.name) for column in self.columns] + self._writer.writerow(values) + + def writerows(self, rows): + for row in rows: + self.writerow(row) diff --git a/sheets/columns.py b/sheets/columns.py index 25fa790..b17d436 100644 --- a/sheets/columns.py +++ b/sheets/columns.py @@ -1,108 +1,116 @@ -import functools -import datetime -import decimal - -class Column: - """ - An individual column within a CSV file. This serves as a base for attributes - and methods that are common to all types of columns. Subclasses of Column - will define behavior for more specific data types. - """ - - def __init__(self, title=None, required=True): - self.title = title - self.required = required - self._validators = [self.to_python] - - def attach_to_class(self, cls, name, dialect): - self.cls = cls - self.name = name - self.dialect = dialect - if self.title is None: - # Check for None so that an empty string will skip this behavior - self.title = name.replace('_', ' ') - dialect.add_column(self) - - def to_python(self, value): - """ - Convert the given string to a native Python object. - """ - return value - - def to_string(self, value): - """ - Convert the given Python object to a string. - """ - return value - - # Not yet written about - - def validator(self, func): - self._validators.append(functools.partial(func, self)) - return func - - def validate(self, value): - """ - Validate that the given value matches the column's requirements. - Raise a ValueError only if the given value was invalid. - """ - for validate in self._validators: - validate(value) - - -class StringColumn(Column): - pass - - -class IntegerColumn(Column): - def to_python(self, value): - return int(value) - - -class FloatColumn(Column): - def to_python(self, value): - return float(value) - - -class DecimalColumn(Column): - """ - A column that contains data in the form of decimal values, - represented in Python by decimal.Decimal. - """ - - def to_python(self, value): - try: - return decimal.Decimal(value) - except decimal.InvalidOperation as e: - raise ValueError(str(e)) - - -class DateColumn(Column): - """ - A column that contains data in the form of dates, - represented in Python by datetime.date. - - format - A strptime()-style format string. - See http://docs.python.org/library/datetime.html for details - """ - - def __init__(self, *args, format='%Y-%m-%d', **kwargs): - super(DateColumn, self).__init__(*args, **kwargs) - self.format = format - - def to_python(self, value): - """ - Parse a string value according to self.format - and return only the date portion. - """ - if isinstance(value, datetime.date): - return value - return datetime.datetime.strptime(value, self.format).date() - - def to_string(self, value): - """ - Format a date according to self.format and return that as a string. - """ - return value.strftime(self.format) - +import functools +import datetime +import decimal + +from itertools import count + + +class Column(object): + """An individual column within a CSV file. + This serves as a base for attributes and methods that are common to all + types of columns. Subclasses of Column will define behavior for more + specific data types. + """ + + _count = count() # global counter to maintain attr order this can be + # removed in python 3.0 with metaclass.__prepare__. + + def __init__(self, title=None, required=True): + self.title = title + self.required = required + self._validators = [self.to_python] + + # Hack to maintain class attribute order in Python < 3.0 + self.counter = next(self.__class__._count) + + def attach_to_class(self, cls, name, dialect): + self.cls = cls + self.name = name + self.dialect = dialect + if self.title is None: + # Check for None so that an empty string will skip this behavior + self.title = name.replace('_', ' ') + dialect.add_column(self) + + def to_python(self, value): + """ + Convert the given string to a native Python object. + """ + return value + + def to_string(self, value): + """ + Convert the given Python object to a string. + """ + return value + + # Not yet written about + + def validator(self, func): + self._validators.append(functools.partial(func, self)) + return func + + def validate(self, value): + """ + Validate that the given value matches the column's requirements. + Raise a ValueError only if the given value was invalid. + """ + for validate in self._validators: + validate(value) + + +class StringColumn(Column): + pass + + +class IntegerColumn(Column): + def to_python(self, value): + return int(value) + + +class FloatColumn(Column): + def to_python(self, value): + return float(value) + + +class DecimalColumn(Column): + """ + A column that contains data in the form of decimal values, + represented in Python by decimal.Decimal. + """ + + def to_python(self, value): + try: + return decimal.Decimal(value) + except decimal.InvalidOperation as e: + raise ValueError(str(e)) + + +class DateColumn(Column): + """ + A column that contains data in the form of dates, + represented in Python by datetime.date. + + format + A strptime()-style format string. + See http://docs.python.org/library/datetime.html for details + """ + + def __init__(self, format='%Y-%m-%d', *args, **kwargs): + super(DateColumn, self).__init__(*args, **kwargs) + self.format = format + + def to_python(self, value): + """ + Parse a string value according to self.format + and return only the date portion. + """ + if isinstance(value, datetime.date): + return value + return datetime.datetime.strptime(value, self.format).date() + + def to_string(self, value): + """ + Format a date according to self.format and return that as a string. + """ + return value.strftime(self.format) diff --git a/sheets/options.py b/sheets/options.py index cefe517..b083703 100644 --- a/sheets/options.py +++ b/sheets/options.py @@ -1,12 +1,12 @@ -class Dialect: - def __init__(self, has_header_row=False, **kwargs): - self.has_header_row = has_header_row - self.csv_dialect = kwargs - self.columns = [] - - def add_column(self, column): - self.columns.append(column) - - def finalize(self): - self.columns.sort(key=lambda column: column.counter) - +class Dialect: + def __init__(self, has_header_row=False, **kwargs): + self.has_header_row = has_header_row + self.csv_dialect = kwargs + self.columns = [] + + def add_column(self, column): + self.columns.append(column) + + def finalize(self): + self.columns.sort(key=lambda column: column.counter) + diff --git a/sheets/tests.py b/sheets/tests.py index 2879d42..418eeb5 100644 --- a/sheets/tests.py +++ b/sheets/tests.py @@ -1,22 +1,22 @@ -import unittest -times2 = lambda value: value * 2 - -class MultiplicationTests(unittest.TestCase): - - def setUp(self): - self.factor = 2 - - def testNumber(self): - self.assertEqual(times2(5), 42) - - def testString(self): - self.assertTrue(times2(5) == 10) - - def testTuple(self): - self.assertTrue(times2(5) == 10) - - -if __name__ == '__main__': - unittest.main() - - +import unittest +times2 = lambda value: value * 2 + +class MultiplicationTests(unittest.TestCase): + + def setUp(self): + self.factor = 2 + + def testNumber(self): + self.assertEqual(times2(5), 42) + + def testString(self): + self.assertTrue(times2(5) == 10) + + def testTuple(self): + self.assertTrue(times2(5) == 10) + + +if __name__ == '__main__': + unittest.main() + + diff --git a/tests/columns.py b/tests/columns.py index 032df05..fe48163 100644 --- a/tests/columns.py +++ b/tests/columns.py @@ -1,86 +1,86 @@ -import unittest -import datetime -import decimal - -import sheets - - -class ColumnTests(unittest.TestCase): - invalid_values = [] - - def setUp(self): - self.column = sheets.Column() - self.string_value = 'value' - self.python_value = 'value' - - def test_validation(self): - try: - self.column.validate(self.python_value) - except ValueError as e: - self.fail(str(e)) - - def test_python_conversion(self): - python_value = self.column.to_python(self.string_value) - self.assertEqual(python_value, self.python_value) - - def test_string_conversion(self): - string_value = str(self.column.to_string(self.python_value)) - self.assertEqual(string_value, self.string_value) - - def test_invalid_value(self): - for value in self.invalid_values: - try: - value = self.column.to_python(value) - except ValueError: - # If it's caught here, there's no need to test anything else - return - self.assertRaises(ValueError, self.column.validate, value) - - -class StringColumnTests(ColumnTests): - def setUp(self): - self.column = sheets.StringColumn() - self.string_value = 'value' - self.python_value = 'value' - - -class IntegerColumnTests(ColumnTests): - def setUp(self): - self.column = sheets.IntegerColumn() - self.string_value = '1' - self.python_value = 1 - self.invalid_values = ['invalid'] - - -class FloatColumnTests(ColumnTests): - def setUp(self): - self.column = sheets.FloatColumn() - self.string_value = '1.1' - self.python_value = 1.1 - self.invalid_values = ['invalid'] - - -class DecimalColumnTests(ColumnTests): - def setUp(self): - self.column = sheets.DecimalColumn() - self.string_value = '1.1' - self.python_value = decimal.Decimal('1.1') - self.invalid_values = ['invalid'] - - -class DateColumnTests(ColumnTests): - def setUp(self): - self.column = sheets.DateColumn() - self.string_value = '2010-03-31' - self.python_value = datetime.date(2010, 3, 31) - self.invalid_values = ['invalid', '03-31-2010'] - - -class FormattedDateColumnTests(ColumnTests): - def setUp(self): - self.column = sheets.DateColumn(format='%m/%d/%Y') - self.string_value = '03/31/2010' - self.python_value = datetime.date(2010, 3, 31) - self.invalid_values = ['invalid', '03-31-2010'] - - +import unittest +import datetime +import decimal + +import sheets + + +class ColumnTests(unittest.TestCase): + invalid_values = [] + + def setUp(self): + self.column = sheets.Column() + self.string_value = 'value' + self.python_value = 'value' + + def test_validation(self): + try: + self.column.validate(self.python_value) + except ValueError as e: + self.fail(str(e)) + + def test_python_conversion(self): + python_value = self.column.to_python(self.string_value) + self.assertEqual(python_value, self.python_value) + + def test_string_conversion(self): + string_value = str(self.column.to_string(self.python_value)) + self.assertEqual(string_value, self.string_value) + + def test_invalid_value(self): + for value in self.invalid_values: + try: + value = self.column.to_python(value) + except ValueError: + # If it's caught here, there's no need to test anything else + return + self.assertRaises(ValueError, self.column.validate, value) + + +class StringColumnTests(ColumnTests): + def setUp(self): + self.column = sheets.StringColumn() + self.string_value = 'value' + self.python_value = 'value' + + +class IntegerColumnTests(ColumnTests): + def setUp(self): + self.column = sheets.IntegerColumn() + self.string_value = '1' + self.python_value = 1 + self.invalid_values = ['invalid'] + + +class FloatColumnTests(ColumnTests): + def setUp(self): + self.column = sheets.FloatColumn() + self.string_value = '1.1' + self.python_value = 1.1 + self.invalid_values = ['invalid'] + + +class DecimalColumnTests(ColumnTests): + def setUp(self): + self.column = sheets.DecimalColumn() + self.string_value = '1.1' + self.python_value = decimal.Decimal('1.1') + self.invalid_values = ['invalid'] + + +class DateColumnTests(ColumnTests): + def setUp(self): + self.column = sheets.DateColumn() + self.string_value = '2010-03-31' + self.python_value = datetime.date(2010, 3, 31) + self.invalid_values = ['invalid', '03-31-2010'] + + +class FormattedDateColumnTests(ColumnTests): + def setUp(self): + self.column = sheets.DateColumn(format='%m/%d/%Y') + self.string_value = '03/31/2010' + self.python_value = datetime.date(2010, 3, 31) + self.invalid_values = ['invalid', '03-31-2010'] + +