Initial commit
This commit is contained in:
27
Solutions/8_1/follow.py
Normal file
27
Solutions/8_1/follow.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# follow.py
|
||||
import os
|
||||
import time
|
||||
|
||||
def follow(filename):
|
||||
'''
|
||||
Generator that produces a sequence of lines being written at the end of a file.
|
||||
'''
|
||||
with open(filename,'r') as f:
|
||||
f.seek(0,os.SEEK_END)
|
||||
while True:
|
||||
line = f.readline()
|
||||
if line == '':
|
||||
time.sleep(0.1) # Sleep briefly to avoid busy wait
|
||||
continue
|
||||
yield line
|
||||
|
||||
# Example use
|
||||
if __name__ == '__main__':
|
||||
for line in follow('../../Data/stocklog.csv'):
|
||||
fields = line.split(',')
|
||||
name = fields[0].strip('"')
|
||||
price = float(fields[1])
|
||||
change = float(fields[4])
|
||||
if change < 0:
|
||||
print('%10s %10.2f %10.2f' % (name, price, change))
|
||||
|
||||
43
Solutions/8_1/reader.py
Normal file
43
Solutions/8_1/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)
|
||||
|
||||
24
Solutions/8_1/stock.py
Normal file
24
Solutions/8_1/stock.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# stock.py
|
||||
|
||||
from structure import Structure
|
||||
from validate import String, PositiveInteger, PositiveFloat
|
||||
|
||||
class Stock(Structure):
|
||||
name = String('name')
|
||||
shares = PositiveInteger('shares')
|
||||
price = PositiveFloat('price')
|
||||
|
||||
@property
|
||||
def cost(self):
|
||||
return self.shares * self.price
|
||||
|
||||
def sell(self, nshares):
|
||||
self.shares -= nshares
|
||||
|
||||
if __name__ == '__main__':
|
||||
from reader import read_csv_as_instances
|
||||
from 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)
|
||||
92
Solutions/8_1/structure.py
Normal file
92
Solutions/8_1/structure.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# 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
|
||||
70
Solutions/8_1/teststock.py
Normal file
70
Solutions/8_1/teststock.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# teststock.py
|
||||
|
||||
import stock
|
||||
import unittest
|
||||
|
||||
class TestStock(unittest.TestCase):
|
||||
def test_create(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
self.assertEqual(s.name, 'GOOG')
|
||||
self.assertEqual(s.shares, 100)
|
||||
self.assertEqual(s.price, 490.1)
|
||||
|
||||
def test_create_keyword(self):
|
||||
s = stock.Stock(name='GOOG', shares=100, price=490.1)
|
||||
self.assertEqual(s.name, 'GOOG')
|
||||
self.assertEqual(s.shares, 100)
|
||||
self.assertEqual(s.price, 490.1)
|
||||
|
||||
def test_cost(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
self.assertEqual(s.cost, 49010.0)
|
||||
|
||||
def test_sell(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
s.sell(25)
|
||||
self.assertEqual(s.shares, 75)
|
||||
|
||||
def test_from_row(self):
|
||||
s = stock.Stock.from_row(['GOOG','100','490.1'])
|
||||
self.assertEqual(s.name, 'GOOG')
|
||||
self.assertEqual(s.shares, 100)
|
||||
self.assertEqual(s.price, 490.1)
|
||||
|
||||
def test_repr(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)")
|
||||
|
||||
def test_eq(self):
|
||||
a = stock.Stock('GOOG', 100, 490.1)
|
||||
b = stock.Stock('GOOG', 100, 490.1)
|
||||
self.assertTrue(a==b)
|
||||
|
||||
# Tests for failure conditions
|
||||
def test_shares_badtype(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
with self.assertRaises(TypeError):
|
||||
s.shares = '50'
|
||||
|
||||
def test_shares_badvalue(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
with self.assertRaises(ValueError):
|
||||
s.shares = -50
|
||||
|
||||
def test_price_badtype(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
with self.assertRaises(TypeError):
|
||||
s.price = '45.23'
|
||||
|
||||
def test_price_badvalue(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
with self.assertRaises(ValueError):
|
||||
s.price = -45.23
|
||||
|
||||
def test_bad_attribute(self):
|
||||
s = stock.Stock('GOOG', 100, 490.1)
|
||||
with self.assertRaises(AttributeError):
|
||||
s.share = 100
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
173
Solutions/8_1/validate.py
Normal file
173
Solutions/8_1/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