Initial commit
This commit is contained in:
23
Solutions/9_2/stock.py
Normal file
23
Solutions/9_2/stock.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# stock.py
|
||||
|
||||
from structly.structure import Structure
|
||||
|
||||
class Stock(Structure):
|
||||
name = String()
|
||||
shares = PositiveInteger()
|
||||
price = PositiveFloat()
|
||||
|
||||
@property
|
||||
def cost(self):
|
||||
return self.shares * self.price
|
||||
|
||||
def sell(self, nshares: PositiveInteger):
|
||||
self.shares -= nshares
|
||||
|
||||
if __name__ == '__main__':
|
||||
from structly.reader import read_csv_as_instances
|
||||
from structly.tableformat import create_formatter, print_table
|
||||
portfolio = read_csv_as_instances('../../Data/portfolio.csv', Stock)
|
||||
formatter = create_formatter('text')
|
||||
print_table(portfolio, ['name','shares','price'], formatter)
|
||||
|
||||
0
Solutions/9_2/structly/__init__.py
Normal file
0
Solutions/9_2/structly/__init__.py
Normal file
43
Solutions/9_2/structly/reader.py
Normal file
43
Solutions/9_2/structly/reader.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# reader.py
|
||||
|
||||
import csv
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def convert_csv(lines, converter, *, headers=None):
|
||||
rows = csv.reader(lines)
|
||||
if headers is None:
|
||||
headers = next(rows)
|
||||
|
||||
records = []
|
||||
for rowno, row in enumerate(rows, start=1):
|
||||
try:
|
||||
records.append(converter(headers, row))
|
||||
except ValueError as e:
|
||||
log.warning('Row %s: Bad row: %s', rowno, row)
|
||||
log.debug('Row %s: Reason: %s', rowno, row)
|
||||
return records
|
||||
|
||||
def csv_as_dicts(lines, types, *, headers=None):
|
||||
return convert_csv(lines,
|
||||
lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row) })
|
||||
|
||||
def csv_as_instances(lines, cls, *, headers=None):
|
||||
return convert_csv(lines,
|
||||
lambda headers, row: cls.from_row(row))
|
||||
|
||||
def read_csv_as_dicts(filename, types, *, headers=None):
|
||||
'''
|
||||
Read CSV data into a list of dictionaries with optional type conversion
|
||||
'''
|
||||
with open(filename) as file:
|
||||
return csv_as_dicts(file, types, headers=headers)
|
||||
|
||||
def read_csv_as_instances(filename, cls, *, headers=None):
|
||||
'''
|
||||
Read CSV data into a list of instances
|
||||
'''
|
||||
with open(filename) as file:
|
||||
return csv_as_instances(file, cls, headers=headers)
|
||||
|
||||
91
Solutions/9_2/structly/structure.py
Normal file
91
Solutions/9_2/structly/structure.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# structure.py
|
||||
|
||||
from .validate import Validator, validated
|
||||
from collections import ChainMap
|
||||
|
||||
class StructureMeta(type):
|
||||
@classmethod
|
||||
def __prepare__(meta, clsname, bases):
|
||||
return ChainMap({}, Validator.validators)
|
||||
|
||||
@staticmethod
|
||||
def __new__(meta, name, bases, methods):
|
||||
methods = methods.maps[0]
|
||||
return super().__new__(meta, name, bases, methods)
|
||||
|
||||
class Structure(metaclass=StructureMeta):
|
||||
_fields = ()
|
||||
_types = ()
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name.startswith('_') or name in self._fields:
|
||||
super().__setattr__(name, value)
|
||||
else:
|
||||
raise AttributeError('No attribute %s' % name)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%s)' % (type(self).__name__,
|
||||
', '.join(repr(getattr(self, name)) for name in self._fields))
|
||||
|
||||
def __iter__(self):
|
||||
for name in self._fields:
|
||||
yield getattr(self, name)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, type(self)) and tuple(self) == tuple(other)
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row):
|
||||
rowdata = [ func(val) for func, val in zip(cls._types, row) ]
|
||||
return cls(*rowdata)
|
||||
|
||||
@classmethod
|
||||
def create_init(cls):
|
||||
'''
|
||||
Create an __init__ method from _fields
|
||||
'''
|
||||
args = ','.join(cls._fields)
|
||||
code = f'def __init__(self, {args}):\n'
|
||||
for name in cls._fields:
|
||||
code += f' self.{name} = {name}\n'
|
||||
locs = { }
|
||||
exec(code, locs)
|
||||
cls.__init__ = locs['__init__']
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls):
|
||||
# Apply the validated decorator to subclasses
|
||||
validate_attributes(cls)
|
||||
|
||||
def validate_attributes(cls):
|
||||
'''
|
||||
Class decorator that scans a class definition for Validators
|
||||
and builds a _fields variable that captures their definition order.
|
||||
'''
|
||||
validators = []
|
||||
for name, val in vars(cls).items():
|
||||
if isinstance(val, Validator):
|
||||
validators.append(val)
|
||||
|
||||
# Apply validated decorator to any callable with annotations
|
||||
elif callable(val) and val.__annotations__:
|
||||
setattr(cls, name, validated(val))
|
||||
|
||||
# Collect all of the field names
|
||||
cls._fields = tuple([v.name for v in validators])
|
||||
|
||||
# Collect type conversions. The lambda x:x is an identity
|
||||
# function that's used in case no expected_type is found.
|
||||
cls._types = tuple([ getattr(v, 'expected_type', lambda x: x)
|
||||
for v in validators ])
|
||||
|
||||
# Create the __init__ method
|
||||
if cls._fields:
|
||||
cls.create_init()
|
||||
|
||||
|
||||
return cls
|
||||
|
||||
def typed_structure(clsname, **validators):
|
||||
cls = type(clsname, (Structure,), validators)
|
||||
return cls
|
||||
83
Solutions/9_2/structly/tableformat.py
Normal file
83
Solutions/9_2/structly/tableformat.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# tableformat.py
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
def print_table(records, fields, formatter):
|
||||
if not isinstance(formatter, TableFormatter):
|
||||
raise RuntimeError('Expected a TableFormatter')
|
||||
|
||||
formatter.headings(fields)
|
||||
for r in records:
|
||||
rowdata = [getattr(r, fieldname) for fieldname in fields]
|
||||
formatter.row(rowdata)
|
||||
|
||||
class TableFormatter(ABC):
|
||||
@abstractmethod
|
||||
def headings(self, headers):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def row(self, rowdata):
|
||||
pass
|
||||
|
||||
|
||||
class TextTableFormatter(TableFormatter):
|
||||
def headings(self, headers):
|
||||
print(' '.join('%10s' % h for h in headers))
|
||||
print(('-'*10 + ' ')*len(headers))
|
||||
|
||||
def row(self, rowdata):
|
||||
print(' '.join('%10s' % d for d in rowdata))
|
||||
|
||||
class CSVTableFormatter(TableFormatter):
|
||||
def headings(self, headers):
|
||||
print(','.join(headers))
|
||||
|
||||
def row(self, rowdata):
|
||||
print(','.join(str(d) for d in rowdata))
|
||||
|
||||
class HTMLTableFormatter(TableFormatter):
|
||||
def headings(self, headers):
|
||||
print('<tr>', end=' ')
|
||||
for h in headers:
|
||||
print('<th>%s</th>' % h, end=' ')
|
||||
print('</tr>')
|
||||
|
||||
def row(self, rowdata):
|
||||
print('<tr>', end=' ')
|
||||
for d in rowdata:
|
||||
print('<td>%s</td>' % d, end=' ')
|
||||
print('</tr>')
|
||||
|
||||
class ColumnFormatMixin:
|
||||
formats = []
|
||||
def row(self, rowdata):
|
||||
rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)]
|
||||
super().row(rowdata)
|
||||
|
||||
class UpperHeadersMixin:
|
||||
def headings(self, headers):
|
||||
super().headings([h.upper() for h in headers])
|
||||
|
||||
def create_formatter(name, column_formats=None, upper_headers=False):
|
||||
if name == 'text':
|
||||
formatter_cls = TextTableFormatter
|
||||
elif name == 'csv':
|
||||
formatter_cls = CSVTableFormatter
|
||||
elif name == 'html':
|
||||
formatter_cls = HTMLTableFormatter
|
||||
else:
|
||||
raise RuntimeError('Unknown format %s' % name)
|
||||
|
||||
if column_formats:
|
||||
class formatter_cls(ColumnFormatMixin, formatter_cls):
|
||||
formats = column_formats
|
||||
|
||||
if upper_headers:
|
||||
class formatter_cls(UpperHeadersMixin, formatter_cls):
|
||||
pass
|
||||
|
||||
return formatter_cls()
|
||||
|
||||
|
||||
|
||||
|
||||
173
Solutions/9_2/structly/validate.py
Normal file
173
Solutions/9_2/structly/validate.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# validate.py
|
||||
|
||||
class Validator:
|
||||
def __init__(self, name=None):
|
||||
self.name = name
|
||||
|
||||
def __set_name__(self, cls, name):
|
||||
self.name = name
|
||||
|
||||
@classmethod
|
||||
def check(cls, value):
|
||||
return value
|
||||
|
||||
def __set__(self, instance, value):
|
||||
instance.__dict__[self.name] = self.check(value)
|
||||
|
||||
# Collect all derived classes into a dict
|
||||
validators = { }
|
||||
@classmethod
|
||||
def __init_subclass__(cls):
|
||||
cls.validators[cls.__name__] = cls
|
||||
|
||||
class Typed(Validator):
|
||||
expected_type = object
|
||||
@classmethod
|
||||
def check(cls, value):
|
||||
if not isinstance(value, cls.expected_type):
|
||||
raise TypeError(f'expected {cls.expected_type}')
|
||||
return super().check(value)
|
||||
|
||||
_typed_classes = [
|
||||
('Integer', int),
|
||||
('Float', float),
|
||||
('String', str) ]
|
||||
|
||||
globals().update((name, type(name, (Typed,), {'expected_type':ty}))
|
||||
for name, ty in _typed_classes)
|
||||
|
||||
class Positive(Validator):
|
||||
@classmethod
|
||||
def check(cls, value):
|
||||
if value < 0:
|
||||
raise ValueError('must be >= 0')
|
||||
return super().check(value)
|
||||
|
||||
class NonEmpty(Validator):
|
||||
@classmethod
|
||||
def check(cls, value):
|
||||
if len(value) == 0:
|
||||
raise ValueError('must be non-empty')
|
||||
return super().check(value)
|
||||
|
||||
class PositiveInteger(Integer, Positive):
|
||||
pass
|
||||
|
||||
class PositiveFloat(Float, Positive):
|
||||
pass
|
||||
|
||||
class NonEmptyString(String, NonEmpty):
|
||||
pass
|
||||
|
||||
from inspect import signature
|
||||
from functools import wraps
|
||||
|
||||
def isvalidator(item):
|
||||
return isinstance(item, type) and issubclass(item, Validator)
|
||||
|
||||
def validated(func):
|
||||
sig = signature(func)
|
||||
|
||||
# Gather the function annotations
|
||||
annotations = { name:val for name, val in func.__annotations__.items()
|
||||
if isvalidator(val) }
|
||||
|
||||
# Get the return annotation (if any)
|
||||
retcheck = annotations.pop('return', None)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
bound = sig.bind(*args, **kwargs)
|
||||
errors = []
|
||||
|
||||
# Enforce argument checks
|
||||
for name, validator in annotations.items():
|
||||
try:
|
||||
validator.check(bound.arguments[name])
|
||||
except Exception as e:
|
||||
errors.append(f' {name}: {e}')
|
||||
|
||||
if errors:
|
||||
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
|
||||
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
# Enforce return check (if any)
|
||||
if retcheck:
|
||||
try:
|
||||
retcheck.check(result)
|
||||
except Exception as e:
|
||||
raise TypeError(f'Bad return: {e}') from None
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
def enforce(**annotations):
|
||||
retcheck = annotations.pop('return_', None)
|
||||
|
||||
def decorate(func):
|
||||
sig = signature(func)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
bound = sig.bind(*args, **kwargs)
|
||||
errors = []
|
||||
|
||||
# Enforce argument checks
|
||||
for name, validator in annotations.items():
|
||||
try:
|
||||
validator.check(bound.arguments[name])
|
||||
except Exception as e:
|
||||
errors.append(f' {name}: {e}')
|
||||
|
||||
if errors:
|
||||
raise TypeError('Bad Arguments\n' + '\n'.join(errors))
|
||||
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
if retcheck:
|
||||
try:
|
||||
retcheck.check(result)
|
||||
except Exception as e:
|
||||
raise TypeError(f'Bad return: {e}') from None
|
||||
return result
|
||||
return wrapper
|
||||
return decorate
|
||||
|
||||
# Examples
|
||||
if __name__ == '__main__':
|
||||
@validated
|
||||
def add(x:Integer, y:Integer) -> Integer:
|
||||
return x + y
|
||||
|
||||
@validated
|
||||
def div(x:Integer, y:Integer) -> Integer:
|
||||
return x / y
|
||||
|
||||
@enforce(x=Integer, y=Integer)
|
||||
def sub(x, y):
|
||||
return x - y
|
||||
|
||||
class Stock:
|
||||
name = NonEmptyString()
|
||||
shares = PositiveInteger()
|
||||
price = PositiveFloat()
|
||||
def __init__(self, name, shares, price):
|
||||
self.name = name
|
||||
self.shares = shares
|
||||
self.price = price
|
||||
|
||||
def __repr__(self):
|
||||
return f'Stock({self.name!r}, {self.shares!r}, {self.price!r})'
|
||||
|
||||
@property
|
||||
def cost(self):
|
||||
return self.shares * self.price
|
||||
|
||||
@validated
|
||||
def sell(self, nshares:PositiveInteger):
|
||||
self.shares -= nshares
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user