Initial commit

This commit is contained in:
David Beazley
2023-07-16 20:21:00 -05:00
parent 82e815fab2
commit 7d4b30154a
259 changed files with 600233 additions and 2 deletions

27
Solutions/8_1/follow.py Normal file
View 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
View 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
View 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)

View 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

View 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
View 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