From 8d62a00904572970934e685ad9ee24e060cd86fb Mon Sep 17 00:00:00 2001 From: calebsyring Date: Thu, 13 Jun 2024 17:44:03 -0400 Subject: [PATCH] Make ruff-compliant --- ruff.toml | 3 + src/tribune/__init__.py | 123 +++++---- src/tribune/sheet_import/__init__.py | 80 +++--- src/tribune/sheet_import/parsers.py | 41 +-- src/tribune/tests/conftest.py | 4 +- src/tribune/tests/entities.py | 55 ++-- src/tribune/tests/reports.py | 19 +- src/tribune/tests/test_objects.py | 178 ++++++++----- src/tribune/tests/test_sheet_import.py | 241 +++++++++++------- .../tests/test_sheet_import_parsers.py | 108 ++++---- src/tribune/tests/test_utils.py | 14 +- src/tribune/tests/utils.py | 48 ++-- src/tribune/utils.py | 15 +- 13 files changed, 546 insertions(+), 383 deletions(-) diff --git a/ruff.toml b/ruff.toml index 83595bc..bfe4016 100644 --- a/ruff.toml +++ b/ruff.toml @@ -50,6 +50,9 @@ select = [ ignore = [ 'A003', # Class attribute is shadowing a Python builtin 'E731', # Do not assign a `lambda` expression, use a `def` + # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + 'COM812', + 'ISC001', ] # [per-file-ignores] diff --git a/src/tribune/__init__.py b/src/tribune/__init__.py index cb8611a..88a2a27 100644 --- a/src/tribune/__init__.py +++ b/src/tribune/__init__.py @@ -1,3 +1,4 @@ +from contextlib import suppress import copy from decimal import Decimal import sys @@ -6,16 +7,15 @@ from blazeutils.spreadsheets import WriterX from xlsxwriter.format import Format -from .utils import ( - column_letter, - deepcopy_tuple_except -) +from .utils import column_letter, deepcopy_tuple_except + # we do not want to make SA a dependency, just want to work with it when it is used try: from sqlalchemy.orm.attributes import QueryableAttribute except ImportError: - class QueryableAttribute(object): + + class QueryableAttribute: pass @@ -45,9 +45,9 @@ def _fetch_val(record, key): # (SA-col-1/string/int, SA-col-2/string/int, operator) val = key[2]( _fetch_val(record, key[0]), - _fetch_val(record, key[1]) + _fetch_val(record, key[1]), ) - elif not isinstance(key, (str, int, float, Decimal)): + elif not isinstance(key, str | int | float | Decimal): # must be a SA-col, find the key and hit the record datakey = getattr(key, 'key', getattr(key, 'name', None)) val = getattr(record, datakey) @@ -63,8 +63,8 @@ class ProgrammingError(Exception): class _SheetDeclarativeMeta(type): """ - Metaclass to define/create units on a sheet section - Borrowed in part from WebGrid's Grid setup + Metaclass to define/create units on a sheet section + Borrowed in part from WebGrid's Grid setup """ def __new__(cls, name, bases, class_dict): @@ -77,14 +77,14 @@ def __new__(cls, name, bases, class_dict): class_units.extend(class_dict.get('__cls_units__', ())) class_dict['__cls_units__'] = class_units - return super(_SheetDeclarativeMeta, cls).__new__(cls, name, bases, class_dict) + return super().__new__(cls, name, bases, class_dict) -class SheetUnit(object): +class SheetUnit: # Basic sheet object, can be a row or a column def __new__(cls, *args, **kwargs): - col_inst = super(SheetUnit, cls).__new__(cls) + col_inst = super().__new__(cls) if '_dont_assign' not in kwargs: col_inst._assign_to_section() return col_inst @@ -130,25 +130,33 @@ def __deepcopy__(self, memo): class SheetColumn(SheetUnit): """ - Basic column for a spreadsheet report. Holds the SQLAlchemy - column and has methods for rendering various kinds of rows + Basic column for a spreadsheet report. Holds the SQLAlchemy + column and has methods for rendering various kinds of rows """ + def new_instance(self, sheet): column = SheetUnit.new_instance(self, sheet) column._construct_header_data(self._init_header) return column - def __init__(self, key=None, sheet=None, write_header_func=None, write_data_func=None, - write_total_func=None, **kwargs): + def __init__( + self, + key=None, + sheet=None, + write_header_func=None, + write_data_func=None, + write_total_func=None, + **kwargs, + ): """ - header_# in kwargs used to override default (blank) heading values - write_*_func can override the class methods + header_# in kwargs used to override default (blank) heading values + write_*_func can override the class methods """ # key can be one of many types: str, Decimal, int, float, SA column, # or a tuple of the above (with an operator) self.expr = None - if key is not None and not isinstance(key, (str, tuple, Decimal, int, float)): + if key is not None and not isinstance(key, str | tuple | Decimal | int | float): self.expr = col = key # use column.key, column.name, or None in that order key = getattr(col, 'key', getattr(col, 'name', None)) @@ -162,21 +170,19 @@ def __init__(self, key=None, sheet=None, write_header_func=None, write_data_func self.write_total = write_total_func or self.write_total # look for header values in kwargs, construct a header dict - self._init_header = dict() + self._init_header = {} for k, v in kwargs.items(): if k.startswith('header_'): - try: + with suppress(ValueError): self._init_header[int(k[7:])] = v - except ValueError: - pass def _construct_header_data(self, init_header): d = ['' for i in range(self.sheet.pre_data_rows)] for k, v in init_header.items(): try: d[k] = v - except IndexError: - raise ProgrammingError('not enough pre-data rows on sheet') + except IndexError as e: + raise ProgrammingError('not enough pre-data rows on sheet') from e self.header_data = d def xls_width_calc(self, value): @@ -195,7 +201,7 @@ def register_col_width(self, value): return self.xls_computed_width = max( self.xls_computed_width, - self.xls_width_calc(value) + self.xls_width_calc(value), ) def adjust_col_width(self): @@ -244,11 +250,12 @@ def write_total(self): class SheetPortraitColumn(SheetColumn): """ - Column for ReportPortraitSheet. Not tied to a specific data field - (as that will change from row to row), but tracks column width needed + Column for ReportPortraitSheet. Not tied to a specific data field + (as that will change from row to row), but tracks column width needed """ + def __init__(self, record=None, *args, **kwargs): - super(SheetPortraitColumn, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.xls_computed_width = 0 self.record = record @@ -271,8 +278,8 @@ def __init__(self, label=None, key=None, **kwargs): if label: label_rows = label.split('\n') for i_row, label_string in enumerate(label_rows): - kwargs['header_{0}'.format(self.header_start_row + i_row)] = label_string - super(LabeledColumn, self).__init__(key=key, **kwargs) + kwargs[f'header_{self.header_start_row + i_row}'] = label_string + super().__init__(key=key, **kwargs) class PortraitRow(SheetUnit): @@ -313,14 +320,18 @@ def render_func(row): def render(self): raise NotImplementedError - def update_format(self, label={}, value={}): + def update_format(self, label=None, value=None): + if label is None: + label = {} + if value is None: + value = {} self.column_format['label'].update(label) self.column_format['value'].update(value) -class TotaledMixin(object): +class TotaledMixin: def write_total(self): - sum_begin = self.sheet.pre_data_rows+1 + sum_begin = self.sheet.pre_data_rows + 1 sum_end = self.sheet.rownum if sum_begin > sum_end: # no data rows @@ -330,20 +341,21 @@ def write_total(self): '=SUM({0}{1}:{0}{2})'.format( column_letter(self.sheet.colnum), sum_begin, - sum_end + sum_end, ), - self.format_total() + self.format_total(), ) class SheetSection(SheetUnit): """ - Groups SheetColumns together. May override heading render for - individual columns (e.g. need a merged cell to head the section) + Groups SheetColumns together. May override heading render for + individual columns (e.g. need a merged cell to head the section) - If a sheet references a section, it will not reference the individual - columns directly but depend on the section to police its own + If a sheet references a section, it will not reference the individual + columns directly but depend on the section to police its own """ + __metaclass__ = _SheetDeclarativeMeta __cls_units__ = () @@ -387,13 +399,11 @@ def __init__(self, parent_book, worksheet=None, **kwargs): self.parent_book = parent_book if not worksheet: worksheet = parent_book.add_worksheet(self.sheet_name) - super(ReportSheet, self).__init__(ws=worksheet) + super().__init__(ws=worksheet) self.set_base_style_dicts() # fetch records first, as units may need some data for initialization - filter_args = dict([ - (k[7:], v) for k, v in kwargs.items() if k[:7] == 'filter_' - ]) + filter_args = {k[7:]: v for k, v in kwargs.items() if k[:7] == 'filter_'} self.records = self.fetch_records(**filter_args) # list of (row,col) tuples to guard when writing. Idea here is to have @@ -445,8 +455,9 @@ def combine_style_dicts(self, *args): # takes dicts such as those defined in set_base_styles, combines # left-to-right, returns dict def add_dicts(a, b): - return dict(list(a.items()) + list(b.items()) + - [(k, a[k] + b[k]) for k in set(b) & set(a)]) + return dict( + list(a.items()) + list(b.items()) + [(k, a[k] + b[k]) for k in set(b) & set(a)], + ) style = args[0] if len(args) == 1: @@ -465,7 +476,7 @@ def freeze_pane(self, col, row): def write_simple_merge(self, num_cols, data, style=None): """shorthand for WriterX.write_merge, for merge on single row - data: can be a literal value, or a tuple of (record, key) which will do the fetch_val + data: can be a literal value, or a tuple of (record, key) which will do the fetch_val """ data_to_write = data if isinstance(data, tuple): @@ -475,8 +486,14 @@ def write_simple_merge(self, num_cols, data, style=None): if num_cols == 0: raise Exception('Cannot write data length 0') if num_cols > 1: - self.ws.merge_range(self.rownum, self.colnum, self.rownum, self.colnum + num_cols - 1, - data_to_write, self.conform_style(style)) + self.ws.merge_range( + self.rownum, + self.colnum, + self.rownum, + self.colnum + num_cols - 1, + data_to_write, + self.conform_style(style), + ) else: # number of cells is one, so no merge needed self.write(self.rownum, self.colnum, data_to_write, self.conform_style(style)) @@ -504,7 +521,7 @@ def conform_style(self, style): def write(self, row, col, data, style=None): """wraps WriterX.write, checks row,col tuple against guarded cells - data: can be a literal value, or a tuple of (record, key) which will do the fetch_val + data: can be a literal value, or a tuple of (record, key) which will do the fetch_val """ data_to_write = data if isinstance(data, tuple): @@ -515,10 +532,10 @@ def write(self, row, col, data, style=None): self.ws.write(row, col, data_to_write, self.conform_style(style)) def awrite(self, data, style=None, nextrow=False): - super(ReportSheet, self).awrite(data, style=self.conform_style(style), nextrow=nextrow) + super().awrite(data, style=self.conform_style(style), nextrow=nextrow) def render_header(self): - for i in range(self.pre_data_rows): + for _ in range(self.pre_data_rows): for u in self.units: u.write_header() self.nextrow() @@ -550,7 +567,7 @@ class ReportPortraitSheet(ReportSheet): def __init__(self, *args, **kwargs): kwargs['auto_render'] = False - super(ReportPortraitSheet, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.columns = self.init_columns() diff --git a/src/tribune/sheet_import/__init__.py b/src/tribune/sheet_import/__init__.py index ae12b04..028e76d 100644 --- a/src/tribune/sheet_import/__init__.py +++ b/src/tribune/sheet_import/__init__.py @@ -22,16 +22,18 @@ class MySheet(SheetImporter): ]) records = MySheet().get_sheet_records(sheet) """ + import abc -import warnings from collections import namedtuple from io import BytesIO -from typing import BinaryIO, Union +from typing import BinaryIO +import warnings from zipfile import BadZipFile from blazeutils.spreadsheets import xlsx_to_reader, xlsx_to_strio from xlsxwriter import Workbook + try: import openpyxl from openpyxl.utils.exceptions import InvalidFileException @@ -59,13 +61,14 @@ def __init__(self, errors): self.errors = errors def __str__(self): - return ''.format(self.errors) + return f'' def xlrd_workbook_safe_open(file_handle): warnings.warn( - "xlrd support is deprecated and will be removed in a future version", - DeprecationWarning + 'xlrd support is deprecated and will be removed in a future version', + DeprecationWarning, + stacklevel=2, ) try: @@ -74,25 +77,30 @@ def xlrd_workbook_safe_open(file_handle): msg = str(e) # we don't want to screen all xlrd load errors, but a few common ones need to be trapped # for user-friendliness - if 'unsupported format' in msg.lower() or 'not supported' in msg.lower() \ - or 'not a known type of workbook' in str(e).lower(): - raise SpreadsheetImportError(['File format is not recognized. Please upload an Excel' - ' file (.xlsx or .xls).']) + if ( + 'unsupported format' in msg.lower() + or 'not supported' in msg.lower() + or 'not a known type of workbook' in str(e).lower() + ): + raise SpreadsheetImportError( + ['File format is not recognized. Please upload an Excel file (.xlsx or .xls).'], + ) from e elif 'file size is 0 bytes' in msg.lower(): - raise SpreadsheetImportError(['File is empty.']) + raise SpreadsheetImportError(['File is empty.']) from e else: - raise SpreadsheetImportError([msg]) + raise SpreadsheetImportError([msg]) from e -def workbook_safe_open(filename_or_filelike: Union[str, BinaryIO]): +def workbook_safe_open(filename_or_filelike: str | BinaryIO): if not openpyxl: return xlrd_workbook_safe_open(filename_or_filelike) try: return openpyxl.load_workbook(filename_or_filelike, data_only=True) - except (BadZipFile, InvalidFileException): - raise SpreadsheetImportError(['File format is not recognized. Please upload an Excel' - ' file (.xlsx).']) + except (BadZipFile, InvalidFileException) as e: + raise SpreadsheetImportError( + ['File format is not recognized. Please upload an Excel file (.xlsx).'], + ) from e def normalize_text(x): @@ -100,8 +108,9 @@ def normalize_text(x): return str(x).strip().lower() -class Cell(object): +class Cell: """Represents a particular cell on a spreadsheet.""" + is_zero_based = False def __init__(self, row, column): @@ -114,20 +123,21 @@ def __init__(self, row, column): def __str__(self): # If we're using openpyxl, column indices are one-based - return ( - column_letter(self.column, self.is_zero_based) - + str(self.row + (1 if self.is_zero_based else 0)) + return column_letter(self.column, self.is_zero_based) + str( + self.row + (1 if self.is_zero_based else 0), ) class XlrdCell(Cell): """Represents a single cell when reading sheets with xlrd, which uses 0-based indices.""" + is_zero_based = True class SheetDataBase(metaclass=abc.ABCMeta): """An ABC that describes the minimum necessary API for importing a sheet.""" + @abc.abstractmethod def __len__(self): raise NotImplementedError() @@ -173,8 +183,9 @@ def from_nested_iters(cls, data): class XlrdSheetData(SheetDataBase): def __init__(self, sheet): warnings.warn( - "xlrd support is deprecated and will be removed in a future version", - DeprecationWarning + 'xlrd support is deprecated and will be removed in a future version', + DeprecationWarning, + stacklevel=2, ) self.sheet = sheet @@ -242,13 +253,14 @@ def aggregate_error_or_value(error_or_values): Field = namedtuple('Field', ['label', 'field', 'parser']) -class SheetImporter(object): +class SheetImporter: """A base class for defining how to parse a single spreadsheet.""" + data_start_row = 2 # openpyxl indices are 1-based data_start_col = 1 is_zero_based = False cell_cls = Cell - fields = () # Ordered iterable of Field tuples + fields = () # Ordered iterable of Field tuples def __init__(self, fields=None, data_start_row=None): """ @@ -265,10 +277,8 @@ def _get_header_errors(self, sheet): no errors.""" header = self.data_start_row - 1 return [ - 'Expected "{header}" in header cell {cell}.'.format( - header=field.label, - cell=self.cell_cls(header, col) - ) for col, field in enumerate(self.fields, self.data_start_col) + f'Expected "{field.label}" in header cell {self.cell_cls(header, col)}.' + for col, field in enumerate(self.fields, self.data_start_col) if normalize_text(sheet.cell_value(self.cell_cls(header, col))) != normalize_text(field.label) ] @@ -284,10 +294,7 @@ def _parse_cell(field, cell, value): try: return ErrorOrValue([], field.parser(value)) except SpreadsheetImportError as e: - contextualized_errors = [ - 'Unexpected value in cell {cell}: {msg}'.format(cell=cell, msg=msg) - for msg in e.errors - ] + contextualized_errors = [f'Unexpected value in cell {cell}: {msg}' for msg in e.errors] return ErrorOrValue(contextualized_errors, None) def _parse_row(self, sheet, row): @@ -302,8 +309,9 @@ def _parse_row(self, sheet, row): for col, field in enumerate(self.fields, self.data_start_col) ] errors, values = aggregate_error_or_value(col_data) - return ErrorOrValue(errors, None) if errors \ - else ErrorOrValue([], self.row_to_record(values)) + return ( + ErrorOrValue(errors, None) if errors else ErrorOrValue([], self.row_to_record(values)) + ) def _parse_sheet_data(self, sheet): """Parses only the data rows from a sheet and returns aggregated errors or valuse as an @@ -327,7 +335,9 @@ def _parse_sheet(self, sheet): def row_to_record(self, values): """Converts a list of values from a row into a dictionary where each value is keyed on its column name from `self.fields`.""" - return self.modify_record({field.field: v for field, v in zip(self.fields, values)}) + return self.modify_record( + {field.field: v for field, v in zip(self.fields, values, strict=True)}, + ) def modify_record(self, record): """Override to add/remove/change fields in the record (dictionary of column to value). @@ -352,6 +362,7 @@ def get_sheet_records_batched(self, sheet, batch_size): def as_report_sheet(self): """Returns a ReportSheet class which mirrors this SheetImporter.""" + class NewReportSheet(tribune.ReportSheet): pre_data_rows = self.data_start_row @@ -366,6 +377,7 @@ def fetch_records(self): class XlrdSheetImporter(SheetImporter): """A base class for defining how to parse a single spreadsheet.""" + data_start_row = 1 # xlrd rows are 0-based data_start_col = 0 cell_cls = XlrdCell diff --git a/src/tribune/sheet_import/parsers.py b/src/tribune/sheet_import/parsers.py index 3f80cc5..7b9dbc4 100644 --- a/src/tribune/sheet_import/parsers.py +++ b/src/tribune/sheet_import/parsers.py @@ -30,10 +30,10 @@ def parse_yes_no(value): """Parses boolean values represented as 'yes' or 'no' (case insensitive).""" result = { normalize_text('yes'): True, - normalize_text('no'): False + normalize_text('no'): False, }.get(normalize_text(value)) if result is None: - raise SpreadsheetImportError([u'must be "yes" or "no"']) + raise SpreadsheetImportError(['must be "yes" or "no"']) return result @@ -45,8 +45,8 @@ def parse_int(value): as_int = int(as_float) # compare float and integer versions because, e.g. 3.0 == 3 but 3.1 != 3. return as_int if as_int == as_float else raises(ValueError()) - except (ValueError, TypeError): - raise SpreadsheetImportError([u'must be an integer']) + except (ValueError, TypeError) as e: + raise SpreadsheetImportError(['must be an integer']) from e def parse_int_as_text(value): @@ -58,8 +58,8 @@ def parse_number(value): """Parses any floating point number.""" try: return float(value) - except (ValueError, TypeError): - raise SpreadsheetImportError([u'must be a number']) + except (ValueError, TypeError) as e: + raise SpreadsheetImportError(['must be a number']) from e def parse_int_or_text(value): @@ -85,31 +85,31 @@ def nullable(parser): def validate_satisfies(pred, error_message): """Validates that a value satisfies the given predicate or issues the given error if it doesn't.""" - return lambda value: (value if pred(value) - else raises(SpreadsheetImportError([error_message]))) + return lambda value: (value if pred(value) else raises(SpreadsheetImportError([error_message]))) def validate_max_length(maximum): """Validates that the value has at most ``maximum`` digits.""" - return validate_satisfies(lambda v: len(v) <= maximum, - u'must be no more than {} characters long'.format(maximum)) + return validate_satisfies( + lambda v: len(v) <= maximum, + f'must be no more than {maximum} characters long', + ) validate_not_empty = chain( default_to(None), - validate_satisfies(lambda v: v is not None, u'must not be empty') + validate_satisfies(lambda v: v is not None, 'must not be empty'), ) def validate_min(min_): """Validates that a number is equal to or grater than the given minimum.""" - return validate_satisfies(lambda v: v >= min_, u'number must be no less than {}'.format(min_)) + return validate_satisfies(lambda v: v >= min_, f'number must be no less than {min_}') def validate_max(max_): """Validates that a number is equal to or less than the given maximum.""" - return validate_satisfies(lambda v: v <= max_, - u'number must be no greater than {}'.format(max_)) + return validate_satisfies(lambda v: v <= max_, f'number must be no greater than {max_}') def validate_range(min_, max_): @@ -127,9 +127,9 @@ def validate_unique(others, thing='thing'): def parser(value): if value in others_set: - raise SpreadsheetImportError([ - u'{thing} "{value}" already exists'.format(thing=thing, value=value)]) + raise SpreadsheetImportError([f'{thing} "{value}" already exists']) return value + return parser @@ -141,9 +141,9 @@ def validate_one_of(choices, thing='thing', show_choices_in_error=False): :param show_choices_in_error: determines if all the choices should be rendered in the error message. """ - error_message = u'must be a valid {thing}{choices}'.format( + error_message = 'must be a valid {thing}{choices}'.format( thing=thing, - choices=': ' + ', '.join(map(str, choices)) if show_choices_in_error else '' + choices=': ' + ', '.join(map(str, choices)) if show_choices_in_error else '', ) choices_set = frozenset(choices) return validate_satisfies(lambda v: v in choices_set, error_message) @@ -173,10 +173,11 @@ def parse_lookup(mapping, thing='thing'): `dict`. :param thing: is the name of the resulting type for error messages. """ + def parser(value): try: return mapping[value] - except KeyError: - raise SpreadsheetImportError([u'"{}" is not a valid {}'.format(value, thing)]) + except KeyError as e: + raise SpreadsheetImportError([f'"{value}" is not a valid {thing}']) from e return parser diff --git a/src/tribune/tests/conftest.py b/src/tribune/tests/conftest.py index 6b7fa23..205cb29 100644 --- a/src/tribune/tests/conftest.py +++ b/src/tribune/tests/conftest.py @@ -3,11 +3,13 @@ def pytest_configure(config): from sqlalchemy import engine_from_config - from sqlalchemy.orm import sessionmaker, scoped_session + from sqlalchemy.orm import scoped_session, sessionmaker + engine = engine_from_config({'url': 'sqlite:///'}, prefix='') session = scoped_session(sessionmaker(bind=engine)) from tribune.tests import entities + entities.meta.bind = engine entities.meta.create_all(engine) entities.session = session diff --git a/src/tribune/tests/entities.py b/src/tribune/tests/entities.py index 1264c99..8f4c2b2 100644 --- a/src/tribune/tests/entities.py +++ b/src/tribune/tests/entities.py @@ -1,27 +1,28 @@ -from blazeutils.strings import randchars -import sqlalchemy as sa -from sqlalchemy.orm import declarative_base - -from .utils import DefaultMixin - -meta = sa.MetaData() -session = None -Base = declarative_base(metadata=meta) - - -class Person(Base, DefaultMixin): - __tablename__ = 'persons' - - id = sa.Column(sa.Integer, primary_key=True) - firstname = sa.Column(sa.String(50)) - lastname = sa.Column('last_name', sa.String(50)) - intcol = sa.Column(sa.Integer) - floatcol = sa.Column(sa.Float) - - def __repr__(self): - return '' % (self.id, self.createdts) - - @classmethod - def testing_create(cls, **kwargs): - kwargs['firstname'] = kwargs.get('firstname') or randchars() - return cls.add(**kwargs) +from blazeutils.strings import randchars +import sqlalchemy as sa +from sqlalchemy.orm import declarative_base + +from .utils import DefaultMixin + + +meta = sa.MetaData() +session = None +Base = declarative_base(metadata=meta) + + +class Person(Base, DefaultMixin): + __tablename__ = 'persons' + + id = sa.Column(sa.Integer, primary_key=True) + firstname = sa.Column(sa.String(50)) + lastname = sa.Column('last_name', sa.String(50)) + intcol = sa.Column(sa.Integer) + floatcol = sa.Column(sa.Float) + + def __repr__(self): + return f'' + + @classmethod + def testing_create(cls, **kwargs): + kwargs['firstname'] = kwargs.get('firstname') or randchars() + return cls.add(**kwargs) diff --git a/src/tribune/tests/reports.py b/src/tribune/tests/reports.py index bb44b82..d45fc1b 100644 --- a/src/tribune/tests/reports.py +++ b/src/tribune/tests/reports.py @@ -13,6 +13,7 @@ TotaledMixin, ) + """ Objects for testing normal landscape-layout reports """ @@ -67,9 +68,17 @@ def write_sheet_header(self): class DealershipReportRow(PortraitRow): - def __init__(self, label=None, comp=None, inc=None, exp=None, net=None, - comp_curr=False, **kwargs): - super(DealershipReportRow, self).__init__() + def __init__( + self, + label=None, + comp=None, + inc=None, + exp=None, + net=None, + comp_curr=False, + **kwargs, + ): + super().__init__() self.label = label self.comp = comp self.comp_curr = comp_curr @@ -111,13 +120,13 @@ def format_label(self): return self.sheet.combine_style_dicts( self.sheet.style_header, self.sheet.style_right, - {'top': 6} + {'top': 6}, ) def format_component(self): return self.sheet.combine_style_dicts( self.sheet.style_currency, - {'top': 6} + {'top': 6}, ) def format_income(self): diff --git a/src/tribune/tests/test_objects.py b/src/tribune/tests/test_objects.py index 5bf6810..4c56a9b 100644 --- a/src/tribune/tests/test_objects.py +++ b/src/tribune/tests/test_objects.py @@ -1,12 +1,12 @@ -import operator from io import BytesIO +import operator from unittest import mock from blazeutils.datastructures import BlankObject from blazeutils.spreadsheets import xlsx_to_reader import pytest -from xlsxwriter import Workbook import wrapt +from xlsxwriter import Workbook from tribune import ( LabeledColumn, @@ -17,32 +17,59 @@ SheetSection, SheetUnit, ) + from .entities import Person -from .reports import CarSheet, CarDealerSheet +from .reports import CarDealerSheet, CarSheet from .utils import find_sheet_col, find_sheet_row def car_data(self): return [ - {'year': 1998, 'make': 'Ford', 'model': 'Taurus', 'style': 'SE Wagon', - 'color': 'silver', 'book_value': 1500}, - {'year': 2004, 'make': 'Oldsmobile', 'model': 'Alero', 'style': '4D Sedan', - 'color': 'silver', 'book_value': 4500}, - {'year': 2003, 'make': 'Ford', 'model': 'F-150', 'style': 'XL Supercab', - 'color': 'blue', 'book_value': 8500}, + { + 'year': 1998, + 'make': 'Ford', + 'model': 'Taurus', + 'style': 'SE Wagon', + 'color': 'silver', + 'book_value': 1500, + }, + { + 'year': 2004, + 'make': 'Oldsmobile', + 'model': 'Alero', + 'style': '4D Sedan', + 'color': 'silver', + 'book_value': 4500, + }, + { + 'year': 2003, + 'make': 'Ford', + 'model': 'F-150', + 'style': 'XL Supercab', + 'color': 'blue', + 'book_value': 8500, + }, ] def dealer_data(self): return [ - {'sale_count': 5, 'sale_income': 75000, 'sale_expense': 25000, - 'rental_count': 46, 'rental_income': 8600, 'rental_expense': 2900, - 'lease_count': 27, 'lease_income': 10548, 'lease_expense': 3680, - 'total_net': 100000}, + { + 'sale_count': 5, + 'sale_income': 75000, + 'sale_expense': 25000, + 'rental_count': 46, + 'rental_income': 8600, + 'rental_expense': 2900, + 'lease_count': 27, + 'lease_income': 10548, + 'lease_expense': 3680, + 'total_net': 100000, + }, ] -class TestSheetDecl(object): +class TestSheetDecl: @mock.patch('tribune.tests.reports.CarSheet.fetch_records', car_data) def test_sheet_units(self): sheet = CarSheet(Workbook(BytesIO())) @@ -100,12 +127,13 @@ def test_deep_copy_unbound_method(self): """This test is kind of hard to set up, so it needs some explanation. A great deal of function objects are fine with the deep copy, but this particular case is not. What we have is an unbound method (status_render) wrapped in a decorator.""" + def status_render(row): pass @wrapt.decorator def some_decorator(wrapped, instance, args, kwargs): - row, = args + (row,) = args return wrapped(row) class DummyUnit(SheetUnit): @@ -119,9 +147,10 @@ def test_deep_copy_static_method(self): """This test is kind of hard to set up, so it needs some explanation. A great deal of function objects are fine with the deep copy, but this particular case is not. What we have is an static method (status_render) wrapped in a decorator.""" + @wrapt.decorator def some_decorator(wrapped, instance, args, kwargs): - row, = args + (row,) = args return wrapped(row) class DummyUnit(SheetUnit): @@ -136,20 +165,23 @@ def __init__(self): base_unit.new_instance(None) -class TestSheetColumn(object): +class TestSheetColumn: def test_extract_data_no_key(self): col = SheetColumn() assert col.extract_data({}) == '' def test_extract_data_attr(self): col = SheetColumn('bar') - assert col.extract_data( - BlankObject( - foo=1, - bar='something', - baz='else', + assert ( + col.extract_data( + BlankObject( + foo=1, + bar='something', + baz='else', + ), ) - ) == 'something' + == 'something' + ) def test_extract_data_dict(self): col = SheetColumn('bar') @@ -165,7 +197,7 @@ def test_extract_data_sacol(self): assert col.extract_data(person) == person.firstname def test_extract_data_sacol_altcolname(self): - person = Person.testing_create(lastname=u'foo') + person = Person.testing_create(lastname='foo') col = SheetColumn(Person.lastname) assert col.extract_data(person) == 'foo' @@ -217,7 +249,7 @@ def test_width(self): assert col.xls_computed_width == 14 -class TestLabeledColumn(object): +class TestLabeledColumn: def test_header_construction(self): col = LabeledColumn('foo\nbar', 'key') assert col._init_header == {0: 'foo', 1: 'bar'} @@ -230,7 +262,7 @@ class _LabeledColumn(LabeledColumn): assert col._init_header == {4: 'foo', 5: 'bar'} -class TestOutput(object): +class TestOutput: @mock.patch('tribune.tests.reports.CarSheet.fetch_records', car_data) def generate_report(self, reportcls=CarSheet): wb = Workbook(BytesIO()) @@ -243,29 +275,35 @@ def test_header(self): ws = self.generate_report() assert ws.cell_value(0, 0) == 'Cars' - @pytest.mark.parametrize('row,column,value', [ - (2, 0, ''), - (2, 1, 'Year'), - (2, 2, 'Make'), - (2, 3, 'Model'), - (2, 4, 'Style'), - (2, 5, 'Color'), - (2, 6, 'Blue'), - (3, 6, 'Book'), - ]) + @pytest.mark.parametrize( + 'row,column,value', + [ + (2, 0, ''), + (2, 1, 'Year'), + (2, 2, 'Make'), + (2, 3, 'Model'), + (2, 4, 'Style'), + (2, 5, 'Color'), + (2, 6, 'Blue'), + (3, 6, 'Book'), + ], + ) def test_column_headings(self, row, column, value): ws = self.generate_report() assert ws.cell_value(row, column) == value @pytest.mark.parametrize('data_row, data', enumerate(car_data(None))) - @pytest.mark.parametrize('header,key', [ - ('Year', 'year'), - ('Make', 'make'), - ('Model', 'model'), - ('Style', 'style'), - ('Color', 'color'), - ('Blue', 'book_value'), - ]) + @pytest.mark.parametrize( + 'header,key', + [ + ('Year', 'year'), + ('Make', 'make'), + ('Model', 'model'), + ('Style', 'style'), + ('Color', 'color'), + ('Blue', 'book_value'), + ], + ) def test_column_data(self, header, key, data_row, data): ws = self.generate_report() row = data_row + 4 @@ -313,7 +351,7 @@ def fetch_records(self): assert ws.cell_value(0, 0) == 'data' -class TestPortraitOutput(object): +class TestPortraitOutput: @mock.patch('tribune.tests.reports.CarDealerSheet.fetch_records', dealer_data) def generate_report(self): wb = Workbook(BytesIO()) @@ -326,32 +364,38 @@ def test_header(self): ws = self.generate_report() assert ws.cell_value(0, 0) == 'Dealership' - @pytest.mark.parametrize('row,column,value', [ - (3, 0, ''), - (3, 1, 'Count'), - (3, 2, 'Income'), - (3, 3, 'Expense'), - (3, 4, 'Net'), - ]) + @pytest.mark.parametrize( + 'row,column,value', + [ + (3, 0, ''), + (3, 1, 'Count'), + (3, 2, 'Income'), + (3, 3, 'Expense'), + (3, 4, 'Net'), + ], + ) def test_column_headings(self, row, column, value): ws = self.generate_report() assert ws.cell_value(row, column) == value - @pytest.mark.parametrize('row_header,column,value', [ - ('Sales', 1, 5), - ('Sales', 2, 75000), - ('Sales', 3, 25000), - ('Sales', 4, 50000), - ('Rentals', 1, 46), - ('Rentals', 2, 8600), - ('Rentals', 3, 2900), - ('Rentals', 4, 5700), - ('Leases', 1, 27), - ('Leases', 2, 10548), - ('Leases', 3, 3680), - ('Leases', 4, 6868), - ('Totals', 4, 100000), - ]) + @pytest.mark.parametrize( + 'row_header,column,value', + [ + ('Sales', 1, 5), + ('Sales', 2, 75000), + ('Sales', 3, 25000), + ('Sales', 4, 50000), + ('Rentals', 1, 46), + ('Rentals', 2, 8600), + ('Rentals', 3, 2900), + ('Rentals', 4, 5700), + ('Leases', 1, 27), + ('Leases', 2, 10548), + ('Leases', 3, 3680), + ('Leases', 4, 6868), + ('Totals', 4, 100000), + ], + ) def test_row_data(self, row_header, column, value): ws = self.generate_report() row = find_sheet_row(ws, row_header, 0) diff --git a/src/tribune/tests/test_sheet_import.py b/src/tribune/tests/test_sheet_import.py index c7ee416..b385ed6 100644 --- a/src/tribune/tests/test_sheet_import.py +++ b/src/tribune/tests/test_sheet_import.py @@ -1,5 +1,6 @@ import tribune.sheet_import as si import tribune.sheet_import.parsers as p + from .utils import assert_import_errors @@ -22,7 +23,7 @@ def parse_records_with(importer): return lambda data: list(importer.get_sheet_records(sd_cls.from_nested_iters(data))) -class TestXlrdSheetImport(object): +class TestXlrdSheetImport: def test_sheet_import_no_fields(self): parse_records = parse_records_with(si.XlrdSheetImporter([])) @@ -31,38 +32,50 @@ def test_sheet_import_no_fields(self): assert parse_records([['data']]) == [] def test_sheet_import_simple(self): - parse_records = parse_records_with(si.XlrdSheetImporter([ - si.Field('Name', 'name', p.parse_text), - ])) + parse_records = parse_records_with( + si.XlrdSheetImporter( + [ + si.Field('Name', 'name', p.parse_text), + ], + ), + ) assert parse_records([['Name']]) == [] # no data assert parse_records([['name']]) == [] # different header format assert parse_records([[' name ']]) == [] # different header format assert parse_records([['Name'], ['Ed']]) == [{'name': 'Ed'}] # one row, one column assert parse_records([['Name'], ['Ed', '']]) == [{'name': 'Ed'}] # one row, extra column - assert parse_records([['Name'], ['Ed'], ['Joe']]) \ - == [{'name': 'Ed'}, {'name': 'Joe'}] # two rows + assert parse_records([['Name'], ['Ed'], ['Joe']]) == [ + {'name': 'Ed'}, + {'name': 'Joe'}, + ] # two rows # invalid header invalid_headers = ( - [], # No data + [], # No data [['Some other name']], # Wrong header name ) for case in invalid_headers: - assert_import_errors({'Expected "Name" in header cell A1.'}, - lambda: parse_records(case)) + assert_import_errors( + {'Expected "Name" in header cell A1.'}, + lambda case=case: parse_records(case), + ) def test_sheet_import_data_validation(self): - parse_records = parse_records_with(si.XlrdSheetImporter([ - si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)), - si.Field('Age', 'age', p.parse_int), - ])) + parse_records = parse_records_with( + si.XlrdSheetImporter( + [ + si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)), + si.Field('Age', 'age', p.parse_int), + ], + ), + ) assert parse_records([['Name', 'Age']]) == [] assert parse_records([['Name', 'Age'], ['Ed', '27']]) == [{'name': 'Ed', 'age': 27}] ed_n_joe = [ - {'name': 'Ed', 'age': 27}, + {'name': 'Ed', 'age': 27}, {'name': 'Joe', 'age': 32}, ] @@ -75,37 +88,43 @@ def test_sheet_import_data_validation(self): # invalid header and/or data assert_import_errors( {'Expected "Age" in header cell B1.'}, - lambda: parse_records([['Name', 'SSN']]) + lambda: parse_records([['Name', 'SSN']]), ) assert_import_errors( {'Expected "Age" in header cell B1.'}, # header errors beat data errors - lambda: parse_records([['Name', 'SSN'], ['', '2']]) + lambda: parse_records([['Name', 'SSN'], ['', '2']]), ) assert_import_errors( - {'Unexpected value in cell A2: must not be empty', - 'Unexpected value in cell B2: must be an integer'}, - lambda: parse_records([['Name', 'Age'], ['', '3.9']]) + { + 'Unexpected value in cell A2: must not be empty', + 'Unexpected value in cell B2: must be an integer', + }, + lambda: parse_records([['Name', 'Age'], ['', '3.9']]), ) assert_import_errors( - {'Unexpected value in cell A3: must not be empty', - 'Unexpected value in cell B3: must be an integer'}, - lambda: parse_records([['Name', 'Age'], ['Ed', '27'], ['', '3.9']]) + { + 'Unexpected value in cell A3: must not be empty', + 'Unexpected value in cell B3: must be an integer', + }, + lambda: parse_records([['Name', 'Age'], ['Ed', '27'], ['', '3.9']]), ) def test_non_string_header(self): - parse_records = parse_records_with(si.XlrdSheetImporter([ - si.Field('Age', 'age', p.parse_int) - ])) + parse_records = parse_records_with( + si.XlrdSheetImporter( + [ + si.Field('Age', 'age', p.parse_int), + ], + ), + ) assert_import_errors( {'Expected "Age" in header cell A1.'}, - lambda: parse_records([[29]]) + lambda: parse_records([[29]]), ) def test_sheet_import_with_modify_record(self): class TestImport(si.XlrdSheetImporter): - fields = ( - si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)), - ) + fields = (si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)),) def modify_record(self, record): if record['name'] == 'Ed': # Filter out Eds @@ -129,36 +148,45 @@ def modify_record(self, record): # invalid header and/or data assert_import_errors( {'Expected "Name" in header cell A1.'}, - lambda: parse_records([['Age', 'Name']]) + lambda: parse_records([['Age', 'Name']]), ) assert_import_errors( {'Unexpected value in cell A2: must not be empty'}, - lambda: parse_records([['Name'], [''], ['data']]) + lambda: parse_records([['Name'], [''], ['data']]), ) def test_sheet_import_data_start_row(self): - parse_records = parse_records_with(si.XlrdSheetImporter([ - si.Field('Is Cool', 'is_cool', p.parse_yes_no) - ], data_start_row=3)) - - parse_records([ - [''], - [''], - ['Is Cool'], - ['yes'], - ['no'], - ]) == [{'is_cool': True}, {'is_cool': False}] + parse_records = parse_records_with( + si.XlrdSheetImporter( + [ + si.Field('Is Cool', 'is_cool', p.parse_yes_no), + ], + data_start_row=3, + ), + ) + + assert parse_records( + [ + [''], + [''], + ['Is Cool'], + ['yes'], + ['no'], + ], + ) == [{'is_cool': True}, {'is_cool': False}] assert_import_errors( {'Expected "Is Cool" in header cell A3.'}, - lambda: parse_records([['Is Cool']]) + lambda: parse_records([['Is Cool']]), ) def test_sheet_import_as_report_sheet(self): - importer = si.XlrdSheetImporter(( - si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)), - si.Field('Job', 'job', p.parse_text), - )) + importer = si.XlrdSheetImporter( + ( + si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)), + si.Field('Job', 'job', p.parse_text), + ), + ) TestReportSheet = importer.as_report_sheet() assert TestReportSheet.pre_data_rows == importer.data_start_row @@ -174,38 +202,50 @@ def test_sheet_import_no_fields(self): assert parse_records([['data']]) == [] def test_sheet_import_simple(self): - parse_records = parse_records_with(si.SheetImporter([ - si.Field('Name', 'name', p.parse_text), - ])) + parse_records = parse_records_with( + si.SheetImporter( + [ + si.Field('Name', 'name', p.parse_text), + ], + ), + ) assert parse_records([['Name']]) == [] # no data assert parse_records([['name']]) == [] # different header format assert parse_records([[' name ']]) == [] # different header format assert parse_records([['Name'], ['Ed']]) == [{'name': 'Ed'}] # one row, one column assert parse_records([['Name'], ['Ed', '']]) == [{'name': 'Ed'}] # one row, extra column - assert parse_records([['Name'], ['Ed'], ['Joe']]) \ - == [{'name': 'Ed'}, {'name': 'Joe'}] # two rows + assert parse_records([['Name'], ['Ed'], ['Joe']]) == [ + {'name': 'Ed'}, + {'name': 'Joe'}, + ] # two rows # invalid header invalid_headers = ( - [], # No data + [], # No data [['Some other name']], # Wrong header name ) for case in invalid_headers: - assert_import_errors({'Expected "Name" in header cell A1.'}, - lambda: parse_records(case)) + assert_import_errors( + {'Expected "Name" in header cell A1.'}, + lambda case=case: parse_records(case), + ) def test_sheet_import_data_validation(self): - parse_records = parse_records_with(si.SheetImporter([ - si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)), - si.Field('Age', 'age', p.parse_int), - ])) + parse_records = parse_records_with( + si.SheetImporter( + [ + si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)), + si.Field('Age', 'age', p.parse_int), + ], + ), + ) assert parse_records([['Name', 'Age']]) == [] assert parse_records([['Name', 'Age'], ['Ed', '27']]) == [{'name': 'Ed', 'age': 27}] ed_n_joe = [ - {'name': 'Ed', 'age': 27}, + {'name': 'Ed', 'age': 27}, {'name': 'Joe', 'age': 32}, ] @@ -218,37 +258,43 @@ def test_sheet_import_data_validation(self): # invalid header and/or data assert_import_errors( {'Expected "Age" in header cell B1.'}, - lambda: parse_records([['Name', 'SSN']]) + lambda: parse_records([['Name', 'SSN']]), ) assert_import_errors( {'Expected "Age" in header cell B1.'}, # header errors beat data errors - lambda: parse_records([['Name', 'SSN'], ['', '2']]) + lambda: parse_records([['Name', 'SSN'], ['', '2']]), ) assert_import_errors( - {'Unexpected value in cell A2: must not be empty', - 'Unexpected value in cell B2: must be an integer'}, - lambda: parse_records([['Name', 'Age'], ['', '3.9']]) + { + 'Unexpected value in cell A2: must not be empty', + 'Unexpected value in cell B2: must be an integer', + }, + lambda: parse_records([['Name', 'Age'], ['', '3.9']]), ) assert_import_errors( - {'Unexpected value in cell A3: must not be empty', - 'Unexpected value in cell B3: must be an integer'}, - lambda: parse_records([['Name', 'Age'], ['Ed', '27'], ['', '3.9']]) + { + 'Unexpected value in cell A3: must not be empty', + 'Unexpected value in cell B3: must be an integer', + }, + lambda: parse_records([['Name', 'Age'], ['Ed', '27'], ['', '3.9']]), ) def test_non_string_header(self): - parse_records = parse_records_with(si.SheetImporter([ - si.Field('Age', 'age', p.parse_int) - ])) + parse_records = parse_records_with( + si.SheetImporter( + [ + si.Field('Age', 'age', p.parse_int), + ], + ), + ) assert_import_errors( {'Expected "Age" in header cell A1.'}, - lambda: parse_records([[29]]) + lambda: parse_records([[29]]), ) def test_sheet_import_with_modify_record(self): class TestImport(si.SheetImporter): - fields = ( - si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)), - ) + fields = (si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)),) def modify_record(self, record): if record['name'] == 'Ed': # Filter out Eds @@ -272,36 +318,45 @@ def modify_record(self, record): # invalid header and/or data assert_import_errors( {'Expected "Name" in header cell A1.'}, - lambda: parse_records([['Age', 'Name']]) + lambda: parse_records([['Age', 'Name']]), ) assert_import_errors( {'Unexpected value in cell A2: must not be empty'}, - lambda: parse_records([['Name'], [''], ['data']]) + lambda: parse_records([['Name'], [''], ['data']]), ) def test_sheet_import_data_start_row(self): - parse_records = parse_records_with(si.SheetImporter([ - si.Field('Is Cool', 'is_cool', p.parse_yes_no) - ], data_start_row=4)) - - parse_records([ - [''], - [''], - ['Is Cool'], - ['yes'], - ['no'], - ]) == [{'is_cool': True}, {'is_cool': False}] + parse_records = parse_records_with( + si.SheetImporter( + [ + si.Field('Is Cool', 'is_cool', p.parse_yes_no), + ], + data_start_row=4, + ), + ) + + assert parse_records( + [ + [''], + [''], + ['Is Cool'], + ['yes'], + ['no'], + ], + ) == [{'is_cool': True}, {'is_cool': False}] assert_import_errors( {'Expected "Is Cool" in header cell A3.'}, - lambda: parse_records([['Is Cool']]) + lambda: parse_records([['Is Cool']]), ) def test_sheet_import_as_report_sheet(self): - importer = si.SheetImporter(( - si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)), - si.Field('Job', 'job', p.parse_text), - )) + importer = si.SheetImporter( + ( + si.Field('Name', 'name', p.chain(p.parse_text, p.validate_not_empty)), + si.Field('Job', 'job', p.parse_text), + ), + ) TestReportSheet = importer.as_report_sheet() assert TestReportSheet.pre_data_rows == importer.data_start_row diff --git a/src/tribune/tests/test_sheet_import_parsers.py b/src/tribune/tests/test_sheet_import_parsers.py index 20a163b..557bd4c 100644 --- a/src/tribune/tests/test_sheet_import_parsers.py +++ b/src/tribune/tests/test_sheet_import_parsers.py @@ -1,7 +1,9 @@ from collections import defaultdict import pytest + import tribune.sheet_import.parsers as p + from .utils import assert_import_errors @@ -18,10 +20,10 @@ def test_chain(): def test_parse_text(): - assert p.parse_text(None) == u'' - assert p.parse_text('') == u'' - assert p.parse_text(' ') == u'' - assert p.parse_text(' A ') == u'A' + assert p.parse_text(None) == '' + assert p.parse_text('') == '' + assert p.parse_text(' ') == '' + assert p.parse_text(' A ') == 'A' def test_parse_yes_no(): @@ -33,7 +35,7 @@ def test_parse_yes_no(): assert p.parse_yes_no(' NO ') is False for case in {'yen', 'YEN', ''}: - assert_import_errors({'must be "yes" or "no"'}, lambda: p.parse_yes_no(case)) + assert_import_errors({'must be "yes" or "no"'}, lambda case=case: p.parse_yes_no(case)) def test_parse_int(): @@ -47,11 +49,11 @@ def test_parse_int(): assert_really_equal(p.parse_int(1), 1) for case in {'', 'a', '1ob', 'ob1', '3.2', 'one'}: - assert_import_errors({'must be an integer'}, lambda: p.parse_int(case)) + assert_import_errors({'must be an integer'}, lambda case=case: p.parse_int(case)) def test_parse_int_as_text(): - assert_really_equal(p.parse_int_as_text('1'), u'1') + assert_really_equal(p.parse_int_as_text('1'), '1') def test_parse_number(): @@ -63,18 +65,18 @@ def test_parse_number(): assert_really_equal(p.parse_number(1.0), 1.0) for case in {'', 'a1', '1a', 'one'}: - assert_import_errors({'must be a number'}, lambda: p.parse_number(case)) + assert_import_errors({'must be a number'}, lambda case=case: p.parse_number(case)) def test_parse_int_or_text(): - assert_really_equal(p.parse_int_or_text(''), u'') - assert_really_equal(p.parse_int_or_text(' '), u'') - assert_really_equal(p.parse_int_or_text('1'), u'1') - assert_really_equal(p.parse_int_or_text('1.0'), u'1') - assert_really_equal(p.parse_int_or_text(' 937'), u'937') - assert_really_equal(p.parse_int_or_text('1e3'), u'1000') - assert_really_equal(p.parse_int_or_text('a b c'), u'a b c') - assert_really_equal(p.parse_int_or_text(' a b c '), u'a b c') + assert_really_equal(p.parse_int_or_text(''), '') + assert_really_equal(p.parse_int_or_text(' '), '') + assert_really_equal(p.parse_int_or_text('1'), '1') + assert_really_equal(p.parse_int_or_text('1.0'), '1') + assert_really_equal(p.parse_int_or_text(' 937'), '937') + assert_really_equal(p.parse_int_or_text('1e3'), '1000') + assert_really_equal(p.parse_int_or_text('a b c'), 'a b c') + assert_really_equal(p.parse_int_or_text(' a b c '), 'a b c') def test_default_to(): @@ -122,11 +124,15 @@ def test_validate_max_length(): assert p.validate_max_length(1)('a') == 'a' assert p.validate_max_length(2)('a') == 'a' assert p.validate_max_length(200)('a') == 'a' - assert p.validate_max_length(200)('a'*200) == 'a'*200 - assert_import_errors({'must be no more than 1 characters long'}, - lambda: p.validate_max_length(1)('ab')) - assert_import_errors({'must be no more than 200 characters long'}, - lambda: p.validate_max_length(200)('a'*201)) + assert p.validate_max_length(200)('a' * 200) == 'a' * 200 + assert_import_errors( + {'must be no more than 1 characters long'}, + lambda: p.validate_max_length(1)('ab'), + ) + assert_import_errors( + {'must be no more than 200 characters long'}, + lambda: p.validate_max_length(200)('a' * 201), + ) def test_validate_min(): @@ -139,12 +145,9 @@ def test_validate_min(): assert p.validate_min(float('-inf'))(1) == 1 assert p.validate_min(100)(100) == 100 - assert_import_errors({'number must be no less than 12'}, - lambda: p.validate_min(12)(11)) - assert_import_errors({'number must be no less than -30'}, - lambda: p.validate_min(-30)(-31)) - assert_import_errors({'number must be no less than 3.9'}, - lambda: p.validate_min(3.9)(3.8)) + assert_import_errors({'number must be no less than 12'}, lambda: p.validate_min(12)(11)) + assert_import_errors({'number must be no less than -30'}, lambda: p.validate_min(-30)(-31)) + assert_import_errors({'number must be no less than 3.9'}, lambda: p.validate_min(3.9)(3.8)) def test_validate_max(): @@ -154,21 +157,22 @@ def test_validate_max(): assert p.validate_max(float('inf'))(1) == 1 assert p.validate_max(100)(100) == 100 - assert_import_errors({'number must be no greater than 12'}, - lambda: p.validate_max(12)(13)) - assert_import_errors({'number must be no greater than -30'}, - lambda: p.validate_max(-30)(-29)) - assert_import_errors({'number must be no greater than 3.9'}, - lambda: p.validate_max(3.9)(4)) + assert_import_errors({'number must be no greater than 12'}, lambda: p.validate_max(12)(13)) + assert_import_errors({'number must be no greater than -30'}, lambda: p.validate_max(-30)(-29)) + assert_import_errors({'number must be no greater than 3.9'}, lambda: p.validate_max(3.9)(4)) def test_validate_range(): assert p.validate_range(-30, 30)(0) == 0 - assert_import_errors({'number must be no greater than 30'}, - lambda: p.validate_range(-30, 30)(31)) - assert_import_errors({'number must be no less than -30'}, - lambda: p.validate_range(-30, 30)(-31)) + assert_import_errors( + {'number must be no greater than 30'}, + lambda: p.validate_range(-30, 30)(31), + ) + assert_import_errors( + {'number must be no less than -30'}, + lambda: p.validate_range(-30, 30)(-31), + ) def test_validate_unique(): @@ -199,25 +203,27 @@ def test_validate_one_of(): with pytest.raises(p.SpreadsheetImportError): p.validate_one_of(others)(value) - assert_import_errors({u'must be a valid thing'}, lambda: p.validate_one_of({1})(2)) - assert_import_errors({u'must be a valid chicken'}, - lambda: p.validate_one_of({1}, thing='chicken')(2)) + assert_import_errors({'must be a valid thing'}, lambda: p.validate_one_of({1})(2)) + assert_import_errors( + {'must be a valid chicken'}, + lambda: p.validate_one_of({1}, thing='chicken')(2), + ) assert_import_errors( - {u'must be a valid chicken: 1, 2, 3'}, - lambda: p.validate_one_of([1, 2, 3], thing='chicken', show_choices_in_error=True)(4) + {'must be a valid chicken: 1, 2, 3'}, + lambda: p.validate_one_of([1, 2, 3], thing='chicken', show_choices_in_error=True)(4), ) def test_parse_list(): - assert p.parse_list(delim=' ')('') == [u''] - assert p.parse_list(delim=' ')('a') == [u'a'] - assert p.parse_list(delim=' ')(' a ') == [u'a'] - assert p.parse_list(delim=',')(' a ') == [u'a'] - assert p.parse_list(delim=' ')('a b') == [u'a', u'b'] - assert p.parse_list(delim=',')('a b') == [u'a b'] - assert p.parse_list(delim=',')('a, b, c') == [u'a', u' b', u' c'] - assert p.parse_list(p.parse_text, delim=',')('a, b, c') == [u'a', u'b', u'c'] - assert p.parse_list(lambda x: x.upper(), delim=',')('a,b,c') == [u'A', u'B', u'C'] + assert p.parse_list(delim=' ')('') == [''] + assert p.parse_list(delim=' ')('a') == ['a'] + assert p.parse_list(delim=' ')(' a ') == ['a'] + assert p.parse_list(delim=',')(' a ') == ['a'] + assert p.parse_list(delim=' ')('a b') == ['a', 'b'] + assert p.parse_list(delim=',')('a b') == ['a b'] + assert p.parse_list(delim=',')('a, b, c') == ['a', ' b', ' c'] + assert p.parse_list(p.parse_text, delim=',')('a, b, c') == ['a', 'b', 'c'] + assert p.parse_list(lambda x: x.upper(), delim=',')('a,b,c') == ['A', 'B', 'C'] def test_parse_lookup(): diff --git a/src/tribune/tests/test_utils.py b/src/tribune/tests/test_utils.py index 6ce5383..8769190 100644 --- a/src/tribune/tests/test_utils.py +++ b/src/tribune/tests/test_utils.py @@ -13,14 +13,14 @@ def test_column_letter(): assert column_letter(26) == 'Z' assert column_letter(27) == 'AA' assert column_letter(28) == 'AB' - assert column_letter(26*2 + 1) == 'BA' - assert column_letter(26*3 + 1) == 'CA' - assert column_letter(26**2+26) == 'ZZ' - assert column_letter(26**2+27) == 'AAA' + assert column_letter(26 * 2 + 1) == 'BA' + assert column_letter(26 * 3 + 1) == 'CA' + assert column_letter(26**2 + 26) == 'ZZ' + assert column_letter(26**2 + 27) == 'AAA' assert column_letter(1, is_zero_based=True) == 'B' -class TestDeepCopyTupleExcept(object): +class TestDeepCopyTupleExcept: class DoDeepCopy: def __init__(self, value): self.value = value @@ -29,7 +29,7 @@ def __eq__(self, other): return self.value == other.value class DoNotDeepCopy(DoDeepCopy): - def __deepcopy__(self, memodict={}): + def __deepcopy__(self, memodict): raise Exception('Deep Copy!') def test_simple(self): @@ -80,7 +80,7 @@ def test_exceptions_are_not_copied_at_all(self): def test_exceptions_are_used(self): original = (1, self.DoNotDeepCopy([2, 3, 4]), 5) - with pytest.raises(Exception): + with pytest.raises(Exception, match='Deep Copy!'): deepcopy_tuple_except(original) diff --git a/src/tribune/tests/utils.py b/src/tribune/tests/utils.py index 0c222c9..2bde3c1 100644 --- a/src/tribune/tests/utils.py +++ b/src/tribune/tests/utils.py @@ -14,9 +14,10 @@ # within, and starting/ending indices # returns -1 if the value is not found + def find_sheet_row(sheet, needle, column, start=0, end=100): try: - for x in range(start, end+1): + for x in range(start, end + 1): if sheet.cell_value(x, column) == needle: return x except IndexError: @@ -26,7 +27,7 @@ def find_sheet_row(sheet, needle, column, start=0, end=100): def find_sheet_col(sheet, needle, row, start=0, end=100): try: - for x in range(start, end+1): + for x in range(start, end + 1): if sheet.cell_value(row, x) == needle: return x except IndexError: @@ -37,27 +38,36 @@ def find_sheet_col(sheet, needle, row, start=0, end=100): def assert_import_errors(errors, func): try: func() - assert False, 'Did not raise SpreadsheetImportError' + raise AssertionError('Did not raise SpreadsheetImportError') except SpreadsheetImportError as e: assert set(e.errors) == set(errors) -class DefaultColsMixin(object): +class DefaultColsMixin: id = sa.Column(sa.Integer, primary_key=True) - createdts = sa.Column(sa.DateTime, nullable=False, default=dt.datetime.now, - server_default=sasql.text('CURRENT_TIMESTAMP')) - updatedts = sa.Column(sa.DateTime, nullable=False, default=dt.datetime.now, - server_default=sasql.text('CURRENT_TIMESTAMP'), onupdate=dt.datetime.now) + createdts = sa.Column( + sa.DateTime, + nullable=False, + default=dt.datetime.now, + server_default=sasql.text('CURRENT_TIMESTAMP'), + ) + updatedts = sa.Column( + sa.DateTime, + nullable=False, + default=dt.datetime.now, + server_default=sasql.text('CURRENT_TIMESTAMP'), + onupdate=dt.datetime.now, + ) @wrapt.decorator def transaction(f, decorated_obj, args, kwargs): """ - decorates a function so that a DB transaction is always committed after - the wrapped function returns and also rolls back the transaction if - an unhandled exception occurs. + decorates a function so that a DB transaction is always committed after + the wrapped function returns and also rolls back the transaction if + an unhandled exception occurs. - 'ncm' = non class method (version) + 'ncm' = non class method (version) """ from tribune.tests.entities import session as dbsess @@ -72,15 +82,16 @@ def transaction(f, decorated_obj, args, kwargs): def transaction_classmethod(f): """ - like transaction() but makes the function a class method + like transaction() but makes the function a class method """ return transaction(classmethod(f)) -class MethodsMixin(object): +class MethodsMixin: @classmethod def _sa_sess(cls): from .entities import session + return session @transaction_classmethod @@ -108,22 +119,19 @@ def from_dict(self, data): # If the data doesn't contain any pk, and the relationship # already has a value, update that record. - if not [1 for p in pk_props if p.key in data] and \ - dbvalue is not None: + if not [1 for p in pk_props if p.key in data] and dbvalue is not None: dbvalue.from_dict(value) else: record = rel_class.update_or_create(value) setattr(self, key, record) - elif isinstance(value, list) and \ - value and isinstance(value[0], dict): - + elif isinstance(value, list) and value and isinstance(value[0], dict): rel_class = mapper.get_property(key).mapper.class_ new_attr_value = [] for row in value: if not isinstance(row, dict): raise Exception( 'Cannot send mixed (dict/non dict) data ' - 'to list relationships in from_dict data.' + 'to list relationships in from_dict data.', ) record = rel_class.update_or_create(row) new_attr_value.append(record) diff --git a/src/tribune/utils.py b/src/tribune/utils.py index f4c8e2b..b7e6785 100644 --- a/src/tribune/utils.py +++ b/src/tribune/utils.py @@ -1,12 +1,14 @@ import copy -import string +from functools import reduce from itertools import ( chain as _itertools_chain, +) +from itertools import ( count, islice, takewhile, ) -from functools import reduce +import string def raises(e): @@ -45,16 +47,17 @@ def unzip(iterable): :returns: an iterable of the unzipped input. """ - return map(list, zip(*iterable)) + return map(list, zip(*iterable, strict=True)) def flatten(iterable): - """Takes an iterable of iterables and flattens it by one layer (e.g. [[1],[2]] becomes [1,2]). + """ + Takes an iterable of iterables and flattens it by one layer (e.g. [[1],[2]] becomes [1,2]). """ return list(_itertools_chain(*iterable)) -def deepcopy_tuple_except(tup, exceptions=type(None)): +def deepcopy_tuple_except(tup, exceptions=None): """ Deeply-copy a nested tuple, but don't copy any items within the tuple which have a type listed in `exceptions` @@ -63,6 +66,8 @@ def deepcopy_tuple_except(tup, exceptions=type(None)): :param exceptions: a set of types not to copy :return: a clone of `tup` """ + if exceptions is None: + exceptions = type(None) result = [] for item in tup: