From d871b47d279ba78a03af6abb8dbc7a29e15afadd Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Sat, 19 Aug 2023 12:57:35 +0530 Subject: [PATCH 01/22] exercises added --- .vscode/settings.json | 6 ++ art.py | 15 +++++ mutint.py | 65 ++++++++++++++++++++ pcost.py | 34 +++++++++++ readport.py | 20 ++++++ readrides.py | 138 ++++++++++++++++++++++++++++++++++++++++++ stock.py | 11 ++++ 7 files changed, 289 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 art.py create mode 100644 mutint.py create mode 100644 pcost.py create mode 100644 readport.py create mode 100644 readrides.py create mode 100644 stock.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..ba77eac9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[python]": { + "editor.defaultFormatter": "ms-python.python" + }, + "python.formatting.provider": "none" +} \ No newline at end of file diff --git a/art.py b/art.py new file mode 100644 index 00000000..5ca0efc9 --- /dev/null +++ b/art.py @@ -0,0 +1,15 @@ +# art.py + +import sys +import random + +chars = '\|/' + +def draw(rows, columns): + for r in range(rows): + print(''.join(random.choice(chars) for _ in range(columns))) + +if __name__ == '__main__': + if len(sys.argv) != 3: + raise SystemExit("Usage: art.py rows columns") + draw(int(sys.argv[1]), int(sys.argv[2])) \ No newline at end of file diff --git a/mutint.py b/mutint.py new file mode 100644 index 00000000..2300d76c --- /dev/null +++ b/mutint.py @@ -0,0 +1,65 @@ +# mutint.py + +from functools import total_ordering + +@total_ordering +class MutInt: + __slot__ = ['value'] + + def __init__(self, value): + self.value = value + + def __str__(self): + return str(self.value) + + def __repr__(self): + return f'MutInt({self.value!r})' + + def __format__(self, fmt): + return format(self.value, fmt) + + def __add__(self, other): + if isinstance(other, MutInt): + return MutInt(self.value + other.value) + elif isinstance(other, int): + return MutInt(self.value + other) + else: + return NotImplemented + + __radd__ = __add__ + + def __iadd__(self, other): + if isinstance(other, MutInt): + self.value += other.value + return self + elif isinstance(other, int): + self.value += other + return self + else: + return NotImplemented + + def __eq__(self, other): + if isinstance(other, MutInt): + return self.value == other.value + elif isinstance(other, int): + return self.value == other + else: + return NotImplemented + + def __lt__(self, other): + if isinstance(other, MutInt): + return self.value < other.value + elif isinstance(other, int): + return self.value < other + else: + return NotImplemented + + def __int__(self): + return self.value + + def __float__(self): + return float(self.value) + + __index__ = __int__ # Make indexing work + + \ No newline at end of file diff --git a/pcost.py b/pcost.py new file mode 100644 index 00000000..a8d84bd3 --- /dev/null +++ b/pcost.py @@ -0,0 +1,34 @@ +# pcost.py + +# total_cost = 0.0 + +# with open('Data/portfolio.dat', 'r') as f: +# for line in f: +# row = line.split() +# nshares = int(row[1]) +# price = float(row[2]) +# total_cost += nshares * price + + +# print('Total Cost:', total_cost) + + +def portfolio_cost(filename): + total_cost = 0.0 + with open(filename, 'r') as f: + for line in f: + row = line.split() + try: + nshares = int(row[1]) + price = float(row[2]) + total_cost += nshares * price + except ValueError as e: + print('Couldn\'t parse:', repr(line)) + print('Reason:', e) + continue + return total_cost + +if __name__ == '__main__': + print(portfolio_cost('Data/portfolio.dat')) + + \ No newline at end of file diff --git a/readport.py b/readport.py new file mode 100644 index 00000000..6d38448c --- /dev/null +++ b/readport.py @@ -0,0 +1,20 @@ +# readport.py + +import csv + +# A function that reads a file into a list of dicts + + +def read_portfolio(filename): + portfolio = [] + with open(filename) as f: + rows = csv.reader(f) + headers = next(rows) + for row in rows: + record = { + 'name': row[0], + 'shares': int(row[1]), + 'price': float(row[2]) + } + portfolio.append(record) + return portfolio diff --git a/readrides.py b/readrides.py new file mode 100644 index 00000000..3c577b67 --- /dev/null +++ b/readrides.py @@ -0,0 +1,138 @@ +# readrides.py + +import csv + +def read_rides_as_tuples(filename): + ''' + Read the bus ride data as a list of tuples + ''' + records = [] + with open(filename) as f: + rows = csv.reader(f) + headings = next(rows) # Skip headers + for row in rows: + route = row[0] + date = row[1] + daytype = row[2] + rides = int(row[3]) + record = (route, date, daytype, rides) + records.append(record) + return records + +def read_rides_as_dicts(filename): + ''' + Read the bus ride data as a list of dicts + ''' + records = RideData() # <---- CHANGED + with open(filename) as f: + rows = csv.reader(f) + heading = next(rows) # Skip headers + for row in rows: + route = row[0] + date = row[1] + daytype = row[2] + rides = int(row[3]) + record = { + 'route': route, + 'date': date, + 'daytype': daytype, + 'rides': rides + } + records.append(record) + return records + +class Row: + # Slot class, uncomment this to use slot + # __slots__ = ('route', 'date', 'daytype', 'rides') + def __init__(self, route, date, dattype, rides): + self.route = route + self.date = date + self.daytype = dattype + self.rides = rides + +# Named Tuples, uncomment this to use named tuples +# from collections import namedtuple +# Row = namedtuple('Row', ['route', 'date', 'daytype', 'rides']) + +def read_rides_as_instances(filename): + ''' + Read the bus ride data as a list of instances + ''' + records = [] + with open(filename) as f: + rows = csv.reader(f) + heading = next(rows) # Skip headers + for row in rows: + route = row[0] + date = row[1] + daytype = row[2] + rides = int(row[3]) + record = Row(route, date, daytype, rides) + records.append(record) + return records + +def read_rides_as_columns(filename): + ''' + Read the bus ride data into 4 lists, representing columns + ''' + routes = [] + dates = [] + daytypes = [] + numrides = [] + with open(filename) as f: + rows = csv.reader(f) + headings = next(rows) # Skip headers + for row in rows: + routes.append(row[0]) + dates.append(row[1]) + daytypes.append(row[2]) + numrides.append(row[3]) + + return dict(routes=routes, dates=dates, daytypes=daytypes, numrides=numrides) + +# The great "fake" +from collections.abc import Sequence +class RideData(Sequence): + def __init__(self): + # Each value is a list with all the values( a column) + self.routes = [] # Columns + self.dates = [] + self.dattypes = [] + self.numrides = [] + + def __len__(self): + # All lists assumed to have the same length + return len(self.routes) + + def __getitem__(self, index): + # If index is given as slice + if isinstance(index, slice): + new_self = self.__class__() + new_self.routes = self.routes[index] + new_self.dates = self.dates[index] + new_self.dattypes = self.dattypes[index] + new_self.numrides = self.numrides[index] + return new_self + return { + 'route': self.routes[index], + 'date': self.dates[index], + 'daytype': self.dattypes[index], + 'rides': self.numrides[index] } + + def append(self, d): + self.routes.append(d['route']) + self.dates.append(d['date']) + self.dattypes.append(d['daytype']) + self.numrides.append(d['rides']) + + + +if __name__ == '__main__': + import tracemalloc + tracemalloc.start() + read_rides = read_rides_as_dicts + rows = read_rides('Data/ctabus.csv') + + print('Memory Use: Current %d, Peak %d' % tracemalloc.get_traced_memory()) + + # slot < tuple < namedtuple < class row < dict \ No newline at end of file diff --git a/stock.py b/stock.py new file mode 100644 index 00000000..d03b3d59 --- /dev/null +++ b/stock.py @@ -0,0 +1,11 @@ +# stock.py + +class Stock: + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def cost(self): + return self.shares * self.price + \ No newline at end of file From ce9587b37b3562b9a040f936704ddebed214b21e Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Sun, 20 Aug 2023 01:10:50 +0530 Subject: [PATCH 02/22] exercise 3.1 --- reader.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ stock.py | 24 ++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 reader.py diff --git a/reader.py b/reader.py new file mode 100644 index 00000000..cf338c18 --- /dev/null +++ b/reader.py @@ -0,0 +1,61 @@ +# reader.py + +import csv + +def read_csv_as_dicts(filename, types): + ''' + Read a CSV file with column type conversion + ''' + records = [] + with open(filename, 'r') as f: + rows = csv.reader(f) + headers = next(rows) + for row in rows: + record = {name: func(val) for name, func, val in zip(headers, types, row)} + records.append(record) + + return records + +from collections.abc import Sequence +class DataCollection(Sequence): + def __init__(self, headers): + for col_name in headers: + setattr(self, col_name, []) + self.headers = headers + + def __len__(self): + return len(getattr(self, self.headers[0])) + + def __getitem__(self, index): + if isinstance(index, slice): + new_self = self.__class__(self.headers) + for col_name in self.headers: + setattr(new_self, col_name, getattr(self, col_name)[index]) + return new_self + + data = {col_name:getattr(self, col_name)[index] for col_name in self.headers} + return data + + def append(self, d): + for col_name in self.headers: + getattr(self, col_name).append(d[col_name]) + + +def read_csv_as_columns(filename, types): + with open(filename, 'r') as f: + rows = csv.reader(f) + headers = next(rows) + data = DataCollection(headers) + for row in rows: + record = {name:func(val) for name, func, val in zip(headers, types, row)} + data.append(record) + + return data + + + + + + + + diff --git a/stock.py b/stock.py index d03b3d59..089b927e 100644 --- a/stock.py +++ b/stock.py @@ -8,4 +8,28 @@ def __init__(self, name, shares, price): def cost(self): return self.shares * self.price + + def sell(self, nshares): + if nshares > 0 and nshares < self.shares: + self.shares -= nshares + +import csv +def read_portfolio(filename): + records = [] + with open(filename) as f: + rows = csv.reader(f) + headers = next(rows) # Skip headers + for row in rows: + records.append( Stock( str(row[0]), int(row[1]),float(row[2]) ) ) + return records + +def print_portfolio(portfolio): + headers = ['name', 'shares', 'price'] + print('%10s %10s %10s' % (headers[0], headers[1], headers[2])) + print(('-' * 10 + ' ') * len(headers)) + for s in portfolio: + print('%10s %10d %10.2f' % (s.name, s.shares, s.price)) + + + \ No newline at end of file From 293a7c47c5b3477ba0fe81b6b17bfcf4c334b4e3 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Tue, 22 Aug 2023 10:43:57 +0530 Subject: [PATCH 03/22] exercise 3.2 to 3.5 --- date.py | 36 ++++++++++++++++++++++++ reader.py | 12 ++++++++ stock.py | 38 +++++++++++++++++++++++-- tableformat.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 date.py create mode 100644 tableformat.py diff --git a/date.py b/date.py new file mode 100644 index 00000000..8a5ad8e6 --- /dev/null +++ b/date.py @@ -0,0 +1,36 @@ +# date.py + +import time + +class Date: + def __init__(self, year, month, day): + self.year = year + self.month = month + self.day = day + + def __str__(self): + return '%d-%d-%d' % (self.year, self.month, self.day) + + def __repr__(self): + return 'Date(%r,%r,%r)' % (self.year, self.month, self.day) + + @classmethod + def today(cls): + t = time.localtime() + self = cls.__new__(cls) + self.year = t.tm_year + self.month = t.tm_mon + self.day = t.tm_mday + return self + +class Manager: + def __enter__(self): + print('Enterning...') + return self + + def __exit__(self, ty, val, tb): + print('Leaving...') + if ty: + print('An exception occurred.') + + diff --git a/reader.py b/reader.py index cf338c18..7a820059 100644 --- a/reader.py +++ b/reader.py @@ -52,6 +52,18 @@ def read_csv_as_columns(filename, types): return data +def read_csv_as_instances(filename, cls): + ''' + Read a CSV file into a list of instances + ''' + records = [] + with open(filename) as f: + rows = csv.reader(f) + headers = next(rows) + for row in rows: + records.append(cls.from_row(row)) + return records + diff --git a/stock.py b/stock.py index 089b927e..c20ec10f 100644 --- a/stock.py +++ b/stock.py @@ -1,18 +1,52 @@ # stock.py class Stock: + _types = (str, int, float) + + __slots__ = ('name', '_shares', '_price') + def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price + @property def cost(self): return self.shares * self.price + + @property + def shares(self): + return self._shares + + @property + def price(self): + return self._price + + @shares.setter + def shares(self, value): + if not isinstance(value, self._types[1]): + raise TypeError(f'Expected {self._types[1].__name__}') + if value < 0: + raise ValueError('shares must be >= 0') + self._shares = value + + @price.setter + def price(self, value): + if not isinstance(value, self._types[2]): + raise TypeError(f'Expected {self._types[2].__name__}') + if value < 0: + raise ValueError('price must be >= 0') + self._price = value def sell(self, nshares): - if nshares > 0 and nshares < self.shares: + if nshares > 0 and nshares <= self.shares: self.shares -= nshares + @classmethod + def from_row(cls, row): + values = [func(val) for func, val in zip(cls._types, row)] + return cls(*values) + import csv def read_portfolio(filename): records = [] @@ -20,7 +54,7 @@ def read_portfolio(filename): rows = csv.reader(f) headers = next(rows) # Skip headers for row in rows: - records.append( Stock( str(row[0]), int(row[1]),float(row[2]) ) ) + records.append(Stock.from_row(row)) return records def print_portfolio(portfolio): diff --git a/tableformat.py b/tableformat.py new file mode 100644 index 00000000..b75b6ff8 --- /dev/null +++ b/tableformat.py @@ -0,0 +1,75 @@ +# tableformat.py + +# def print_table(sequence, headers): +# for column in headers: +# print('%10s ' % (column), end='') +# print() + +# print(('-'*10 + ' ')*len(headers)) + +# for item in sequence: +# for column in headers: +# print('%10s ' % (getattr(item, column)), end='') +# print() + + +# def print_table(records, fields): +# print(' '.join('%10s' % fieldname for fieldname in fields)) +# print(('-'*10 + ' ')*len(fields)) +# for record in records: +# print(' '.join('%10s' % getattr(record, fieldname) for fieldname in fields)) + +class FormatError(Exception): + pass + +class TableFormatter: + def headings(self, headers): + raise NotImplementedError() + + def row(self, rowdata): + raise NotImplementedError() + +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('%s' % h for h in headers)) + + def row(self, rowdata): + print(','.join('%s' % d for d in rowdata)) + +class HTMLTableFormatter(TableFormatter): + def headings(self, headers): + print('', end=' ') + print(' '.join(f'{h}' for h in headers), end=' ') + print('') + + def row(self, rowdata): + print('', end=' ') + print(' '.join(f'{d}' for d in rowdata), end=' ') + print('') + +def print_table(records, fields, formatter): + formatter.headings(fields) + for r in records: + rowdata = [getattr(r, fieldname) for fieldname in fields] + formatter.row(rowdata) + +def create_formatter(fmt): + formatter = { + 'text': TextTableFormatter(), + 'csv': CSVTableFormatter(), + 'html': HTMLTableFormatter() + } + if fmt in formatter: + return formatter[fmt] + else: + raise FormatError('Invalid format') + + From 46757f63bee1a0add369341e93840e96e0b8fd5f Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Tue, 22 Aug 2023 12:19:43 +0530 Subject: [PATCH 04/22] exercise 3.7 --- reader.py | 69 +++++++++++++++++++++++++++++++++----------------- stock.py | 16 ++++++++---- tableformat.py | 13 +++++++--- 3 files changed, 67 insertions(+), 31 deletions(-) diff --git a/reader.py b/reader.py index 7a820059..de4fbef3 100644 --- a/reader.py +++ b/reader.py @@ -1,22 +1,41 @@ # reader.py import csv +from abc import ABC, abstractmethod +from collections.abc import Sequence -def read_csv_as_dicts(filename, types): - ''' - Read a CSV file with column type conversion - ''' - records = [] - with open(filename, 'r') as f: - rows = csv.reader(f) - headers = next(rows) - for row in rows: - record = {name: func(val) for name, func, val in zip(headers, types, row)} - records.append(record) +class CSVParser(ABC): + def parse(self, filename): + records = [] + with open(filename) as f: + rows = csv.reader(f) + headers = next(rows) + for row in rows: + record = self.make_record(headers, row) + records.append(record) + return records + + @abstractmethod + def make_record(self, headers, row): + pass - return records -from collections.abc import Sequence +class DictCSVParser(CSVParser): + def __init__(self, types): + self.types = types + + def make_record(self, headers, row): + return {name: func(val) for name, func, val in zip(headers, self.types, row)} + + +class InstanceCSVParser(CSVParser): + def __init__(self, cls): + self.cls = cls + + def make_record(self, headers, row): + return self.cls.from_row(row) + + class DataCollection(Sequence): def __init__(self, headers): for col_name in headers: @@ -45,24 +64,28 @@ def read_csv_as_columns(filename, types): with open(filename, 'r') as f: rows = csv.reader(f) headers = next(rows) - data = DataCollection(headers) + records = DataCollection(headers) for row in rows: record = {name:func(val) for name, func, val in zip(headers, types, row)} - data.append(record) + records.append(record) + + return records + + +def read_csv_as_dicts(filename, types): + ''' + Read a CSV file with column type conversion + ''' + parser = DictCSVParser(types) + parser.parse(filename) - return data def read_csv_as_instances(filename, cls): ''' Read a CSV file into a list of instances ''' - records = [] - with open(filename) as f: - rows = csv.reader(f) - headers = next(rows) - for row in rows: - records.append(cls.from_row(row)) - return records + parser = InstanceCSVParser(cls) + parser.parse(filename) diff --git a/stock.py b/stock.py index c20ec10f..66c232e8 100644 --- a/stock.py +++ b/stock.py @@ -10,6 +10,12 @@ def __init__(self, name, shares, price): self.shares = shares self.price = price + def __repr__(self): + return f"Stock('{self.name}', {self.shares}, {self.price})" + + def __eq__(self, other): + return isinstance(other, Stock) and ((self.name, self.shares, self.price) == (other.name, other.shares, other.price)) + @property def cost(self): return self.shares * self.price @@ -17,11 +23,6 @@ def cost(self): @property def shares(self): return self._shares - - @property - def price(self): - return self._price - @shares.setter def shares(self, value): if not isinstance(value, self._types[1]): @@ -30,6 +31,9 @@ def shares(self, value): raise ValueError('shares must be >= 0') self._shares = value + @property + def price(self): + return self._price @price.setter def price(self, value): if not isinstance(value, self._types[2]): @@ -47,6 +51,8 @@ def from_row(cls, row): values = [func(val) for func, val in zip(cls._types, row)] return cls(*values) + + import csv def read_portfolio(filename): records = [] diff --git a/tableformat.py b/tableformat.py index b75b6ff8..2bfab0e3 100644 --- a/tableformat.py +++ b/tableformat.py @@ -19,15 +19,19 @@ # for record in records: # print(' '.join('%10s' % getattr(record, fieldname) for fieldname in fields)) +from abc import ABC, abstractmethod + class FormatError(Exception): pass -class TableFormatter: +class TableFormatter(ABC): + @abstractmethod def headings(self, headers): - raise NotImplementedError() + pass + @abstractmethod def row(self, rowdata): - raise NotImplementedError() + pass class TextTableFormatter(TableFormatter): def headings(self, headers): @@ -56,6 +60,9 @@ def row(self, rowdata): print('') def print_table(records, fields, formatter): + if not isinstance(formatter, TableFormatter): + raise TypeError('Expected a TableFormatter') + formatter.headings(fields) for r in records: rowdata = [getattr(r, fieldname) for fieldname in fields] From b4798cd46db97ace22ac5ca6df7d442a3f574df8 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Wed, 23 Aug 2023 11:32:39 +0530 Subject: [PATCH 05/22] exercise 3.8 --- reader.py | 6 +++--- tableformat.py | 35 +++++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/reader.py b/reader.py index de4fbef3..4f4e8427 100644 --- a/reader.py +++ b/reader.py @@ -25,7 +25,7 @@ def __init__(self, types): self.types = types def make_record(self, headers, row): - return {name: func(val) for name, func, val in zip(headers, self.types, row)} + return { name: func(val) for name, func, val in zip(headers, self.types, row) } class InstanceCSVParser(CSVParser): @@ -77,7 +77,7 @@ def read_csv_as_dicts(filename, types): Read a CSV file with column type conversion ''' parser = DictCSVParser(types) - parser.parse(filename) + return parser.parse(filename) def read_csv_as_instances(filename, cls): @@ -85,7 +85,7 @@ def read_csv_as_instances(filename, cls): Read a CSV file into a list of instances ''' parser = InstanceCSVParser(cls) - parser.parse(filename) + return parser.parse(filename) diff --git a/tableformat.py b/tableformat.py index 2bfab0e3..d030eb4f 100644 --- a/tableformat.py +++ b/tableformat.py @@ -59,6 +59,16 @@ def row(self, rowdata): print(' '.join(f'{d}' for d in rowdata), end=' ') print('') +class ColumnFormatMixin: + formats = [] + def row(self, rowdata): + rowdata = [(fmt % d) for fmt, d in zip(self.formats, rowdata)] + super().row(rowdata) + +class UpperHeadersMixin: + def headings(self, headers): + super().headings([h.upper() for h in headers]) + def print_table(records, fields, formatter): if not isinstance(formatter, TableFormatter): raise TypeError('Expected a TableFormatter') @@ -68,15 +78,20 @@ def print_table(records, fields, formatter): rowdata = [getattr(r, fieldname) for fieldname in fields] formatter.row(rowdata) -def create_formatter(fmt): - formatter = { - 'text': TextTableFormatter(), - 'csv': CSVTableFormatter(), - 'html': HTMLTableFormatter() - } - if fmt in formatter: - return formatter[fmt] +def create_formatter(fmt, column_formats=None, upper_headers=False): + if fmt == 'text': + formatter_cls = TextTableFormatter + elif fmt == 'csv': + formatter_cls = CSVTableFormatter + elif fmt == 'html': + formatter_cls = HTMLTableFormatter else: - raise FormatError('Invalid format') + raise FormatError('Unknown format %s' % fmt) - + 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() \ No newline at end of file From c34f78a75a832c69df15e6660a44257cbac3b0ac Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Wed, 23 Aug 2023 15:43:04 +0530 Subject: [PATCH 06/22] exercise 4.2 --- stock.py | 13 +++---------- validate.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 validate.py diff --git a/stock.py b/stock.py index 66c232e8..f6891d87 100644 --- a/stock.py +++ b/stock.py @@ -1,4 +1,5 @@ # stock.py +import validate class Stock: _types = (str, int, float) @@ -25,22 +26,14 @@ def shares(self): return self._shares @shares.setter def shares(self, value): - if not isinstance(value, self._types[1]): - raise TypeError(f'Expected {self._types[1].__name__}') - if value < 0: - raise ValueError('shares must be >= 0') - self._shares = value + self._shares = validate.PositiveInteger.check(value) @property def price(self): return self._price @price.setter def price(self, value): - if not isinstance(value, self._types[2]): - raise TypeError(f'Expected {self._types[2].__name__}') - if value < 0: - raise ValueError('price must be >= 0') - self._price = value + self._price = validate.PositiveFloat.check(value) def sell(self, nshares): if nshares > 0 and nshares <= self.shares: diff --git a/validate.py b/validate.py new file mode 100644 index 00000000..d8cc459e --- /dev/null +++ b/validate.py @@ -0,0 +1,50 @@ +# validate.py + +class Validator: + @classmethod + def check(cls, value): + return value + + +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) + +class Integer(Typed): + expected_type = int + +class Float(Typed): + expected_type = float + +class String(Typed): + expected_type = str + + +class Positive(Validator): + @classmethod + def check(cls, value): + if value < 0: + raise ValueError('Expected >= 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 6a6d3c8cfca4f34ff9f96a661e7b8e4db413db99 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Thu, 24 Aug 2023 15:26:37 +0530 Subject: [PATCH 07/22] exercise 4.3 --- descrip.py | 12 ++++++++++++ stock.py | 4 +++- validate.py | 9 +++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 descrip.py diff --git a/descrip.py b/descrip.py new file mode 100644 index 00000000..0e37d3df --- /dev/null +++ b/descrip.py @@ -0,0 +1,12 @@ +# descrip.py + +class Descriptor: + def __init__(self, name): + self.name = name + def __get__(self, instance, cls): + print('%s:__get__' % self.name) + def __set__(self, instance, value): + print('%s:__set__ %s' % (self.name, value)) + def __delete__(self, instance): + print('%s:__delete__' % self.name) + \ No newline at end of file diff --git a/stock.py b/stock.py index f6891d87..694331b5 100644 --- a/stock.py +++ b/stock.py @@ -4,7 +4,9 @@ class Stock: _types = (str, int, float) - __slots__ = ('name', '_shares', '_price') + name = validate.String() + shares = validate.PositiveInteger() + price = validate.PositiveFloat() def __init__(self, name, shares, price): self.name = name diff --git a/validate.py b/validate.py index d8cc459e..f1c74c68 100644 --- a/validate.py +++ b/validate.py @@ -1,10 +1,19 @@ # 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) + class Typed(Validator): expected_type = object From 5914eb956d9b06c86eb972256ddeb07a14bebd62 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Thu, 24 Aug 2023 23:13:14 +0530 Subject: [PATCH 08/22] exercise 5.1 --- reader.py | 81 ++++++++++++++++++++++++------------------------------- 1 file changed, 35 insertions(+), 46 deletions(-) diff --git a/reader.py b/reader.py index 4f4e8427..aaad35a9 100644 --- a/reader.py +++ b/reader.py @@ -1,40 +1,8 @@ # reader.py import csv -from abc import ABC, abstractmethod from collections.abc import Sequence -class CSVParser(ABC): - def parse(self, filename): - records = [] - with open(filename) as f: - rows = csv.reader(f) - headers = next(rows) - for row in rows: - record = self.make_record(headers, row) - records.append(record) - return records - - @abstractmethod - def make_record(self, headers, row): - pass - - -class DictCSVParser(CSVParser): - def __init__(self, types): - self.types = types - - def make_record(self, headers, row): - return { name: func(val) for name, func, val in zip(headers, self.types, row) } - - -class InstanceCSVParser(CSVParser): - def __init__(self, cls): - self.cls = cls - - def make_record(self, headers, row): - return self.cls.from_row(row) - class DataCollection(Sequence): def __init__(self, headers): @@ -72,25 +40,46 @@ def read_csv_as_columns(filename, types): return records -def read_csv_as_dicts(filename, types): +def csv_as_dicts(lines, types, *, headers = None): ''' - Read a CSV file with column type conversion + Convert lines of CSV data into a list of dictionaries ''' - parser = DictCSVParser(types) - return parser.parse(filename) - - -def read_csv_as_instances(filename, cls): + records = [] + rows = csv.reader(lines) + if headers is None: + headers = next(rows) + for row in rows: + record = { name: func(val) for name, func, val in zip(headers, types, row) } + records.append(record) + return records + +def csv_as_instances(lines, cls, *, headers=None): ''' - Read a CSV file into a list of instances + Convert lines of CSV data into a list of instances ''' - parser = InstanceCSVParser(cls) - return parser.parse(filename) - - - - + records = [] + rows = csv.reader(lines) + if headers is None: + headers = next(rows) + for row in rows: + record = cls.from_row(row) + records.append(record) + return records +def read_csv_as_dicts(filename, types, *, headers=None): + ''' + Read CSV data into a list of dictionaries with optional type conversion + ''' + with open(filename, 'rt') as file: + return csv_as_dicts(file, types, headers=headers) + + +def read_csv_as_instances(filename, cls, *, headers=None): + ''' + Read a CSV data into a list of instances + ''' + with open(filename, 'rt') as file: + return csv_as_instances(file, cls, headers=headers) From fec555124ab1dc4e6402756675c1584e07dcae33 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Fri, 25 Aug 2023 00:17:54 +0530 Subject: [PATCH 09/22] exercise 5.2 --- future.py | 19 +++++++++++++++++++ thread.py | 13 +++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 future.py create mode 100644 thread.py diff --git a/future.py b/future.py new file mode 100644 index 00000000..7a676408 --- /dev/null +++ b/future.py @@ -0,0 +1,19 @@ +import time +from concurrent.futures import Future +from threading import Thread + +def worker(x, y): + print('About to work') + time.sleep(20) + print('Done') + return x + y + +# Wrapper around the function to use a future +def do_work(x, y, fut): + fut.set_result(worker(x, y)) + +def caller(): + fut = Future() + Thread(target=do_work, args=(2, 3, fut)).start() + result = fut.result() + print('Got:', result) diff --git a/thread.py b/thread.py new file mode 100644 index 00000000..92259a37 --- /dev/null +++ b/thread.py @@ -0,0 +1,13 @@ +def foo(): + print('Foo') + +def bar(): + print('Bar') + +from threading import Thread + +t1 = Thread(target=foo) +t1.start() + +t2 = Thread(target=bar) +t2.start() \ No newline at end of file From d880b150edd9b4406532168636d4f4e2e3b5d870 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Fri, 25 Aug 2023 12:33:18 +0530 Subject: [PATCH 10/22] exercise 5.3 --- higher.py | 25 +++++++++++++++++++++++++ mapreduce.py | 21 +++++++++++++++++++++ reader.py | 28 +++++++++++++--------------- 3 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 higher.py create mode 100644 mapreduce.py diff --git a/higher.py b/higher.py new file mode 100644 index 00000000..759c2cd3 --- /dev/null +++ b/higher.py @@ -0,0 +1,25 @@ +def sum_squares(nums): + total = 0 + for n in nums: + total += n ** 2 + return total + +def sum_cubes(nums): + total = 0 + for n in nums: + total += n ** 3 + return total + + +def sum_map(func, nums): + total = 0 + for n in nums: + total += func(n) + return total + +def square(x): + return x * x + +# Lambda function - Anonymous function on the spot +nums = [1,2,3,4] +r = sum_map(lambda x: x*x, nums) \ No newline at end of file diff --git a/mapreduce.py b/mapreduce.py new file mode 100644 index 00000000..322600f3 --- /dev/null +++ b/mapreduce.py @@ -0,0 +1,21 @@ +def map(func, values): + result = [] + for x in values: + result.append(func(x)) + return result + +def reduce(func, values, initial=0): + result = initial + for x in values: + result = func(x, result) + return result + +def sum(x, y): + return x + y + +def square(x): + return x * x + +nums = [1, 2, 3, 4] + +result = reduce(sum, map(square, nums)) \ No newline at end of file diff --git a/reader.py b/reader.py index aaad35a9..9748a1be 100644 --- a/reader.py +++ b/reader.py @@ -40,31 +40,27 @@ def read_csv_as_columns(filename, types): return records -def csv_as_dicts(lines, types, *, headers = None): +def convert_csv(lines, converter_func, *, headers=None): ''' - Convert lines of CSV data into a list of dictionaries + Convert lines of CSV data into a list of container defined by converter_func ''' - records = [] rows = csv.reader(lines) if headers is None: headers = next(rows) - for row in rows: - record = { name: func(val) for name, func, val in zip(headers, types, row) } - records.append(record) - return records + return list(map(lambda row: converter_func(headers, row), rows)) + + +def csv_as_dicts(lines, types, *, headers = None): + ''' + Convert lines of CSV data into a list of dictionaries + ''' + return convert_csv(lines, lambda headers, row: { name: func(val) for name, func, val in zip(headers, types, row)}, headers=headers) def csv_as_instances(lines, cls, *, headers=None): ''' Convert lines of CSV data into a list of instances ''' - records = [] - rows = csv.reader(lines) - if headers is None: - headers = next(rows) - for row in rows: - record = cls.from_row(row) - records.append(record) - return records + return convert_csv(lines, lambda headers, row: cls.from_row(row),headers=headers) def read_csv_as_dicts(filename, types, *, headers=None): @@ -81,5 +77,7 @@ def read_csv_as_instances(filename, cls, *, headers=None): ''' with open(filename, 'rt') as file: return csv_as_instances(file, cls, headers=headers) + + From 5591897a2938e95dada513901a554c09547f34c0 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Fri, 25 Aug 2023 15:02:14 +0530 Subject: [PATCH 11/22] added type hints in functions in reader.py --- reader.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/reader.py b/reader.py index 9748a1be..b10f3171 100644 --- a/reader.py +++ b/reader.py @@ -2,6 +2,7 @@ import csv from collections.abc import Sequence +from stock import Stock class DataCollection(Sequence): @@ -39,8 +40,9 @@ def read_csv_as_columns(filename, types): return records +from typing import Iterable -def convert_csv(lines, converter_func, *, headers=None): +def convert_csv(lines: Iterable, converter_func: function, *, headers: list|None = None) -> list: ''' Convert lines of CSV data into a list of container defined by converter_func ''' @@ -49,8 +51,7 @@ def convert_csv(lines, converter_func, *, headers=None): headers = next(rows) return list(map(lambda row: converter_func(headers, row), rows)) - -def csv_as_dicts(lines, types, *, headers = None): +def csv_as_dicts(lines: Iterable, types: list, *, headers:list|None = None) ->list[dict]: ''' Convert lines of CSV data into a list of dictionaries ''' @@ -62,16 +63,14 @@ def csv_as_instances(lines, cls, *, headers=None): ''' return convert_csv(lines, lambda headers, row: cls.from_row(row),headers=headers) - -def read_csv_as_dicts(filename, types, *, headers=None): +def read_csv_as_dicts(filename: str, types: list, *, headers: list|None = None) -> list[dict]: ''' Read CSV data into a list of dictionaries with optional type conversion ''' with open(filename, 'rt') as file: return csv_as_dicts(file, types, headers=headers) - -def read_csv_as_instances(filename, cls, *, headers=None): +def read_csv_as_instances(filename: str, cls: Stock, *, headers: list|None = None) -> list[Stock]: ''' Read a CSV data into a list of instances ''' From 582fdff91e4b2d3996531ed07d55e4dcc7186af7 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Sun, 27 Aug 2023 01:22:24 +0530 Subject: [PATCH 12/22] exercise 5.4 and 5.5 --- closure.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ reader.py | 21 ++++++++++++++++-- typedproperty.py | 32 ++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 closure.py create mode 100644 typedproperty.py diff --git a/closure.py b/closure.py new file mode 100644 index 00000000..aec213f6 --- /dev/null +++ b/closure.py @@ -0,0 +1,58 @@ +# CLOSURE + +# Function that returns another function +# def add(x, y): +# def do_add(): +# print(f'{x} + {y} -> {x + y}') +# return do_add + +# Observe how the inner function refers to variables defined by the other function + +# Futher observe that those variables are somehow kept alive after add() has finished. + +# If an inner function is returned as a 'result', the inner function is known as a 'Closure' + +# Essential feature: A "Closure" retains the values of all variables needed for the function to run properly later on. + +# To make it work, references to the outer variables(bound variables) get carried along with the function. + +# a = add(6, 7) +# print(a.__closure__) +# print(a.__closure__[0].cell_contents) +# print(a.__closure__[1].cell_contents) + +# def sub(x, y): +# result = x - y +# def get_result(): +# return result +# return get_result + +# Closures only capture used variables +# Carefully observe: x and y are not included (not needded in the function body) + +# def counter(n=0): +# def incr(): +# nonlocal n +# n += 1 +# return n +# return incr + +# Closure variables are mutable ( can be declared by nonlocal) +# Can be used to hold mutable internal state, much like object or class + +def counter(value): + def incr(): + nonlocal value + value += 1 + return value + def decr(): + nonlocal value + value -= 1 + return value + return incr, decr + +# Above define two functions that manipulate a value + + + + diff --git a/reader.py b/reader.py index b10f3171..3f1bd826 100644 --- a/reader.py +++ b/reader.py @@ -3,6 +3,11 @@ import csv from collections.abc import Sequence from stock import Stock +import logging + +logging.basicConfig(level=logging.DEBUG, filename='reader.log') +log = logging.getLogger(__name__) + class DataCollection(Sequence): @@ -42,14 +47,26 @@ def read_csv_as_columns(filename, types): from typing import Iterable -def convert_csv(lines: Iterable, converter_func: function, *, headers: list|None = None) -> list: +def convert_csv(lines: Iterable, converter_func, *, headers: list|None = None) -> list: ''' Convert lines of CSV data into a list of container defined by converter_func ''' rows = csv.reader(lines) if headers is None: headers = next(rows) - return list(map(lambda row: converter_func(headers, row), rows)) + records = [] + for rowno, row in enumerate(rows, start=1): + try: + record = converter_func(headers, row) + records.append(record) + except ValueError as e: + # print(f"Row {rowno}: Bad row: {repr(row)}") + log.warning(f"Row {rowno}: Bad row: {repr(row)}") + # log.warning('Row %s: Bad row: %s', rowno, row) + # print(f"Error: {e}") + log.debug(f"Row {rowno}: Reason: {repr(row)}") + continue + return records def csv_as_dicts(lines: Iterable, types: list, *, headers:list|None = None) ->list[dict]: ''' diff --git a/typedproperty.py b/typedproperty.py new file mode 100644 index 00000000..f98e3507 --- /dev/null +++ b/typedproperty.py @@ -0,0 +1,32 @@ +# typedproperty.py + +def typedproperty(name, expected_type): + private_name = '_' + name + + @property + def value(self): + return getattr(self, private_name) + @value.setter + def value(self, val): + if not isinstance(val, expected_type): + raise TypeError(f'Expected {expected_type}') + setattr(self, private_name, val) + + return value + +String = lambda name: typedproperty(name, str) +Integer = lambda name: typedproperty(name, int) +Float = lambda name: typedproperty(name, float) + +# Example +if __name__ == '__main__': + class Stock: + name = String('name') + shares = Integer('shares') + price = Float('price') + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + +# Solve the challenge prroblem From 6a2105cf650730d3dbff847fd6093169fac27973 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Sun, 27 Aug 2023 02:23:33 +0530 Subject: [PATCH 13/22] exercise 5.6 --- simple.py | 19 ++++++++++++++ stock.py | 1 - testsimple.py | 28 +++++++++++++++++++++ teststock.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 simple.py create mode 100644 testsimple.py create mode 100644 teststock.py diff --git a/simple.py b/simple.py new file mode 100644 index 00000000..5cf103a5 --- /dev/null +++ b/simple.py @@ -0,0 +1,19 @@ +# simple.py + +def add(x, y): + ''' + Add x and y. + ''' + # assert isinstance(x , int) + # assert isinstance(y, int) + return x + y + +# Assertions are meant to check user inputs + +# Should validate program invarients(internal conditions that must always hold true) + +# Failure indicates a programming error and assign blame(e.g., to the caller) + +# Can be disabled (python -O) + + diff --git a/stock.py b/stock.py index 694331b5..608b85a1 100644 --- a/stock.py +++ b/stock.py @@ -3,7 +3,6 @@ class Stock: _types = (str, int, float) - name = validate.String() shares = validate.PositiveInteger() price = validate.PositiveFloat() diff --git a/testsimple.py b/testsimple.py new file mode 100644 index 00000000..10ce3402 --- /dev/null +++ b/testsimple.py @@ -0,0 +1,28 @@ +# testsimple.py + +import simple +import unittest + +# Must inherit from unittest.TestCase +class TestAdd(unittest.TestCase): + def test_simple(self): + # Test with simple integer arguments + r = simple.add(2,2) + self.assertEqual(r, 5) + + def test_str(self): + # Test with strings + r = simple.add('hello', 'world') + self.assertEqual(r, 'helloworld') + + # Each method must start with 'test...' + +# Running test +if __name__ == '__main__': + unittest.main() + +# Comments on unittest + +# Can grow to be quite complicated for large applications +# The unittest module has a huge number of options related to test runners, collection of results, and other aspects of testing +# Look at 'pytest' as an alternative diff --git a/teststock.py b/teststock.py new file mode 100644 index 00000000..7aaccde0 --- /dev/null +++ b/teststock.py @@ -0,0 +1,69 @@ +# teststock.py + +import unittest +import stock + +class TestStock(unittest.TestCase): + def test_create(self): + s = stock.Stock('GOOG', 100, 490.10) + 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.10) + self.assertEqual(s.name, 'GOOG') + self.assertEqual(s.shares, 100) + self.assertEqual(s.price, 490.1) + + def test_cost(self): + s = s = stock.Stock('GOOG', 100, 490.10) + self.assertEqual(s.cost, 49010.0) + + def test_sell(self): + s = s = stock.Stock('GOOG', 100, 490.10) + s.sell(20) + self.assertEqual(s.shares, 80) + + def test_from_row(self): + s = stock.Stock.from_row(['GOOG', '100', '490.10']) + 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.10) + self.assertEqual(repr(s), "Stock('GOOG', 100, 490.1)") + + def test_eq(self): + s = stock.Stock('GOOG', 100, 490.10) + t = stock.Stock('GOOG', 100, 490.10) + self.assertTrue(s == t) + + def test_shares_badtype(self): + s = stock.Stock('GOOG', 100, 490.10) + with self.assertRaises(TypeError): + s.shares = '55' + + def test_shares_badvalue(self): + s = stock.Stock('GOOG', 100, 490.10) + with self.assertRaises(ValueError): + s.shares = -3 + + def test_price_badtype(self): + s = stock.Stock('GOOG', 100, 490.10) + with self.assertRaises(TypeError): + s.price = '490.1' + + def test_price_badvalue(self): + s = stock.Stock('GOOG', 100, 490.10) + with self.assertRaises(ValueError): + s.price = -490.1 + + def test_bad_attribute(self): + s = stock.Stock('GOOG', 100, 490.10) + with self.assertRaises(AttributeError): + s.share = 100 + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From c75867d275dd3e6cd7a976552e5b55801cd11001 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Sun, 27 Aug 2023 02:29:09 +0530 Subject: [PATCH 14/22] added new code for stock.py and teststock.py to solve exercise 6.1 onward --- stock.py | 72 ++++++++++++++++++++-------------------------------- teststock.py | 3 ++- 2 files changed, 29 insertions(+), 46 deletions(-) diff --git a/stock.py b/stock.py index 608b85a1..226e294e 100644 --- a/stock.py +++ b/stock.py @@ -1,69 +1,51 @@ # stock.py -import validate class Stock: + __slots__ = ('name','_shares','_price') _types = (str, int, float) - name = validate.String() - shares = validate.PositiveInteger() - price = validate.PositiveFloat() - def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price def __repr__(self): - return f"Stock('{self.name}', {self.shares}, {self.price})" - + # Note: The !r format code produces the repr() string + return f'{type(self).__name__}({self.name!r}, {self.shares!r}, {self.price!r})' + def __eq__(self, other): - return isinstance(other, Stock) and ((self.name, self.shares, self.price) == (other.name, other.shares, other.price)) + return isinstance(other, Stock) and ((self.name, self.shares, self.price) == + (other.name, other.shares, other.price)) + + @classmethod + def from_row(cls, row): + values = [func(val) for func, val in zip(cls._types, row)] + return cls(*values) @property - def cost(self): - return self.shares * self.price - - @property def shares(self): return self._shares @shares.setter def shares(self, value): - self._shares = validate.PositiveInteger.check(value) + if not isinstance(value, self._types[1]): + raise TypeError(f'Expected {self._types[1].__name__}') + if value < 0: + raise ValueError('shares must be >= 0') + self._shares = value - @property + @property def price(self): return self._price @price.setter def price(self, value): - self._price = validate.PositiveFloat.check(value) - - def sell(self, nshares): - if nshares > 0 and nshares <= self.shares: - self.shares -= nshares - - @classmethod - def from_row(cls, row): - values = [func(val) for func, val in zip(cls._types, row)] - return cls(*values) - - - -import csv -def read_portfolio(filename): - records = [] - with open(filename) as f: - rows = csv.reader(f) - headers = next(rows) # Skip headers - for row in rows: - records.append(Stock.from_row(row)) - return records - -def print_portfolio(portfolio): - headers = ['name', 'shares', 'price'] - print('%10s %10s %10s' % (headers[0], headers[1], headers[2])) - print(('-' * 10 + ' ') * len(headers)) - for s in portfolio: - print('%10s %10d %10.2f' % (s.name, s.shares, s.price)) + if not isinstance(value, self._types[2]): + raise TypeError(f'Expected {self._types[2].__name__}') + if value < 0: + raise ValueError('price must be >= 0') + self._price = value + @property + def cost(self): + return self.shares * self.price - - \ No newline at end of file + def sell(self, nshares): + self.shares -= nshares diff --git a/teststock.py b/teststock.py index 7aaccde0..791289d2 100644 --- a/teststock.py +++ b/teststock.py @@ -40,6 +40,7 @@ def test_eq(self): t = stock.Stock('GOOG', 100, 490.10) self.assertTrue(s == t) + # Test for failure conditions def test_shares_badtype(self): s = stock.Stock('GOOG', 100, 490.10) with self.assertRaises(TypeError): @@ -48,7 +49,7 @@ def test_shares_badtype(self): def test_shares_badvalue(self): s = stock.Stock('GOOG', 100, 490.10) with self.assertRaises(ValueError): - s.shares = -3 + s.shares = -55 def test_price_badtype(self): s = stock.Stock('GOOG', 100, 490.10) From ac311185b50572f63850359948ed941d472a6bd5 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Sun, 27 Aug 2023 19:41:49 +0530 Subject: [PATCH 15/22] exercise 6.1 --- orig_stock.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ stock.py | 48 +++++------------------------------------------- structure.py | 24 ++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 43 deletions(-) create mode 100644 orig_stock.py create mode 100644 structure.py diff --git a/orig_stock.py b/orig_stock.py new file mode 100644 index 00000000..22c3c410 --- /dev/null +++ b/orig_stock.py @@ -0,0 +1,51 @@ +# orig_stock.py + +class Stock: + __slots__ = ('name','_shares','_price') + _types = (str, int, float) + def __init__(self, name, shares, price): + self.name = name + self.shares = shares + self.price = price + + def __repr__(self): + # Note: The !r format code produces the repr() string + return f'{type(self).__name__}({self.name!r}, {self.shares!r}, {self.price!r})' + + def __eq__(self, other): + return isinstance(other, Stock) and ((self.name, self.shares, self.price) == + (other.name, other.shares, other.price)) + + @classmethod + def from_row(cls, row): + values = [func(val) for func, val in zip(cls._types, row)] + return cls(*values) + + @property + def shares(self): + return self._shares + @shares.setter + def shares(self, value): + if not isinstance(value, self._types[1]): + raise TypeError(f'Expected {self._types[1].__name__}') + if value < 0: + raise ValueError('shares must be >= 0') + self._shares = value + + @property + def price(self): + return self._price + @price.setter + def price(self, value): + if not isinstance(value, self._types[2]): + raise TypeError(f'Expected {self._types[2].__name__}') + if value < 0: + raise ValueError('price must be >= 0') + self._price = value + + @property + def cost(self): + return self.shares * self.price + + def sell(self, nshares): + self.shares -= nshares diff --git a/stock.py b/stock.py index 226e294e..0c1da87b 100644 --- a/stock.py +++ b/stock.py @@ -1,51 +1,13 @@ # stock.py -class Stock: - __slots__ = ('name','_shares','_price') - _types = (str, int, float) - def __init__(self, name, shares, price): - self.name = name - self.shares = shares - self.price = price +from structure import Structure - def __repr__(self): - # Note: The !r format code produces the repr() string - return f'{type(self).__name__}({self.name!r}, {self.shares!r}, {self.price!r})' - - def __eq__(self, other): - return isinstance(other, Stock) and ((self.name, self.shares, self.price) == - (other.name, other.shares, other.price)) - - @classmethod - def from_row(cls, row): - values = [func(val) for func, val in zip(cls._types, row)] - return cls(*values) - - @property - def shares(self): - return self._shares - @shares.setter - def shares(self, value): - if not isinstance(value, self._types[1]): - raise TypeError(f'Expected {self._types[1].__name__}') - if value < 0: - raise ValueError('shares must be >= 0') - self._shares = value - - @property - def price(self): - return self._price - @price.setter - def price(self, value): - if not isinstance(value, self._types[2]): - raise TypeError(f'Expected {self._types[2].__name__}') - if value < 0: - raise ValueError('price must be >= 0') - self._price = value +class Stock(Structure): + _fields = ('name', 'shares', 'price') @property def cost(self): return self.shares * self.price - + def sell(self, nshares): - self.shares -= nshares + self.shares -= nshares \ No newline at end of file diff --git a/structure.py b/structure.py new file mode 100644 index 00000000..fdf847bd --- /dev/null +++ b/structure.py @@ -0,0 +1,24 @@ +# structure.py + +class Structure: + _fields = tuple() + def __init__(self, *args): + if len(args) != len(self._fields): + raise TypeError('Expected %d arguments' % len(self._fields)) + for name, arg in zip(self._fields, args): + setattr(self, name, arg) + + def __repr__(self) -> str: + return f"{type(self).__name__}({', '.join(repr(getattr(self, name)) for name in self._fields)})" + + def __setattr__(self, name, value): + if name.startswith('_') or name in self._fields: + super().__setattr__(name, value) + else: + raise AttributeError('No attribute %s' % name) + +class Stock(Structure): + _fields = ('name', 'shares', 'price') + +class Date(Structure): + _fields = ('year', 'month', 'day') \ No newline at end of file From be7ba859affa105fa82c076385138797c30171cd Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Mon, 28 Aug 2023 13:24:23 +0530 Subject: [PATCH 16/22] exercise 6.2 --- stock.py | 2 ++ structure.py | 19 ++++++++----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/stock.py b/stock.py index 0c1da87b..f96fcf3f 100644 --- a/stock.py +++ b/stock.py @@ -4,6 +4,8 @@ class Stock(Structure): _fields = ('name', 'shares', 'price') + def __init__(self, name, shares, price): + self._init() @property def cost(self): diff --git a/structure.py b/structure.py index fdf847bd..cc4ead0f 100644 --- a/structure.py +++ b/structure.py @@ -1,12 +1,15 @@ # structure.py +import sys + class Structure: _fields = tuple() - def __init__(self, *args): - if len(args) != len(self._fields): - raise TypeError('Expected %d arguments' % len(self._fields)) - for name, arg in zip(self._fields, args): - setattr(self, name, arg) + @staticmethod + def _init(): + locs = sys._getframe(1).f_locals + self = locs.pop('self') + for name, val in locs.items(): + setattr(self, name, val) def __repr__(self) -> str: return f"{type(self).__name__}({', '.join(repr(getattr(self, name)) for name in self._fields)})" @@ -16,9 +19,3 @@ def __setattr__(self, name, value): super().__setattr__(name, value) else: raise AttributeError('No attribute %s' % name) - -class Stock(Structure): - _fields = ('name', 'shares', 'price') - -class Date(Structure): - _fields = ('year', 'month', 'day') \ No newline at end of file From fd298e3ab057c4f84e7a71a2d69121b4fb14070f Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Mon, 28 Aug 2023 16:22:01 +0530 Subject: [PATCH 17/22] exercise 6.3 --- stock.py | 5 +++-- structure.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/stock.py b/stock.py index f96fcf3f..7b3140ea 100644 --- a/stock.py +++ b/stock.py @@ -3,7 +3,6 @@ from structure import Structure class Stock(Structure): - _fields = ('name', 'shares', 'price') def __init__(self, name, shares, price): self._init() @@ -12,4 +11,6 @@ def cost(self): return self.shares * self.price def sell(self, nshares): - self.shares -= nshares \ No newline at end of file + self.shares -= nshares + +Stock.set_fields() \ No newline at end of file diff --git a/structure.py b/structure.py index cc4ead0f..6715a2ba 100644 --- a/structure.py +++ b/structure.py @@ -19,3 +19,9 @@ def __setattr__(self, name, value): super().__setattr__(name, value) else: raise AttributeError('No attribute %s' % name) + + @classmethod + def set_fields(cls): + import inspect + sig = inspect.signature(cls) + cls._fields = tuple(sig.parameters) From 7d16ea7365e0ec93e5b7bd46b8a3ccdc2017962e Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Mon, 28 Aug 2023 17:42:40 +0530 Subject: [PATCH 18/22] exercise 6.4 --- stock.py | 7 ++++--- structure.py | 22 +++++++++------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/stock.py b/stock.py index 7b3140ea..10eab46b 100644 --- a/stock.py +++ b/stock.py @@ -3,8 +3,7 @@ from structure import Structure class Stock(Structure): - def __init__(self, name, shares, price): - self._init() + _fields = ('name', 'shares', 'price') @property def cost(self): @@ -13,4 +12,6 @@ def cost(self): def sell(self, nshares): self.shares -= nshares -Stock.set_fields() \ No newline at end of file +Stock.create_init() + + \ No newline at end of file diff --git a/structure.py b/structure.py index 6715a2ba..fcabefb5 100644 --- a/structure.py +++ b/structure.py @@ -1,17 +1,9 @@ # structure.py -import sys - class Structure: _fields = tuple() - @staticmethod - def _init(): - locs = sys._getframe(1).f_locals - self = locs.pop('self') - for name, val in locs.items(): - setattr(self, name, val) - def __repr__(self) -> str: + def __repr__(self): return f"{type(self).__name__}({', '.join(repr(getattr(self, name)) for name in self._fields)})" def __setattr__(self, name, value): @@ -21,7 +13,11 @@ def __setattr__(self, name, value): raise AttributeError('No attribute %s' % name) @classmethod - def set_fields(cls): - import inspect - sig = inspect.signature(cls) - cls._fields = tuple(sig.parameters) + def create_init(cls): + argstr = ', '.join(cls._fields) + init_code = f'def __init__(self, {argstr}):\n' + for name in cls._fields: + init_code += f' self.{name} = {name}\n' + locs = { } + exec(init_code, locs) + cls.__init__ = locs['__init__'] From 7f717622cf0acfd047ff59d593d189aa0c4294f3 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Tue, 29 Aug 2023 12:00:51 +0530 Subject: [PATCH 19/22] exercise 6.5 --- validate.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/validate.py b/validate.py index f1c74c68..a44bf536 100644 --- a/validate.py +++ b/validate.py @@ -57,3 +57,32 @@ class PositiveFloat(Float, Positive): class NonEmptyString(String, NonEmpty): pass +from inspect import signature + +class ValidatedFunction: + def __init__(self, func): + self._func = func + self._signature = signature(func) + self._annotations = dict(func.__annotations__) + self._retcheck = self._annotations.pop('return', None) # Return check + + def __call__(self, *args, **kwargs): + + bound = self._signature.bind(*args, **kwargs) + + for name, val in self._annotations.items(): + val.check(bound.arguments[name]) + + result = self._func(*args, **kwargs) + + if self._retcheck: + self._retcheck.check(result) + + return result + + +# 'signature' from 'inspect' module are used to get details about functions in a more useful form + +# Signatures of functions(its name, parameters and return its type) can be bound to *args and **kwargs(passed when calling the function) + +# Help performing all error checking From 1c3c328b57d9ce8970a828a0d6e2977a8530750c Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Tue, 29 Aug 2023 12:01:21 +0530 Subject: [PATCH 20/22] exercise 6.5 --- validate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/validate.py b/validate.py index a44bf536..5e99bc7b 100644 --- a/validate.py +++ b/validate.py @@ -85,4 +85,6 @@ def __call__(self, *args, **kwargs): # Signatures of functions(its name, parameters and return its type) can be bound to *args and **kwargs(passed when calling the function) +# Use 'bind()' method of 'signature' objects to bind function arguments to argument names. + # Help performing all error checking From f0a49a67f9a6405065589749b16cf915d8324813 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Tue, 29 Aug 2023 13:43:34 +0530 Subject: [PATCH 21/22] exercise 7.1 --- decorator.py | 38 ++++++++++++++++++++++++++++++++++++++ logcall.py | 10 ++++++++++ simple.py | 25 ++++++++++++++++++------- stock.py | 5 ++++- validate.py | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 decorator.py create mode 100644 logcall.py diff --git a/decorator.py b/decorator.py new file mode 100644 index 00000000..c5641cc7 --- /dev/null +++ b/decorator.py @@ -0,0 +1,38 @@ +# A decorator is a function that creates a wrapper around another function + +# The warpper is a new function that works exactly like the original function(same arguments, same return type) except that some kind of extra processing is carried out + +def add(x, y): + return x + y + +def logged(func): + # Define a wrapper function around func + def wrapper(*args, **kwargs): + print('Calling', func.__name__) + return func(*args, **kwargs) + return wrapper + +# When we create a wrapper, we often want to replace the original function with it + +# Other codes continues to use the original function name, but it is unaware that a wrapper has been injected(that's the whole point) + +# When we replace a function with a wrapper, we are usually giving the function extra functionality. This process is known as 'decoration'. We are 'decorating' a function with some extra features. + +# Whenever we see a decorader syntax, just remember that a function is getting wrapped. + +# A decorator that reports execution time +import time +def timethis(func): + def wrapper(*args, **kwargs): + start = time.time() + r = func(*args, **kwargs) + end = time.time() + print(func.__name__, end - start) + return r + return wrapper + +@timethis +def check(): + l = [] + for i in range(10000000): + l.append(i) \ No newline at end of file diff --git a/logcall.py b/logcall.py new file mode 100644 index 00000000..118a7c42 --- /dev/null +++ b/logcall.py @@ -0,0 +1,10 @@ +# logcall.py + +def logged(func): + print('Adding logged to', func.__name__) + def wrapper(*args, **kwargs): + print('Calling', func.__name__) + return func(*args, **kwargs) + return wrapper + + diff --git a/simple.py b/simple.py index 5cf103a5..d14c881c 100644 --- a/simple.py +++ b/simple.py @@ -1,12 +1,12 @@ # simple.py -def add(x, y): - ''' - Add x and y. - ''' - # assert isinstance(x , int) - # assert isinstance(y, int) - return x + y +# def add(x, y): +# ''' +# Add x and y. +# ''' +# # assert isinstance(x , int) +# # assert isinstance(y, int) +# return x + y # Assertions are meant to check user inputs @@ -17,3 +17,14 @@ def add(x, y): # Can be disabled (python -O) +from validate import Integer, validated + +@validated +def add(x: Integer, y: Integer) -> Integer: + return x + y + +@validated +def pow(x: Integer, y: Integer) -> Integer: + return x ** y + + diff --git a/stock.py b/stock.py index 10eab46b..de077936 100644 --- a/stock.py +++ b/stock.py @@ -2,6 +2,8 @@ from structure import Structure +from validate import validated, PositiveInteger + class Stock(Structure): _fields = ('name', 'shares', 'price') @@ -9,7 +11,8 @@ class Stock(Structure): def cost(self): return self.shares * self.price - def sell(self, nshares): + @validated + def sell(self, nshares: PositiveInteger): self.shares -= nshares Stock.create_init() diff --git a/validate.py b/validate.py index 5e99bc7b..80636dfc 100644 --- a/validate.py +++ b/validate.py @@ -88,3 +88,38 @@ def __call__(self, *args, **kwargs): # Use 'bind()' method of 'signature' objects to bind function arguments to argument names. # Help performing all error checking + +def validated(func): + sig = signature(func) + + # Gather the function annotations + annotations = dict(func.__annotations__) + + # Get the return annonations (if any) + retcheck = annotations.pop('return', None) + + 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 From 12caef826cb59a84964a4fadef3425070aa7fee1 Mon Sep 17 00:00:00 2001 From: Dip Kumar Das Date: Tue, 29 Aug 2023 17:40:00 +0530 Subject: [PATCH 22/22] exercise 7.2 --- decorator.py | 20 +++++++++++++++++++- logcall.py | 24 ++++++++++++++++++------ sample.py | 30 ++++++++++++++++++++++++++++++ validate.py | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 sample.py diff --git a/decorator.py b/decorator.py index c5641cc7..e47d44f2 100644 --- a/decorator.py +++ b/decorator.py @@ -21,8 +21,10 @@ def wrapper(*args, **kwargs): # Whenever we see a decorader syntax, just remember that a function is getting wrapped. # A decorator that reports execution time +from functools import wraps import time def timethis(func): + @wraps(func) def wrapper(*args, **kwargs): start = time.time() r = func(*args, **kwargs) @@ -33,6 +35,22 @@ def wrapper(*args, **kwargs): @timethis def check(): + ''' + List creation + ''' l = [] for i in range(10000000): - l.append(i) \ No newline at end of file + l.append(i) + + +# Decorator with Args +# Logging with a custom message +def logmsg(message): + def logged(func): + @wraps(func) + def wrapper(*args, **kwargs): + print(message.format(name=func.__name__)) + return func(*args, **kwargs) + return wrapper + return logged + diff --git a/logcall.py b/logcall.py index 118a7c42..69126e74 100644 --- a/logcall.py +++ b/logcall.py @@ -1,10 +1,22 @@ # logcall.py -def logged(func): - print('Adding logged to', func.__name__) - def wrapper(*args, **kwargs): - print('Calling', func.__name__) - return func(*args, **kwargs) - return wrapper +from functools import wraps +# def logged(func): +# print('Adding logged to', func.__name__) +# @wraps(func) +# def wrapper(*args, **kwargs): +# print('Calling', func.__name__) +# return func(*args, **kwargs) +# return wrapper + +def logformat(fmt): + def logged(func): + print('Adding logged to', func.__name__) + @wraps(func) + def wrapper(*args, **kwargs): + print(fmt.format(func=func)) + return func(*args, **kwargs) + return wrapper + return logged \ No newline at end of file diff --git a/sample.py b/sample.py new file mode 100644 index 00000000..03beab65 --- /dev/null +++ b/sample.py @@ -0,0 +1,30 @@ +# sample.py + +from logcall import logformat + + +logged = logformat('{func.__code__.co_filename}:{func.__name__}') + +@logged +def mul(x,y): + return x * y + +class Spam: + @logged + def instance_method(self): + pass + + @classmethod + @logged + def class_method(cls): + pass + + @staticmethod + @logged + def static_method(): + pass + + @property + @logged + def property_method(self): + pass \ No newline at end of file diff --git a/validate.py b/validate.py index 80636dfc..eb3bd95c 100644 --- a/validate.py +++ b/validate.py @@ -89,6 +89,8 @@ def __call__(self, *args, **kwargs): # Help performing all error checking +from functools import wraps + def validated(func): sig = signature(func) @@ -98,6 +100,7 @@ def validated(func): # Get the return annonations (if any) retcheck = annotations.pop('return', None) + @wraps(func) def wrapper(*args, **kwargs): bound = sig.bind(*args, **kwargs) @@ -123,3 +126,40 @@ def wrapper(*args, **kwargs): 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) + + # 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 + + return decorate +