renumbering chapters >= 19

This commit is contained in:
Luciano Ramalho
2021-09-10 12:34:39 -03:00
parent cbd13885fc
commit 4ae4096c4c
154 changed files with 7 additions and 1134 deletions

View File

@@ -0,0 +1,4 @@
Sample code for Chapter 19 - "Dynamic attributes and properties"
From the book "Fluent Python" by Luciano Ramalho (O'Reilly, 2015)
http://shop.oreilly.com/product/0636920032519.do

View File

@@ -0,0 +1,44 @@
"""
This class is inspired by the Black Knight scene in the movie
"Monty Python and the Holy Grail", where King Arthur fights the
Black Knight, slicing off his arms and legs, but the knight
refuses to concede defeat.
# tag::BLACK_KNIGHT_DEMO[]
>>> knight = BlackKnight()
>>> knight.member
next member is:
'an arm'
>>> del knight.member
BLACK KNIGHT (loses an arm) -- 'Tis but a scratch.
>>> del knight.member
BLACK KNIGHT (loses another arm) -- It's just a flesh wound.
>>> del knight.member
BLACK KNIGHT (loses a leg) -- I'm invincible!
>>> del knight.member
BLACK KNIGHT (loses another leg) -- All right, we'll call it a draw.
# end::BLACK_KNIGHT_DEMO[]
"""
# tag::BLACK_KNIGHT[]
class BlackKnight:
def __init__(self):
self.phrases = [
('an arm', "'Tis but a scratch."),
('another arm', "It's just a flesh wound."),
('a leg', "I'm invincible!"),
('another leg', "All right, we'll call it a draw.")
]
@property
def member(self):
print('next member is:')
return self.phrases[0][0]
@member.deleter
def member(self):
member, text = self.phrases.pop(0)
print(f'BLACK KNIGHT (loses {member}) -- {text}')
# end::BLACK_KNIGHT[]

View File

@@ -0,0 +1,37 @@
"""
A line item for a bulk food order has description, weight and price fields.
A ``subtotal`` method gives the total price for that line item::
>>> raisins = LineItem('Golden raisins', 10, 6.95)
>>> raisins.weight, raisins.description, raisins.price
(10, 'Golden raisins', 6.95)
>>> raisins.subtotal()
69.5
But, without validation, these public attributes can cause trouble::
# tag::LINEITEM_PROBLEM_V1[]
>>> raisins = LineItem('Golden raisins', 10, 6.95)
>>> raisins.subtotal()
69.5
>>> raisins.weight = -20 # garbage in...
>>> raisins.subtotal() # garbage out...
-139.0
# end::LINEITEM_PROBLEM_V1[]
"""
# tag::LINEITEM_V1[]
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
# end::LINEITEM_V1[]

View File

@@ -0,0 +1,63 @@
"""
A line item for a bulk food order has description, weight and price fields::
>>> raisins = LineItem('Golden raisins', 10, 6.95)
>>> raisins.weight, raisins.description, raisins.price
(10, 'Golden raisins', 6.95)
A ``subtotal`` method gives the total price for that line item::
>>> raisins.subtotal()
69.5
The weight of a ``LineItem`` must be greater than 0::
>>> raisins.weight = -20
Traceback (most recent call last):
...
ValueError: value must be > 0
No change was made::
>>> raisins.weight
10
The check is also performed on instantiation::
>>> walnuts = LineItem('walnuts', 0, 10.00)
Traceback (most recent call last):
...
ValueError: value must be > 0
The proteced attribute can still be accessed if needed for some reason, such as
white box testing)::
>>> raisins._LineItem__weight
10
"""
# tag::LINEITEM_V2[]
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight # <1>
self.price = price
def subtotal(self):
return self.weight * self.price
@property # <2>
def weight(self): # <3>
return self.__weight # <4>
@weight.setter # <5>
def weight(self, value):
if value > 0:
self.__weight = value # <6>
else:
raise ValueError('value must be > 0') # <7>
# end::LINEITEM_V2[]

View File

@@ -0,0 +1,64 @@
"""
A line item for a bulk food order has description, weight and price fields::
>>> raisins = LineItem('Golden raisins', 10, 6.95)
>>> raisins.weight, raisins.description, raisins.price
(10, 'Golden raisins', 6.95)
A ``subtotal`` method gives the total price for that line item::
>>> raisins.subtotal()
69.5
The weight of a ``LineItem`` must be greater than 0::
>>> raisins.weight = -20
Traceback (most recent call last):
...
ValueError: value must be > 0
No change was made::
>>> raisins.weight
10
The check is also performed on instantiation::
>>> walnuts = LineItem('walnuts', 0, 10.00)
Traceback (most recent call last):
...
ValueError: value must be > 0
The proteced attribute can still be accessed if needed for some reason, such as
white box testing)::
>>> raisins._LineItem__weight
10
"""
# tag::LINEITEM_V2B[]
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
def get_weight(self): # <1>
return self.__weight
def set_weight(self, value): # <2>
if value > 0:
self.__weight = value
else:
raise ValueError('value must be > 0')
weight = property(get_weight, set_weight) # <3>
# end::LINEITEM_V2B[]

View File

@@ -0,0 +1,69 @@
"""
A line item for a bulk food order has description, weight and price fields::
>>> raisins = LineItem('Golden raisins', 10, 6.95)
>>> raisins.weight, raisins.description, raisins.price
(10, 'Golden raisins', 6.95)
A ``subtotal`` method gives the total price for that line item::
>>> raisins.subtotal()
69.5
The weight of a ``LineItem`` must be greater than 0::
>>> raisins.weight = -20
Traceback (most recent call last):
...
ValueError: value must be > 0
No change was made::
>>> raisins.weight
10
The value of the attributes managed by the properties are stored in
instance attributes, created in each ``LineItem`` instance::
# tag::LINEITEM_V2_PROP_DEMO[]
>>> nutmeg = LineItem('Moluccan nutmeg', 8, 13.95)
>>> nutmeg.weight, nutmeg.price # <1>
(8, 13.95)
>>> sorted(vars(nutmeg).items()) # <2>
[('description', 'Moluccan nutmeg'), ('price', 13.95), ('weight', 8)]
# end::LINEITEM_V2_PROP_DEMO[]
"""
# tag::LINEITEM_V2_PROP_FACTORY_FUNCTION[]
def quantity(storage_name): # <1>
def qty_getter(instance): # <2>
return instance.__dict__[storage_name] # <3>
def qty_setter(instance, value): # <4>
if value > 0:
instance.__dict__[storage_name] = value # <5>
else:
raise ValueError('value must be > 0')
return property(qty_getter, qty_setter) # <6>
# end::LINEITEM_V2_PROP_FACTORY_FUNCTION[]
# tag::LINEITEM_V2_PROP_CLASS[]
class LineItem:
weight = quantity('weight') # <1>
price = quantity('price') # <2>
def __init__(self, description, weight, price):
self.description = description
self.weight = weight # <3>
self.price = price
def subtotal(self):
return self.weight * self.price # <4>
# end::LINEITEM_V2_PROP_CLASS[]

View File

@@ -0,0 +1,23 @@
"""
Example of property documentation
>>> f = Foo()
>>> f.bar = 77
>>> f.bar
77
>>> Foo.bar.__doc__
'The bar attribute'
"""
# tag::DOC_PROPERTY[]
class Foo:
@property
def bar(self):
'''The bar attribute'''
return self.__dict__['bar']
@bar.setter
def bar(self, value):
self.__dict__['bar'] = value
# end::DOC_PROPERTY[]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env python3
import shelve
from schedule_v2 import DB_NAME, CONFERENCE, load_db
from schedule_v2 import DbRecord, Event
with shelve.open(DB_NAME) as db:
if CONFERENCE not in db:
load_db(db)
DbRecord.set_db(db)
event = DbRecord.fetch('event.33950')
print(event)
print(event.venue)
print(event.venue.name)
for spkr in event.speakers:
print(f'{spkr.serial}:', spkr.name)
print(repr(Event.venue))
event2 = DbRecord.fetch('event.33451')
print(event2)
print(event2.fetch)
print(event2.venue)

View File

@@ -0,0 +1,65 @@
"""
explore0.py: Script to explore the OSCON schedule feed
# tag::EXPLORE0_DEMO[]
>>> import json
>>> raw_feed = json.load(open('data/osconfeed.json'))
>>> feed = FrozenJSON(raw_feed) # <1>
>>> len(feed.Schedule.speakers) # <2>
357
>>> feed.keys()
dict_keys(['Schedule'])
>>> sorted(feed.Schedule.keys()) # <3>
['conferences', 'events', 'speakers', 'venues']
>>> for key, value in sorted(feed.Schedule.items()): # <4>
... print(f'{len(value):3} {key}')
...
1 conferences
484 events
357 speakers
53 venues
>>> feed.Schedule.speakers[-1].name # <5>
'Carina C. Zona'
>>> talk = feed.Schedule.events[40]
>>> type(talk) # <6>
<class 'explore0.FrozenJSON'>
>>> talk.name
'There *Will* Be Bugs'
>>> talk.speakers # <7>
[3471, 5199]
>>> talk.flavor # <8>
Traceback (most recent call last):
...
KeyError: 'flavor'
# end::EXPLORE0_DEMO[]
"""
# tag::EXPLORE0[]
from collections import abc
class FrozenJSON:
"""A read-only façade for navigating a JSON-like object
using attribute notation
"""
def __init__(self, mapping):
self.__data = dict(mapping) # <1>
def __getattr__(self, name): # <2>
try:
return getattr(self.__data, name) # <3>
except AttributeError:
return FrozenJSON.build(self.__data[name]) # <4>
@classmethod
def build(cls, obj): # <5>
if isinstance(obj, abc.Mapping): # <6>
return cls(obj)
elif isinstance(obj, abc.MutableSequence): # <7>
return [cls.build(item) for item in obj]
else: # <8>
return obj
# end::EXPLORE0[]

View File

@@ -0,0 +1,78 @@
"""
explore1.py: Script to explore the OSCON schedule feed
>>> import json
>>> raw_feed = json.load(open('data/osconfeed.json'))
>>> feed = FrozenJSON(raw_feed)
>>> len(feed.Schedule.speakers)
357
>>> sorted(feed.Schedule.keys())
['conferences', 'events', 'speakers', 'venues']
>>> for key, value in sorted(feed.Schedule.items()):
... print(f'{len(value):3} {key}')
...
1 conferences
484 events
357 speakers
53 venues
>>> feed.Schedule.speakers[-1].name
'Carina C. Zona'
>>> talk = feed.Schedule.events[40]
>>> type(talk)
<class 'explore1.FrozenJSON'>
>>> talk.name
'There *Will* Be Bugs'
>>> talk.speakers
[3471, 5199]
>>> talk.flavor
Traceback (most recent call last):
...
KeyError: 'flavor'
Handle keywords by appending a `_`.
# tag::EXPLORE1_DEMO[]
>>> grad = FrozenJSON({'name': 'Jim Bo', 'class': 1982})
>>> grad.name
'Jim Bo'
>>> grad.class_
1982
# end::EXPLORE1_DEMO[]
"""
from collections import abc
import keyword
class FrozenJSON:
"""A read-only façade for navigating a JSON-like object
using attribute notation
"""
# tag::EXPLORE1[]
def __init__(self, mapping):
self.__data = {}
for key, value in mapping.items():
if keyword.iskeyword(key): # <1>
key += '_'
self.__data[key] = value
# end::EXPLORE1[]
def __getattr__(self, name):
if hasattr(self.__data, name):
return getattr(self.__data, name)
else:
return FrozenJSON.build(self.__data[name])
@classmethod
def build(cls, obj):
if isinstance(obj, abc.Mapping):
return cls(obj)
elif isinstance(obj, abc.MutableSequence):
return [cls.build(item) for item in obj]
else: # <8>
return obj

View File

@@ -0,0 +1,54 @@
"""
explore2.py: Script to explore the OSCON schedule feed
>>> import json
>>> raw_feed = json.load(open('data/osconfeed.json'))
>>> feed = FrozenJSON(raw_feed)
>>> len(feed.Schedule.speakers)
357
>>> sorted(feed.Schedule.keys())
['conferences', 'events', 'speakers', 'venues']
>>> feed.Schedule.speakers[-1].name
'Carina C. Zona'
>>> talk = feed.Schedule.events[40]
>>> talk.name
'There *Will* Be Bugs'
>>> talk.speakers
[3471, 5199]
>>> talk.flavor
Traceback (most recent call last):
...
KeyError: 'flavor'
"""
# tag::EXPLORE2[]
from collections import abc
import keyword
class FrozenJSON:
"""A read-only façade for navigating a JSON-like object
using attribute notation
"""
def __new__(cls, arg): # <1>
if isinstance(arg, abc.Mapping):
return super().__new__(cls) # <2>
elif isinstance(arg, abc.MutableSequence): # <3>
return [cls(item) for item in arg]
else:
return arg
def __init__(self, mapping):
self.__data = {}
for key, value in mapping.items():
if keyword.iskeyword(key):
key += '_'
self.__data[key] = value
def __getattr__(self, name):
if hasattr(self.__data, name):
return getattr(self.__data, name)
else:
return FrozenJSON(self.__data[name]) # <4>
# end::EXPLORE2[]

View File

@@ -0,0 +1,32 @@
{ "Schedule":
{ "conferences": [{"serial": 115 }],
"events": [
{ "serial": 34505,
"name": "Why Schools Don´t Use Open Source to Teach Programming",
"event_type": "40-minute conference session",
"time_start": "2014-07-23 11:30:00",
"time_stop": "2014-07-23 12:10:00",
"venue_serial": 1462,
"description": "Aside from the fact that high school programming...",
"website_url": "http://oscon.com/oscon2014/public/schedule/detail/34505",
"speakers": [157509],
"categories": ["Education"] }
],
"speakers": [
{ "serial": 157509,
"name": "Robert Lefkowitz",
"photo": null,
"url": "http://sharewave.com/",
"position": "CTO",
"affiliation": "Sharewave",
"twitter": "sharewaveteam",
"bio": "Robert ´r0ml´ Lefkowitz is the CTO at Sharewave, a startup..." }
],
"venues": [
{ "serial": 1462,
"name": "F151",
"category": "Conference Venues" }
]
}
}

View File

@@ -0,0 +1,20 @@
>>> import json
>>> with open('data/osconfeed.json') as fp:
... feed = json.load(fp) # <1>
>>> sorted(feed['Schedule'].keys()) # <2>
['conferences', 'events', 'speakers', 'venues']
>>> for key, value in sorted(feed['Schedule'].items()):
... print(f'{len(value):3} {key}') # <3>
...
1 conferences
484 events
357 speakers
53 venues
>>> feed['Schedule']['speakers'][-1]['name'] # <4>
'Carina C. Zona'
>>> feed['Schedule']['speakers'][-1]['serial'] # <5>
141590
>>> feed['Schedule']['events'][40]['name']
'There *Will* Be Bugs'
>>> feed['Schedule']['events'][40]['speakers'] # <6>
[3471, 5199]

View File

@@ -0,0 +1,2 @@
#!/bin/bash
pytest --doctest-modules $2 $1 test_$1

View File

@@ -0,0 +1,39 @@
"""
schedule_v1.py: traversing OSCON schedule data
# tag::SCHEDULE1_DEMO[]
>>> records = load(JSON_PATH) # <1>
>>> speaker = records['speaker.3471'] # <2>
>>> speaker # <3>
<Record serial=3471>
>>> speaker.name, speaker.twitter # <4>
('Anna Martelli Ravenscroft', 'annaraven')
# end::SCHEDULE1_DEMO[]
"""
# tag::SCHEDULE1[]
import json
JSON_PATH = 'data/osconfeed.json'
class Record:
def __init__(self, **kwargs):
self.__dict__.update(kwargs) # <1>
def __repr__(self):
cls_name = self.__class__.__name__
return f'<{cls_name} serial={self.serial!r}>' # <2>
def load(path=JSON_PATH):
records = {} # <3>
with open(path) as fp:
raw_data = json.load(fp) # <4>
for collection, raw_records in raw_data['Schedule'].items(): # <5>
record_type = collection[:-1] # <6>
for raw_record in raw_records:
key = f'{record_type}.{raw_record["serial"]}' # <7>
records[key] = Record(**raw_record) # <8>
return records
# end::SCHEDULE1[]

View File

@@ -0,0 +1,76 @@
"""
schedule_v2.py: property to get venue linked to an event
# tag::SCHEDULE2_DEMO[]
>>> event = Record.fetch('event.33950') # <1>
>>> event # <2>
<Event 'There *Will* Be Bugs'>
>>> event.venue # <3>
<Record serial=1449>
>>> event.venue.name # <4>
'Portland 251'
>>> event.venue_serial # <5>
1449
# end::SCHEDULE2_DEMO[]
"""
# tag::SCHEDULE2_RECORD[]
import inspect # <1>
import json
JSON_PATH = 'data/osconfeed.json'
class Record:
__index = None # <2>
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __repr__(self):
cls_name = self.__class__.__name__
return f'<{cls_name} serial={self.serial!r}>'
@staticmethod # <3>
def fetch(key):
if Record.__index is None: # <4>
Record.__index = load()
return Record.__index[key] # <5>
# end::SCHEDULE2_RECORD[]
# tag::SCHEDULE2_EVENT[]
class Event(Record): # <1>
def __repr__(self):
if hasattr(self, 'name'): # <2>
cls_name = self.__class__.__name__
return f'<{cls_name} {self.name!r}>'
else:
return super().__repr__()
@property
def venue(self):
key = f'venue.{self.venue_serial}'
return self.__class__.fetch(key) # <3>
# end::SCHEDULE2_EVENT[]
# tag::SCHEDULE2_LOAD[]
def load(path=JSON_PATH):
records = {}
with open(path) as fp:
raw_data = json.load(fp)
for collection, raw_records in raw_data['Schedule'].items():
record_type = collection[:-1] # <1>
cls_name = record_type.capitalize() # <2>
cls = globals().get(cls_name, Record) # <3>
if inspect.isclass(cls) and issubclass(cls, Record): # <4>
factory = cls # <5>
else:
factory = Record # <6>
for raw_record in raw_records: # <7>
key = f'{record_type}.{raw_record["serial"]}'
records[key] = factory(**raw_record) # <8>
return records
# end::SCHEDULE2_LOAD[]

View File

@@ -0,0 +1,86 @@
"""
schedule_v3.py: property to get list of event speakers
>>> event = Record.fetch('event.33950')
>>> event
<Event 'There *Will* Be Bugs'>
>>> event.venue
<Record serial=1449>
>>> event.venue_serial
1449
>>> event.venue.name
'Portland 251'
# tag::SCHEDULE3_DEMO[]
>>> for spkr in event.speakers:
... print(f'{spkr.serial}: {spkr.name}')
3471: Anna Martelli Ravenscroft
5199: Alex Martelli
# end::SCHEDULE3_DEMO[]
"""
import inspect
import json
JSON_PATH = 'data/osconfeed.json'
class Record:
__index = None
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __repr__(self):
cls_name = self.__class__.__name__
return f'<{cls_name} serial={self.serial!r}>'
@staticmethod
def fetch(key):
if Record.__index is None:
Record.__index = load()
return Record.__index[key]
class Event(Record):
def __repr__(self):
if hasattr(self, 'name'): # <3>
cls_name = self.__class__.__name__
return f'<{cls_name} {self.name!r}>'
else:
return super().__repr__() # <4>
@property
def venue(self):
key = f'venue.{self.venue_serial}'
return self.__class__.fetch(key)
# tag::SCHEDULE3_SPEAKERS[]
@property
def speakers(self):
spkr_serials = self.__dict__['speakers'] # <1>
fetch = self.__class__.fetch
return [fetch(f'speaker.{key}')
for key in spkr_serials] # <2>
# end::SCHEDULE3_SPEAKERS[]
def load(path=JSON_PATH):
records = {}
with open(path) as fp:
raw_data = json.load(fp)
for collection, raw_records in raw_data['Schedule'].items():
record_type = collection[:-1]
cls_name = record_type.capitalize()
cls = globals().get(cls_name, Record)
if inspect.isclass(cls) and issubclass(cls, Record):
factory = cls
else:
factory = Record
for raw_record in raw_records:
key = f'{record_type}.{raw_record["serial"]}'
records[key] = factory(**raw_record)
return records

View File

@@ -0,0 +1,94 @@
"""
schedule_v4.py: homegrown cached property for speakers
>>> event = Record.fetch('event.33950')
# tag::SCHEDULE4_DEMO[]
>>> event # <1>
<Event 'There *Will* Be Bugs'>
>>> event.venue # <2>
<Record serial=1449>
>>> event.venue.name # <3>
'Portland 251'
>>> for spkr in event.speakers: # <4>
... print(f'{spkr.serial}: {spkr.name}')
...
3471: Anna Martelli Ravenscroft
5199: Alex Martelli
# end::SCHEDULE4_DEMO[]
"""
import json
import inspect
JSON_PATH = 'data/osconfeed.json'
class Record:
__index = None
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __repr__(self):
cls_name = self.__class__.__name__
return f'<{cls_name} serial={self.serial!r}>'
@staticmethod
def fetch(key):
if Record.__index is None:
Record.__index = load()
return Record.__index[key]
# tag::SCHEDULE4_INIT[]
class Event(Record):
def __init__(self, **kwargs):
self.__speaker_objs = None
super().__init__(**kwargs)
# end::SCHEDULE4_INIT[]
def __repr__(self):
if hasattr(self, 'name'):
cls_name = self.__class__.__name__
return f'<{cls_name} {self.name!r}>'
else:
return super().__repr__() # <4>
@property
def venue(self):
key = f'venue.{self.venue_serial}'
return self.__class__.fetch(key)
# tag::SCHEDULE4_CACHE[]
@property
def speakers(self):
if self.__speaker_objs is None:
spkr_serials = self.__dict__['speakers']
fetch = self.__class__.fetch
self.__speaker_objs = [fetch(f'speaker.{key}')
for key in spkr_serials]
return self.__speaker_objs
# end::SCHEDULE4_CACHE[]
def load(path=JSON_PATH):
records = {}
with open(path) as fp:
raw_data = json.load(fp)
for collection, raw_records in raw_data['Schedule'].items():
record_type = collection[:-1]
cls_name = record_type.capitalize()
cls = globals().get(cls_name, Record)
if inspect.isclass(cls) and issubclass(cls, Record):
factory = cls
else:
factory = Record
for raw_record in raw_records:
key = f'{record_type}.{raw_record["serial"]}'
records[key] = factory(**raw_record)
return records

View File

@@ -0,0 +1,86 @@
"""
schedule_v4.py: homegrown cached property for speakers
>>> event = Record.fetch('event.33950')
# tag::SCHEDULE4_DEMO[]
>>> event # <1>
<Event 'There *Will* Be Bugs'>
>>> event.venue # <2>
<Record serial=1449>
>>> event.venue.name # <3>
'Portland 251'
>>> for spkr in event.speakers: # <4>
... print(f'{spkr.serial}: {spkr.name}')
3471: Anna Martelli Ravenscroft
5199: Alex Martelli
# end::SCHEDULE4_DEMO[]
"""
import inspect
import json
JSON_PATH = 'data/osconfeed.json'
class Record:
__index = None
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __repr__(self):
cls_name = self.__class__.__name__
return f'<{cls_name} serial={self.serial!r}>'
@staticmethod
def fetch(key):
if Record.__index is None:
Record.__index = load()
return Record.__index[key]
class Event(Record):
def __repr__(self):
if hasattr(self, 'name'):
cls_name = self.__class__.__name__
return f'<{cls_name} {self.name!r}>'
else:
return super().__repr__() # <4>
@property
def venue(self):
key = f'venue.{self.venue_serial}'
return self.__class__.fetch(key)
# tag::SCHEDULE4_HASATTR_CACHE[]
@property
def speakers(self):
if not hasattr(self, '__speaker_objs'): # <1>
spkr_serials = self.__dict__['speakers']
fetch = self.__class__.fetch
self.__speaker_objs = [fetch(f'speaker.{key}')
for key in spkr_serials]
return self.__speaker_objs # <2>
# end::SCHEDULE4_HASATTR_CACHE[]
def load(path=JSON_PATH):
records = {}
with open(path) as fp:
raw_data = json.load(fp)
for collection, raw_records in raw_data['Schedule'].items():
record_type = collection[:-1]
cls_name = record_type.capitalize()
cls = globals().get(cls_name, Record)
if inspect.isclass(cls) and issubclass(cls, Record):
factory = cls
else:
factory = Record
for raw_record in raw_records:
key = f'{record_type}.{raw_record["serial"]}'
records[key] = factory(**raw_record)
return records

View File

@@ -0,0 +1,89 @@
"""
schedule_v5.py: cached properties using functools
>>> event = Record.fetch('event.33950')
>>> event
<Event 'There *Will* Be Bugs'>
>>> event.venue
<Record serial=1449>
>>> event.venue_serial
1449
>>> event.venue.name
'Portland 251'
# tag::SCHEDULE3_DEMO[]
>>> for spkr in event.speakers: # <3>
... print(f'{spkr.serial}: {spkr.name}')
...
3471: Anna Martelli Ravenscroft
5199: Alex Martelli
# end::SCHEDULE3_DEMO[]
"""
import json
import inspect
from functools import cached_property, cache
JSON_PATH = 'data/osconfeed.json'
class Record:
__index = None
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __repr__(self):
cls_name = self.__class__.__name__
return f'<{cls_name} serial={self.serial!r}>'
@staticmethod
def fetch(key):
if Record.__index is None:
Record.__index = load()
return Record.__index[key]
class Event(Record):
def __repr__(self):
if hasattr(self, 'name'):
cls_name = self.__class__.__name__
return f'<{cls_name} {self.name!r}>'
else:
return super().__repr__()
# tag::SCHEDULE5_CACHED_PROPERTY[]
@cached_property
def venue(self):
key = f'venue.{self.venue_serial}'
return self.__class__.fetch(key)
# end::SCHEDULE5_CACHED_PROPERTY[]
# tag::SCHEDULE5_PROPERTY_OVER_CACHE[]
@property # <1>
@cache # <2>
def speakers(self):
spkr_serials = self.__dict__['speakers']
fetch = self.__class__.fetch
return [fetch(f'speaker.{key}')
for key in spkr_serials]
# end::SCHEDULE5_PROPERTY_OVER_CACHE[]
def load(path=JSON_PATH):
records = {}
with open(path) as fp:
raw_data = json.load(fp)
for collection, raw_records in raw_data['Schedule'].items():
record_type = collection[:-1]
cls_name = record_type.capitalize()
cls = globals().get(cls_name, Record)
if inspect.isclass(cls) and issubclass(cls, Record):
factory = cls
else:
factory = Record
for raw_record in raw_records:
key = f'{record_type}.{raw_record["serial"]}'
records[key] = factory(**raw_record)
return records

View File

@@ -0,0 +1,24 @@
import pytest
import schedule_v1 as schedule
@pytest.yield_fixture
def records():
yield schedule.load(schedule.JSON_PATH)
def test_load(records):
assert len(records) == 895
def test_record_attr_access():
rec = schedule.Record(spam=99, eggs=12)
assert rec.spam == 99
assert rec.eggs == 12
def test_venue_record(records):
venue = records['venue.1469']
assert venue.serial == 1469
assert venue.name == 'Exhibit Hall C'

View File

@@ -0,0 +1,48 @@
import pytest
import schedule_v2 as schedule
@pytest.yield_fixture
def records():
yield schedule.load(schedule.JSON_PATH)
def test_load(records):
assert len(records) == 895
def test_record_attr_access():
rec = schedule.Record(spam=99, eggs=12)
assert rec.spam == 99
assert rec.eggs == 12
def test_venue_record(records):
venue = records['venue.1469']
assert venue.serial == 1469
assert venue.name == 'Exhibit Hall C'
def test_fetch_speaker_record():
speaker = schedule.Record.fetch('speaker.3471')
assert speaker.name == 'Anna Martelli Ravenscroft'
def test_event_type():
event = schedule.Record.fetch('event.33950')
assert type(event) is schedule.Event
assert repr(event) == "<Event 'There *Will* Be Bugs'>"
def test_event_repr():
event = schedule.Record.fetch('event.33950')
assert repr(event) == "<Event 'There *Will* Be Bugs'>"
event2 = schedule.Event(serial=77, kind='show')
assert repr(event2) == '<Event serial=77>'
def test_event_venue():
event = schedule.Record.fetch('event.33950')
assert event.venue_serial == 1449
assert event.venue == schedule.Record.fetch('venue.1449')
assert event.venue.name == 'Portland 251'

View File

@@ -0,0 +1,59 @@
import pytest
import schedule_v3 as schedule
@pytest.yield_fixture
def records():
yield schedule.load(schedule.JSON_PATH)
def test_load(records):
assert len(records) == 895
def test_record_attr_access():
rec = schedule.Record(spam=99, eggs=12)
assert rec.spam == 99
assert rec.eggs == 12
def test_venue_record(records):
venue = records['venue.1469']
assert venue.serial == 1469
assert venue.name == 'Exhibit Hall C'
def test_fetch_speaker_record():
speaker = schedule.Record.fetch('speaker.3471')
assert speaker.name == 'Anna Martelli Ravenscroft'
def test_event_type():
event = schedule.Record.fetch('event.33950')
assert type(event) is schedule.Event
assert repr(event) == "<Event 'There *Will* Be Bugs'>"
def test_event_repr():
event = schedule.Record.fetch('event.33950')
assert repr(event) == "<Event 'There *Will* Be Bugs'>"
event2 = schedule.Event(serial=77, kind='show')
assert repr(event2) == '<Event serial=77>'
def test_event_venue():
event = schedule.Record.fetch('event.33950')
assert event.venue_serial == 1449
assert event.venue == schedule.Record.fetch('venue.1449')
assert event.venue.name == 'Portland 251'
def test_event_speakers():
event = schedule.Record.fetch('event.33950')
assert len(event.speakers) == 2
anna, alex = [schedule.Record.fetch(f'speaker.{s}') for s in (3471, 5199)]
assert event.speakers == [anna, alex]
def test_event_no_speakers():
event = schedule.Record.fetch('event.36848')
assert event.speakers == []

View File

@@ -0,0 +1,59 @@
import pytest
import schedule_v4 as schedule
@pytest.yield_fixture
def records():
yield schedule.load(schedule.JSON_PATH)
def test_load(records):
assert len(records) == 895
def test_record_attr_access():
rec = schedule.Record(spam=99, eggs=12)
assert rec.spam == 99
assert rec.eggs == 12
def test_venue_record(records):
venue = records['venue.1469']
assert venue.serial == 1469
assert venue.name == 'Exhibit Hall C'
def test_fetch_speaker_record():
speaker = schedule.Record.fetch('speaker.3471')
assert speaker.name == 'Anna Martelli Ravenscroft'
def test_event_type():
event = schedule.Record.fetch('event.33950')
assert type(event) is schedule.Event
assert repr(event) == "<Event 'There *Will* Be Bugs'>"
def test_event_repr():
event = schedule.Record.fetch('event.33950')
assert repr(event) == "<Event 'There *Will* Be Bugs'>"
event2 = schedule.Event(serial=77, kind='show')
assert repr(event2) == '<Event serial=77>'
def test_event_venue():
event = schedule.Record.fetch('event.33950')
assert event.venue_serial == 1449
assert event.venue == schedule.Record.fetch('venue.1449')
assert event.venue.name == 'Portland 251'
def test_event_speakers():
event = schedule.Record.fetch('event.33950')
assert len(event.speakers) == 2
anna, alex = [schedule.Record.fetch(f'speaker.{s}') for s in (3471, 5199)]
assert event.speakers == [anna, alex]
def test_event_no_speakers():
event = schedule.Record.fetch('event.36848')
assert event.speakers == []

View File

@@ -0,0 +1,59 @@
import pytest
import schedule_v5 as schedule
@pytest.yield_fixture
def records():
yield schedule.load(schedule.JSON_PATH)
def test_load(records):
assert len(records) == 895
def test_record_attr_access():
rec = schedule.Record(spam=99, eggs=12)
assert rec.spam == 99
assert rec.eggs == 12
def test_venue_record(records):
venue = records['venue.1469']
assert venue.serial == 1469
assert venue.name == 'Exhibit Hall C'
def test_fetch_speaker_record():
speaker = schedule.Record.fetch('speaker.3471')
assert speaker.name == 'Anna Martelli Ravenscroft'
def test_event_type():
event = schedule.Record.fetch('event.33950')
assert type(event) is schedule.Event
assert repr(event) == "<Event 'There *Will* Be Bugs'>"
def test_event_repr():
event = schedule.Record.fetch('event.33950')
assert repr(event) == "<Event 'There *Will* Be Bugs'>"
event2 = schedule.Event(serial=77, kind='show')
assert repr(event2) == '<Event serial=77>'
def test_event_venue():
event = schedule.Record.fetch('event.33950')
assert event.venue_serial == 1449
assert event.venue == schedule.Record.fetch('venue.1449')
assert event.venue.name == 'Portland 251'
def test_event_speakers():
event = schedule.Record.fetch('event.33950')
assert len(event.speakers) == 2
anna, alex = [schedule.Record.fetch(f'speaker.{s}') for s in (3471, 5199)]
assert event.speakers == [anna, alex]
def test_event_no_speakers():
event = schedule.Record.fetch('event.36848')
assert event.speakers == []

View File

@@ -0,0 +1,10 @@
# pseudo-code for object construction
def make(the_class, some_arg):
new_object = the_class.__new__(some_arg)
if isinstance(new_object, the_class):
the_class.__init__(new_object, some_arg)
return new_object
# the following statements are roughly equivalent
x = Foo('bar')
x = make(Foo, 'bar')