update from Atlas
This commit is contained in:
parent
1edb0cd05b
commit
0618105a47
64
descriptors/bulkfood_v2b.py
Normal file
64
descriptors/bulkfood_v2b.py
Normal 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
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# BEGIN 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
|
58
descriptors/bulkfood_v2prop.py
Normal file
58
descriptors/bulkfood_v2prop.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# BEGIN LINEITEM_V2_PROP
|
||||
def quantity(storage_name): # <1>
|
||||
|
||||
@property # <2>
|
||||
def new_prop(self):
|
||||
return self.__dict__[storage_name] # <3>
|
||||
|
||||
@new_prop.setter
|
||||
def new_prop(self, value):
|
||||
if value > 0:
|
||||
self.__dict__[storage_name] = value # <4>
|
||||
else:
|
||||
raise ValueError('value must be > 0')
|
||||
|
||||
return new_prop # <5>
|
||||
|
||||
|
||||
class LineItem:
|
||||
weight = quantity('weight') # <6>
|
||||
price = quantity('price') # <7>
|
||||
|
||||
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_V2_PROP
|
76
descriptors/bulkfood_v4prop.py
Normal file
76
descriptors/bulkfood_v4prop.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""
|
||||
|
||||
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 descriptors are stored in
|
||||
alternate attributes, created by the descriptors in each ``LineItem``
|
||||
instance::
|
||||
|
||||
>>> raisins = LineItem('Golden raisins', 10, 6.95)
|
||||
>>> sorted(dir(raisins)) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
|
||||
[..., '_quantity_0', '_quantity_1', 'description',
|
||||
'price', 'subtotal', 'weight']
|
||||
>>> raisins._quantity_0
|
||||
10
|
||||
>>> raisins._quantity_1
|
||||
6.95
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# BEGIN LINEITEM_V4_PROP
|
||||
def quantity(): # <1>
|
||||
try:
|
||||
quantity.counter += 1 # <2>
|
||||
except AttributeError:
|
||||
quantity.counter = 0 # <3>
|
||||
|
||||
storage_name = '_{}_{}'.format('quantity', quantity.counter) # <4>
|
||||
|
||||
@property # <5>
|
||||
def prop(self):
|
||||
return getattr(self, storage_name)
|
||||
|
||||
@prop.setter
|
||||
def prop(self, value):
|
||||
if value > 0:
|
||||
setattr(self, storage_name, value)
|
||||
else:
|
||||
raise ValueError('value must be > 0')
|
||||
|
||||
return prop # <6>
|
||||
|
||||
|
||||
class LineItem:
|
||||
weight = quantity() # <7>
|
||||
price = quantity()
|
||||
|
||||
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_V4_PROP
|
75
descriptors/bulkfood_v5.py
Normal file
75
descriptors/bulkfood_v5.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""
|
||||
|
||||
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 descriptors are stored in
|
||||
alternate attributes, created by the descriptors in each ``LineItem``
|
||||
instance::
|
||||
|
||||
>>> raisins = LineItem('Golden raisins', 10, 6.95)
|
||||
>>> dir(raisins) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
|
||||
['_NonBlank_0', '_Quantity_0', '_Quantity_1', '__class__', ...
|
||||
'description', 'price', 'subtotal', 'weight']
|
||||
>>> raisins._Quantity_0
|
||||
10
|
||||
>>> raisins._Quantity_1
|
||||
6.95
|
||||
|
||||
If the descriptor is accessed in the class, the descriptor object is
|
||||
returned:
|
||||
|
||||
>>> LineItem.price # doctest: +ELLIPSIS
|
||||
<model_v5.Quantity object at 0x...>
|
||||
>>> br_nuts = LineItem('Brazil nuts', 10, 34.95)
|
||||
>>> br_nuts.price
|
||||
34.95
|
||||
|
||||
The `NonBlank` descriptor prevents empty or blank strings to be used
|
||||
for the description:
|
||||
|
||||
>>> br_nuts.description = ' '
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: value cannot be empty or blank
|
||||
|
||||
|
||||
"""
|
||||
|
||||
# BEGIN LINEITEM_V5
|
||||
import model_v5 as model # <1>
|
||||
|
||||
|
||||
class LineItem:
|
||||
description = model.NonBlank() # <2>
|
||||
weight = model.Quantity()
|
||||
price = model.Quantity()
|
||||
|
||||
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_V5
|
166
descriptors/descriptorkinds.py
Normal file
166
descriptors/descriptorkinds.py
Normal file
@ -0,0 +1,166 @@
|
||||
|
||||
|
||||
"""
|
||||
Data descriptor (a.k.a. overriding or enforced descriptor):
|
||||
|
||||
>>> o = Model()
|
||||
>>> o.data # doctest: +ELLIPSIS
|
||||
DataDescriptor.__get__() invoked with args:
|
||||
self = <descriptorkinds.DataDescriptor object at 0x...>
|
||||
instance = <descriptorkinds.Model object at 0x...>
|
||||
owner = <class 'descriptorkinds.Model'>
|
||||
>>> Model.data # doctest: +ELLIPSIS
|
||||
DataDescriptor.__get__() invoked with args:
|
||||
self = <descriptorkinds.DataDescriptor object at 0x...>
|
||||
instance = None
|
||||
owner = <class 'descriptorkinds.Model'>
|
||||
|
||||
|
||||
A data descriptor cannot be shadowed by assigning to an instance:
|
||||
|
||||
>>> o.data = 7 # doctest: +ELLIPSIS
|
||||
DataDescriptor.__set__() invoked with args:
|
||||
self = <descriptorkinds.DataDescriptor object at 0x...>
|
||||
instance = <descriptorkinds.Model object at 0x...>
|
||||
value = 7
|
||||
>>> o.data # doctest: +ELLIPSIS
|
||||
DataDescriptor.__get__() invoked with args:
|
||||
self = <descriptorkinds.DataDescriptor object at 0x...>
|
||||
instance = <descriptorkinds.Model object at 0x...>
|
||||
owner = <class 'descriptorkinds.Model'>
|
||||
|
||||
|
||||
Not even by poking the attribute into the instance ``__dict__``:
|
||||
|
||||
>>> o.__dict__['data'] = 8
|
||||
>>> o.data # doctest: +ELLIPSIS
|
||||
DataDescriptor.__get__() invoked with args:
|
||||
self = <descriptorkinds.DataDescriptor object at 0x...>
|
||||
instance = <descriptorkinds.Model object at 0x...>
|
||||
owner = <class 'descriptorkinds.Model'>
|
||||
|
||||
|
||||
Data descriptor without ``__get__``:
|
||||
|
||||
>>> o.data_no_get # doctest: +ELLIPSIS
|
||||
<descriptorkinds.DataDescriptorNoGet object at 0x...>
|
||||
>>> Model.data_no_get # doctest: +ELLIPSIS
|
||||
<descriptorkinds.DataDescriptorNoGet object at 0x...>
|
||||
>>> o.data_no_get = 7 # doctest: +ELLIPSIS
|
||||
DataDescriptorNoGet.__set__() invoked with args:
|
||||
self = <descriptorkinds.DataDescriptorNoGet object at 0x...>
|
||||
instance = <descriptorkinds.Model object at 0x...>
|
||||
value = 7
|
||||
>>> o.data_no_get # doctest: +ELLIPSIS
|
||||
<descriptorkinds.DataDescriptorNoGet object at 0x...>
|
||||
|
||||
|
||||
Poking the attribute into the instance ``__dict__`` means you can read the new
|
||||
value for the attribute, but setting it still triggers ``__set__``:
|
||||
|
||||
>>> o.__dict__['data_no_get'] = 8
|
||||
>>> o.data_no_get
|
||||
8
|
||||
>>> o.data_no_get = 7 # doctest: +ELLIPSIS
|
||||
DataDescriptorNoGet.__set__() invoked with args:
|
||||
self = <descriptorkinds.DataDescriptorNoGet object at 0x...>
|
||||
instance = <descriptorkinds.Model object at 0x...>
|
||||
value = 7
|
||||
>>> o.data_no_get # doctest: +ELLIPSIS
|
||||
8
|
||||
|
||||
|
||||
Non-data descriptor (a.k.a. non-overriding or shadowable descriptor):
|
||||
|
||||
>>> o = Model()
|
||||
>>> o.non_data # doctest: +ELLIPSIS
|
||||
NonDataDescriptor.__get__() invoked with args:
|
||||
self = <descriptorkinds.NonDataDescriptor object at 0x...>
|
||||
instance = <descriptorkinds.Model object at 0x...>
|
||||
owner = <class 'descriptorkinds.Model'>
|
||||
>>> Model.non_data # doctest: +ELLIPSIS
|
||||
NonDataDescriptor.__get__() invoked with args:
|
||||
self = <descriptorkinds.NonDataDescriptor object at 0x...>
|
||||
instance = None
|
||||
owner = <class 'descriptorkinds.Model'>
|
||||
|
||||
|
||||
A non-data descriptor can be shadowed by assigning to an instance:
|
||||
|
||||
>>> o.non_data = 7
|
||||
>>> o.non_data
|
||||
7
|
||||
|
||||
|
||||
Methods are non-data descriptors:
|
||||
|
||||
>>> o.spam # doctest: +ELLIPSIS
|
||||
<bound method Model.spam of <descriptorkinds.Model object at 0x...>>
|
||||
>>> Model.spam # doctest: +ELLIPSIS
|
||||
<function Model.spam at 0x...>
|
||||
>>> o.spam() # doctest: +ELLIPSIS
|
||||
Model.spam() invoked with arg:
|
||||
self = <descriptorkinds.Model object at 0x...>
|
||||
>>> o.spam = 7
|
||||
>>> o.spam
|
||||
7
|
||||
|
||||
|
||||
No descriptor type survives being overwritten on the class itself:
|
||||
|
||||
>>> Model.data = 1
|
||||
>>> o.data
|
||||
1
|
||||
>>> Model.data_no_get = 2
|
||||
>>> o.data_no_get
|
||||
2
|
||||
>>> Model.non_data = 3
|
||||
>>> o.non_data
|
||||
7
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class DataDescriptor:
|
||||
"a.k.a. overriding or enforced descriptor"
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
print('DataDescriptor.__get__() invoked with args:')
|
||||
print(' self = ', self)
|
||||
print(' instance = ', instance)
|
||||
print(' owner = ', owner)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
print('DataDescriptor.__set__() invoked with args:')
|
||||
print(' self = ', self)
|
||||
print(' instance = ', instance)
|
||||
print(' value = ', value)
|
||||
|
||||
|
||||
class DataDescriptorNoGet:
|
||||
|
||||
def __set__(self, instance, value):
|
||||
print('DataDescriptorNoGet.__set__() invoked with args:')
|
||||
print(' self = ', self)
|
||||
print(' instance = ', instance)
|
||||
print(' value = ', value)
|
||||
|
||||
|
||||
class NonDataDescriptor:
|
||||
"a.k.a. non-overriding or shadowable descriptor"
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
print('NonDataDescriptor.__get__() invoked with args:')
|
||||
print(' self = ', self)
|
||||
print(' instance = ', instance)
|
||||
print(' owner = ', owner)
|
||||
|
||||
|
||||
class Model:
|
||||
data = DataDescriptor()
|
||||
data_no_get = DataDescriptorNoGet()
|
||||
non_data = NonDataDescriptor()
|
||||
|
||||
def spam(self):
|
||||
print('Model.spam() invoked with arg:')
|
||||
print(' self = ', self)
|
60
descriptors/doc_descriptor.py
Normal file
60
descriptors/doc_descriptor.py
Normal file
@ -0,0 +1,60 @@
|
||||
|
||||
"""
|
||||
|
||||
>>> f = Foo()
|
||||
>>> f.bar = 77
|
||||
>>> f.bar
|
||||
77
|
||||
>>> Foo.bar.__doc__
|
||||
'The "bar" attribute'
|
||||
>>> import pydoc
|
||||
>>> pydoc.getdoc(Foo.bazz)
|
||||
'The "bazz" attribute'
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def doc_descriptor_wrapper_factory(descriptor):
|
||||
wrapper_cls_name = 'DocDescriptorWrapper'
|
||||
wrapper_cls_attrs = descriptor.__dict__.copy()
|
||||
wrapper_cls_attrs['__slots__'] = ['_wrapped']
|
||||
|
||||
def wrapped_getter(self):
|
||||
"the wrapped descriptor instance"
|
||||
return self._wrapped
|
||||
|
||||
def wrapper_repr(self):
|
||||
return '<{} {!r}>'.format(wrapper_cls_name, self.__doc__)
|
||||
|
||||
wrapper_cls_attrs['wrapped'] = property(wrapped_getter)
|
||||
wrapper_cls_attrs['__repr__'] = wrapper_repr
|
||||
wrapper_cls = type(wrapper_cls_name, (), wrapper_cls_attrs)
|
||||
wrapper = wrapper_cls()
|
||||
wrapper._wrapped = descriptor
|
||||
return wrapper
|
||||
|
||||
|
||||
class DocDescriptor:
|
||||
"""A documented descriptor"""
|
||||
|
||||
def __init__(self, documentation):
|
||||
self.__doc__ = documentation
|
||||
cls_name = self.__class__.__name__
|
||||
self.storage_name = '_{}_{:x}'.format(cls_name, id(self))
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""The __get__ method"""
|
||||
if instance is None:
|
||||
return doc_descriptor_wrapper_factory(self)
|
||||
else:
|
||||
return getattr(instance, self.storage_name)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
setattr(instance, self.storage_name, value)
|
||||
|
||||
|
||||
class Foo:
|
||||
"""The "Foo" class"""
|
||||
|
||||
bar = DocDescriptor('The "bar" attribute')
|
||||
bazz = DocDescriptor('The "bazz" attribute')
|
52
descriptors/model_v5.py
Normal file
52
descriptors/model_v5.py
Normal file
@ -0,0 +1,52 @@
|
||||
# BEGIN MODEL_V5
|
||||
import abc
|
||||
|
||||
|
||||
class AutoStorage: # <1>
|
||||
__counter = 0
|
||||
|
||||
def __init__(self):
|
||||
cls = self.__class__
|
||||
prefix = cls.__name__
|
||||
index = cls.__counter
|
||||
self.storage_name = '_{}_{}'.format(prefix, index)
|
||||
cls.__counter += 1
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
else:
|
||||
return getattr(instance, self.storage_name)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
setattr(instance, self.storage_name, value) # <2>
|
||||
|
||||
|
||||
class Validated(abc.ABC, AutoStorage): # <3>
|
||||
|
||||
def __set__(self, instance, value):
|
||||
value = self.validate(instance, value) # <4>
|
||||
super().__set__(instance, value) # <5>
|
||||
|
||||
@abc.abstractmethod
|
||||
def validate(self, instance, value): # <6>
|
||||
"""return validated value or raise ValueError"""
|
||||
|
||||
|
||||
class Quantity(Validated): # <7>
|
||||
|
||||
def validate(self, instance, value):
|
||||
if value <= 0:
|
||||
raise ValueError('value must be > 0')
|
||||
return value
|
||||
|
||||
|
||||
class NonBlank(Validated):
|
||||
|
||||
def validate(self, instance, value):
|
||||
value = value.strip()
|
||||
if len(value) == 0:
|
||||
raise ValueError('value cannot be empty or blank')
|
||||
return value # <8>
|
||||
|
||||
# END MODEL_V5
|
48
metaprog/blackknight.py
Normal file
48
metaprog/blackknight.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""
|
||||
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.
|
||||
|
||||
# BEGIN 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
|
||||
"""
|
||||
|
||||
# BEGIN BLACK_KNIGHT
|
||||
class BlackKnight:
|
||||
|
||||
def __init__(self):
|
||||
self.members = ['an arm', 'another arm',
|
||||
'a leg', 'another leg']
|
||||
self.phrases = ["'Tis but a scratch.",
|
||||
"It's just a flesh wound.",
|
||||
"I'm invincible!",
|
||||
"All right, we'll call it a draw."]
|
||||
|
||||
@property
|
||||
def member(self):
|
||||
print('next member is:')
|
||||
return self.members[0]
|
||||
|
||||
@member.deleter
|
||||
def member(self):
|
||||
text = 'BLACK KNIGHT (loses {})\n-- {}'
|
||||
print(text.format(self.members.pop(0), self.phrases.pop(0)))
|
||||
# END BLACK_KNIGHT
|
23
metaprog/doc_property.py
Normal file
23
metaprog/doc_property.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""
|
||||
Example of property documentation
|
||||
|
||||
>>> f = Foo()
|
||||
>>> f.bar = 77
|
||||
>>> f.bar
|
||||
77
|
||||
>>> Foo.bar.__doc__
|
||||
'The bar attribute'
|
||||
"""
|
||||
|
||||
# BEGIN 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
|
@ -1,30 +1,30 @@
|
||||
"""
|
||||
explore1.py: Script to explore the OSCON schedule feed
|
||||
|
||||
# BEGIN EXPLORE1_DEMO
|
||||
>>> from osconfeed import load
|
||||
>>> raw_feed = load()
|
||||
>>> feed = FrozenJSON(raw_feed)
|
||||
>>> sorted(feed.Schedule.keys())
|
||||
>>> feed = FrozenJSON(raw_feed) # <1>
|
||||
>>> len(feed.Schedule.speakers) # <2>
|
||||
357
|
||||
>>> sorted(feed.Schedule.keys()) # <3>
|
||||
['conferences', 'events', 'speakers', 'venues']
|
||||
>>> for key, value in sorted(feed.Schedule.items()):
|
||||
... print('{:3} {}'.format(len(value), key))
|
||||
...
|
||||
1 conferences
|
||||
484 events
|
||||
357 speakers
|
||||
53 venues
|
||||
>>> feed.Schedule.speakers[-1].name
|
||||
>>> feed.Schedule.speakers[-1].name # <4>
|
||||
'Carina C. Zona'
|
||||
>>> carina = feed.Schedule.speakers[-1]
|
||||
>>> carina.twitter
|
||||
'cczona'
|
||||
>>> feed.Schedule.events[40].name
|
||||
>>> talk = feed.Schedule.events[40] # <5>
|
||||
>>> talk.name
|
||||
'There *Will* Be Bugs'
|
||||
>>> feed.Schedule.events[40].speakers
|
||||
>>> talk.speakers # <6>
|
||||
[3471, 5199]
|
||||
>>> talk.flavor # <7>
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
KeyError: 'flavor'
|
||||
|
||||
# END EXPLORE1_DEMO
|
||||
"""
|
||||
|
||||
# BEGIN EXPLORE1
|
||||
from collections import abc
|
||||
|
||||
|
||||
@ -34,19 +34,20 @@ class FrozenJSON:
|
||||
"""
|
||||
|
||||
def __init__(self, mapping):
|
||||
self._data = dict(mapping)
|
||||
self._data = dict(mapping) # <1>
|
||||
|
||||
def __getattr__(self, name):
|
||||
def __getattr__(self, name): # <2>
|
||||
if hasattr(self._data, name):
|
||||
return getattr(self._data, name)
|
||||
return getattr(self._data, name) # <3>
|
||||
else:
|
||||
return FrozenJSON.build(self._data[name])
|
||||
return FrozenJSON.build(self._data[name]) # <4>
|
||||
|
||||
@classmethod
|
||||
def build(cls, obj):
|
||||
if isinstance(obj, abc.Mapping):
|
||||
def build(cls, obj): # <5>
|
||||
if isinstance(obj, abc.Mapping): # <6>
|
||||
return cls(obj)
|
||||
elif isinstance(obj, abc.MutableSequence):
|
||||
elif isinstance(obj, abc.MutableSequence): # <7>
|
||||
return [cls.build(item) for item in obj]
|
||||
else:
|
||||
else: # <8>
|
||||
return obj
|
||||
# END EXPLORE1
|
||||
|
@ -4,27 +4,25 @@ explore2.py: Script to explore the OSCON schedule feed
|
||||
>>> from osconfeed import load
|
||||
>>> raw_feed = load()
|
||||
>>> 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('{:3} {}'.format(len(value), key))
|
||||
...
|
||||
1 conferences
|
||||
484 events
|
||||
357 speakers
|
||||
53 venues
|
||||
>>> feed.Schedule.speakers[-1].name
|
||||
'Carina C. Zona'
|
||||
>>> carina = feed.Schedule.speakers[-1]
|
||||
>>> carina.twitter
|
||||
'cczona'
|
||||
>>> feed.Schedule.events[40].name
|
||||
>>> talk = feed.Schedule.events[40]
|
||||
>>> talk.name
|
||||
'There *Will* Be Bugs'
|
||||
>>> feed.Schedule.events[40].speakers
|
||||
>>> talk.speakers
|
||||
[3471, 5199]
|
||||
>>> talk.flavor
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
KeyError: 'flavor'
|
||||
|
||||
"""
|
||||
|
||||
# BEGIN EXPLORE2
|
||||
from collections import abc
|
||||
|
||||
|
||||
@ -33,11 +31,11 @@ class FrozenJSON:
|
||||
using attribute notation
|
||||
"""
|
||||
|
||||
def __new__(cls, arg):
|
||||
def __new__(cls, arg): # <1>
|
||||
if isinstance(arg, abc.Mapping):
|
||||
return super().__new__(cls)
|
||||
elif isinstance(arg, abc.MutableSequence):
|
||||
return [FrozenJSON(item) for item in arg]
|
||||
return super().__new__(cls) # <2>
|
||||
elif isinstance(arg, abc.MutableSequence): # <3>
|
||||
return [cls(item) for item in arg]
|
||||
else:
|
||||
return arg
|
||||
|
||||
@ -48,4 +46,5 @@ class FrozenJSON:
|
||||
if hasattr(self._data, name):
|
||||
return getattr(self._data, name)
|
||||
else:
|
||||
return FrozenJSON(self._data[name])
|
||||
return FrozenJSON(self._data[name]) # <4>
|
||||
# END EXPLORE2
|
||||
|
@ -2,12 +2,12 @@
|
||||
{ "conferences": [{"serial": 115 }],
|
||||
"events": [
|
||||
{ "serial": 34505,
|
||||
"name": "Why Schools Don't Use Open Source to Teach Programming",
|
||||
"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 curricula often require proprietary IDEs, they also don't involve examining any source code from Open Source software projects. What changes would be required in programming curricula to incorporate Open Source? And is that a desirable objective?",
|
||||
"description": "Aside from the fact that high school programming...",
|
||||
"website_url": "http://oscon.com/oscon2014/public/schedule/detail/34505",
|
||||
"speakers": [157509],
|
||||
"categories": ["Education"] }
|
||||
@ -20,7 +20,7 @@
|
||||
"position": "CTO",
|
||||
"affiliation": "Sharewave",
|
||||
"twitter": "sharewaveteam",
|
||||
"bio": "Robert 'r0ml' Lefkowitz is the CTO at Sharewave, a startup building an investor management portal. This year, he was a resident at the winter session of Hacker School. He is a Distinguished Engineer of the ACM." }
|
||||
"bio": "Robert ´r0ml´ Lefkowitz is the CTO at Sharewave, a startup..." }
|
||||
],
|
||||
"venues": [
|
||||
{ "serial": 1462,
|
||||
|
@ -1,43 +1,49 @@
|
||||
"""
|
||||
osconfeed.py: Script to download the OSCON schedule feed
|
||||
|
||||
>>> feed = load()
|
||||
>>> sorted(feed['Schedule'].keys())
|
||||
# BEGIN OSCONFEED_DEMO
|
||||
|
||||
>>> feed = load() # <1>
|
||||
>>> sorted(feed['Schedule'].keys()) # <2>
|
||||
['conferences', 'events', 'speakers', 'venues']
|
||||
>>> for key, value in sorted(feed['Schedule'].items()):
|
||||
... print('{:3} {}'.format(len(value), key))
|
||||
... print('{:3} {}'.format(len(value), key)) # <3>
|
||||
...
|
||||
1 conferences
|
||||
484 events
|
||||
357 speakers
|
||||
53 venues
|
||||
>>> feed['Schedule']['speakers'][-1]['name']
|
||||
>>> feed['Schedule']['speakers'][-1]['name'] # <4>
|
||||
'Carina C. Zona'
|
||||
>>> carina = feed['Schedule']['speakers'][-1]
|
||||
>>> carina['twitter']
|
||||
'cczona'
|
||||
>>> feed['Schedule']['speakers'][-1]['serial'] # <5>
|
||||
141590
|
||||
>>> feed['Schedule']['events'][40]['name']
|
||||
'There *Will* Be Bugs'
|
||||
>>> feed['Schedule']['events'][40]['speakers']
|
||||
>>> feed['Schedule']['events'][40]['speakers'] # <6>
|
||||
[3471, 5199]
|
||||
|
||||
|
||||
# END OSCONFEED_DEMO
|
||||
"""
|
||||
|
||||
# BEGIN OSCONFEED
|
||||
from urllib.request import urlopen
|
||||
import warnings
|
||||
import os
|
||||
import json
|
||||
|
||||
URL = 'http://www.oreilly.com/pub/sc/osconfeed'
|
||||
JSON_NAME = 'data/osconfeed.json'
|
||||
JSON = 'data/osconfeed.json'
|
||||
|
||||
|
||||
def load():
|
||||
if not os.path.exists(JSON_NAME):
|
||||
msg = 'downloading {} to {}'.format(URL, JSON_NAME)
|
||||
warnings.warn(msg)
|
||||
with urlopen(URL) as remote, open(JSON_NAME, 'wb') as local:
|
||||
if not os.path.exists(JSON):
|
||||
msg = 'downloading {} to {}'.format(URL, JSON)
|
||||
warnings.warn(msg) # <1>
|
||||
with urlopen(URL) as remote, open(JSON, 'wb') as local: # <2>
|
||||
local.write(remote.read())
|
||||
|
||||
with open(JSON_NAME) as fp:
|
||||
return json.load(fp)
|
||||
with open(JSON) as fp:
|
||||
return json.load(fp) # <3>
|
||||
|
||||
# END OSCONFEED
|
||||
|
@ -1,19 +1,26 @@
|
||||
"""
|
||||
schedule1.py: traversing OSCON schedule data
|
||||
|
||||
# BEGIN SCHEDULE1_DEMO
|
||||
>>> import shelve
|
||||
>>> db = shelve.open(DB_NAME)
|
||||
>>> if CONFERENCE not in db: load_db(db)
|
||||
>>> event = db['event.33950']
|
||||
>>> speaker = db['speaker.3471']
|
||||
>>> speaker.name
|
||||
>>> db = shelve.open(DB_NAME) # <1>
|
||||
>>> if CONFERENCE not in db: # <2>
|
||||
... load_db(db) # <3>
|
||||
...
|
||||
>>> speaker = db['speaker.3471'] # <4>
|
||||
>>> type(speaker) # <5>
|
||||
<class 'schedule1.Record'>
|
||||
>>> speaker.name # <6>
|
||||
'Anna Martelli Ravenscroft'
|
||||
>>> speaker.twitter
|
||||
'annaraven'
|
||||
>>> db.close()
|
||||
>>> db.close() # <7>
|
||||
|
||||
# END SCHEDULE1_DEMO
|
||||
|
||||
"""
|
||||
|
||||
# BEGIN SCHEDULE1
|
||||
import warnings
|
||||
|
||||
import osconfeed
|
||||
@ -23,15 +30,18 @@ CONFERENCE = 'conference.115'
|
||||
|
||||
|
||||
class Record:
|
||||
def __init__(self, mapping):
|
||||
self.__dict__.update(mapping)
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs) # <1>
|
||||
|
||||
|
||||
def load_db(db):
|
||||
raw_data = osconfeed.load()
|
||||
raw_data = osconfeed.load() # <2>
|
||||
warnings.warn('loading ' + DB_NAME)
|
||||
for collection, rec_list in raw_data['Schedule'].items():
|
||||
rec_type = collection[:-1]
|
||||
for fields in rec_list:
|
||||
key = '{}.{}'.format(rec_type, fields['serial'])
|
||||
db[key] = Record(fields)
|
||||
for collection, rec_list in raw_data['Schedule'].items(): # <3>
|
||||
record_type = collection[:-1] # <4>
|
||||
for record in rec_list:
|
||||
key = '{}.{}'.format(record_type, record['serial']) # <5>
|
||||
record['serial'] = key # <6>
|
||||
db[key] = Record(**record) # <7>
|
||||
|
||||
# END SCHEDULE1
|
||||
|
@ -4,82 +4,128 @@ schedule2.py: traversing OSCON schedule data
|
||||
>>> import shelve
|
||||
>>> db = shelve.open(DB_NAME)
|
||||
>>> if CONFERENCE not in db: load_db(db)
|
||||
>>> DbRecord.set_db(db)
|
||||
>>> event = Event.get('event.33950')
|
||||
>>> event
|
||||
|
||||
# BEGIN SCHEDULE2_DEMO
|
||||
|
||||
>>> DbRecord.set_db(db) # <1>
|
||||
>>> event = DbRecord.fetch('event.33950') # <2>
|
||||
>>> event # <3>
|
||||
<Event 'There *Will* Be Bugs'>
|
||||
>>> event.speakers[0].name
|
||||
'Anna Martelli Ravenscroft'
|
||||
>>> event.venue # <4>
|
||||
<DbRecord serial='venue.1449'>
|
||||
>>> event.venue.name # <5>
|
||||
'Portland 251'
|
||||
>>> for spkr in event.speakers: # <6>
|
||||
... print('{0.serial}: {0.name}'.format(spkr))
|
||||
...
|
||||
speaker.3471: Anna Martelli Ravenscroft
|
||||
speaker.5199: Alex Martelli
|
||||
|
||||
# END SCHEDULE2_DEMO
|
||||
|
||||
>>> db.close()
|
||||
|
||||
"""
|
||||
|
||||
# BEGIN SCHEDULE2_RECORD
|
||||
import warnings
|
||||
import inspect
|
||||
import inspect # <1>
|
||||
|
||||
import osconfeed
|
||||
|
||||
DB_NAME = 'data/schedule2_db'
|
||||
DB_NAME = 'data/schedule2_db' # <2>
|
||||
CONFERENCE = 'conference.115'
|
||||
|
||||
|
||||
class Record:
|
||||
def __init__(self, mapping):
|
||||
self.__dict__.update(mapping)
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other): # <3>
|
||||
if isinstance(other, Record):
|
||||
return self.__dict__ == other.__dict__
|
||||
else:
|
||||
return NotImplemented
|
||||
# END SCHEDULE2_RECORD
|
||||
|
||||
|
||||
# BEGIN SCHEDULE2_DBRECORD
|
||||
class MissingDatabaseError(RuntimeError):
|
||||
"""Raised when a database is required but was not set.""" # <1>
|
||||
|
||||
|
||||
class DbRecord(Record): # <2>
|
||||
|
||||
_db = None # <3>
|
||||
|
||||
@staticmethod # <4>
|
||||
def set_db(db):
|
||||
DbRecord._db = db # <5>
|
||||
|
||||
@staticmethod # <6>
|
||||
def get_db():
|
||||
return DbRecord._db
|
||||
|
||||
@classmethod # <7>
|
||||
def fetch(cls, ident):
|
||||
db = cls.get_db()
|
||||
try:
|
||||
return db[ident] # <8>
|
||||
except TypeError:
|
||||
if db is None: # <9>
|
||||
msg = "database not set; call '{}.set_db(my_db)'"
|
||||
raise MissingDatabaseError(msg.format(cls.__name__))
|
||||
else: # <10>
|
||||
raise
|
||||
|
||||
def __repr__(self):
|
||||
if hasattr(self, 'name'):
|
||||
ident = repr(self.name)
|
||||
if hasattr(self, 'serial'): # <11>
|
||||
cls_name = self.__class__.__name__
|
||||
return '<{} serial={!r}>'.format(cls_name, self.serial)
|
||||
else:
|
||||
ident = 'object at ' + hex(id(self))
|
||||
cls_name = self.__class__.__name__
|
||||
return '<{} {}>'.format(cls_name, ident)
|
||||
return super().__repr__() # <12>
|
||||
# END SCHEDULE2_DBRECORD
|
||||
|
||||
|
||||
class DbRecord(Record):
|
||||
|
||||
@classmethod
|
||||
def set_db(cls, db):
|
||||
cls._db = db
|
||||
|
||||
@classmethod
|
||||
def get(cls, ident):
|
||||
return cls._db[ident]
|
||||
|
||||
|
||||
class Event(DbRecord):
|
||||
# BEGIN SCHEDULE2_EVENT
|
||||
class Event(DbRecord): # <1>
|
||||
|
||||
@property
|
||||
def venue(self):
|
||||
key = self.venue_serial
|
||||
return self._db['venue.{}'.format(key)]
|
||||
key = 'venue.{}'.format(self.venue_serial)
|
||||
return self.fetch(key) # <2>
|
||||
|
||||
@property
|
||||
def speakers(self):
|
||||
spkr_serials = self.__dict__['speakers']
|
||||
if not hasattr(self, '_speaker_refs'):
|
||||
self._speaker_refs = [self._db['speaker.{}'.format(key)]
|
||||
for key in spkr_serials]
|
||||
return self._speaker_refs
|
||||
if not hasattr(self, '_speaker_objs'): # <3>
|
||||
spkr_serials = self.__dict__['speakers'] # <4>
|
||||
self._speaker_objs = [self.fetch('speaker.{}'.format(key))
|
||||
for key in spkr_serials] # <5>
|
||||
return self._speaker_objs # <6>
|
||||
|
||||
def __repr__(self):
|
||||
if hasattr(self, 'name'): # <7>
|
||||
cls_name = self.__class__.__name__
|
||||
return '<{} {!r}>'.format(cls_name, self.name)
|
||||
else:
|
||||
return super().__repr__() # <8>
|
||||
# END SCHEDULE2_EVENT
|
||||
|
||||
|
||||
# BEGIN SCHEDULE2_LOAD
|
||||
def load_db(db):
|
||||
raw_data = osconfeed.load()
|
||||
warnings.warn('loading ' + DB_NAME)
|
||||
for collection, rec_list in raw_data['Schedule'].items():
|
||||
rec_type = collection[:-1]
|
||||
for fields in rec_list:
|
||||
cls_name = rec_type.capitalize()
|
||||
cls = globals().get(cls_name, Record)
|
||||
if inspect.isclass(cls) and issubclass(cls, Record):
|
||||
record = cls(fields)
|
||||
else:
|
||||
Record(fields)
|
||||
key = '{}.{}'.format(rec_type, fields['serial'])
|
||||
db[key] = record
|
||||
record_type = collection[:-1] # <1>
|
||||
cls_name = record_type.capitalize() # <2>
|
||||
cls = globals().get(cls_name, DbRecord) # <3>
|
||||
if inspect.isclass(cls) and issubclass(cls, DbRecord): # <4>
|
||||
factory = cls # <5>
|
||||
else:
|
||||
factory = DbRecord # <6>
|
||||
for record in rec_list: # <7>
|
||||
key = '{}.{}'.format(record_type, record['serial'])
|
||||
record['serial'] = key
|
||||
db[key] = factory(**record) # <>
|
||||
# END SCHEDULE2_LOAD
|
||||
|
@ -13,7 +13,7 @@ def db():
|
||||
|
||||
|
||||
def test_record_class():
|
||||
rec = schedule.Record({'spam': 99, 'eggs': 12})
|
||||
rec = schedule.Record(spam=99, eggs=12)
|
||||
assert rec.spam == 99
|
||||
assert rec.eggs == 12
|
||||
|
||||
|
@ -13,16 +13,16 @@ def db():
|
||||
|
||||
|
||||
def test_record_attr_access():
|
||||
rec = schedule.Record({'spam': 99, 'eggs': 12})
|
||||
rec = schedule.Record(spam=99, eggs=12)
|
||||
assert rec.spam == 99
|
||||
assert rec.eggs == 12
|
||||
|
||||
|
||||
def test_record_repr():
|
||||
rec = schedule.Record({'spam': 99, 'eggs': 12})
|
||||
assert repr(rec).startswith('<Record object at 0x')
|
||||
rec2 = schedule.Record({'name': 'Fido'})
|
||||
assert repr(rec2) == "<Record 'Fido'>"
|
||||
rec = schedule.DbRecord(spam=99, eggs=12)
|
||||
assert 'DbRecord object at 0x' in repr(rec)
|
||||
rec2 = schedule.DbRecord(serial=13)
|
||||
assert repr(rec2) == "<DbRecord serial=13>"
|
||||
|
||||
|
||||
def test_conference_record(db):
|
||||
@ -34,9 +34,14 @@ def test_speaker_record(db):
|
||||
assert speaker.name == 'Anna Martelli Ravenscroft'
|
||||
|
||||
|
||||
def test_missing_db_exception():
|
||||
with pytest.raises(schedule.MissingDatabaseError):
|
||||
schedule.DbRecord.fetch('venue.1585')
|
||||
|
||||
|
||||
def test_dbrecord(db):
|
||||
schedule.DbRecord.set_db(db)
|
||||
venue = schedule.DbRecord.get('venue.1585')
|
||||
venue = schedule.DbRecord.fetch('venue.1585')
|
||||
assert venue.name == 'Exhibit Hall B'
|
||||
|
||||
|
||||
|
10
metaprog/pseudo_construction.py
Normal file
10
metaprog/pseudo_construction.py
Normal file
@ -0,0 +1,10 @@
|
||||
# pseudo-code for object construction
|
||||
def object_maker(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 = object_maker(Foo, 'bar')
|
Loading…
x
Reference in New Issue
Block a user