Skip to content

Commit 8e2b8d9

Browse files
committed
metaprogramming examples
1 parent 7a268a5 commit 8e2b8d9

18 files changed

+13902
-11
lines changed

descriptors/bulkfood_v2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,4 @@ def weight(self, value):
6060
self.__weight = value # <6>
6161
else:
6262
raise ValueError('value must be > 0') # <7>
63-
# END LINEITEM_V2
63+
# END LINEITEM_V2

descriptors/bulkfood_v3.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
3434
"""
3535

36+
3637
# BEGIN LINEITEM_V3
3738
class Quantity: # <1>
3839

descriptors/bulkfood_v3_broken.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
3535
"""
3636

37+
3738
class Quantity:
3839

3940
def __init__(self, storage_name):

descriptors/bulkfood_v4.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,23 @@ class Quantity:
4545

4646
def __init__(self):
4747
cls = self.__class__ # <2>
48-
prefix = cls.__name__ # <3>
49-
index = cls.__counter # <4>
50-
self.storage_name = '_{}_{}'.format(prefix, index) # <5>
51-
cls.__counter += 1 # <6>
48+
prefix = cls.__name__
49+
index = cls.__counter
50+
self.storage_name = '_{}_{}'.format(prefix, index) # <3>
51+
cls.__counter += 1 # <4>
5252

53-
def __get__(self, instance, owner): # <7>
54-
return getattr(instance, self.storage_name) # <8>
53+
def __get__(self, instance, owner): # <5>
54+
return getattr(instance, self.storage_name) # <6>
5555

56-
def __set__(self, instance, value): # <9>
56+
def __set__(self, instance, value):
5757
if value > 0:
58-
setattr(instance, self.storage_name, value) # <10>
58+
setattr(instance, self.storage_name, value) # <7>
5959
else:
6060
raise ValueError('value must be > 0')
6161

6262

6363
class LineItem:
64-
weight = Quantity() # <11>
64+
weight = Quantity() # <8>
6565
price = Quantity()
6666

6767
def __init__(self, description, weight, price):
@@ -71,4 +71,4 @@ def __init__(self, description, weight, price):
7171

7272
def subtotal(self):
7373
return self.weight * self.price
74-
# END LINEITEM_V4
74+
# END LINEITEM_V4

descriptors/bulkfood_v4b.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""
2+
3+
A line item for a bulk food order has description, weight and price fields::
4+
5+
>>> raisins = LineItem('Golden raisins', 10, 6.95)
6+
>>> raisins.weight, raisins.description, raisins.price
7+
(10, 'Golden raisins', 6.95)
8+
9+
A ``subtotal`` method gives the total price for that line item::
10+
11+
>>> raisins.subtotal()
12+
69.5
13+
14+
The weight of a ``LineItem`` must be greater than 0::
15+
16+
>>> raisins.weight = -20
17+
Traceback (most recent call last):
18+
...
19+
ValueError: value must be > 0
20+
21+
No change was made::
22+
23+
>>> raisins.weight
24+
10
25+
26+
The value of the attributes managed by the descriptors are stored in
27+
alternate attributes, created by the descriptors in each ``LineItem``
28+
instance::
29+
30+
>>> raisins = LineItem('Golden raisins', 10, 6.95)
31+
>>> dir(raisins) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
32+
['_Quantity_0', '_Quantity_1', '__class__', ...
33+
'description', 'price', 'subtotal', 'weight']
34+
>>> raisins._Quantity_0
35+
10
36+
>>> raisins._Quantity_1
37+
6.95
38+
39+
If the descriptor is accessed in the class, the descriptor object is
40+
returned:
41+
42+
>>> LineItem.price # doctest: +ELLIPSIS
43+
<bulkfood_v4b.Quantity object at 0x...>
44+
>>> br_nuts = LineItem('Brazil nuts', 10, 34.95)
45+
>>> br_nuts.price
46+
34.95
47+
48+
"""
49+
50+
51+
# BEGIN LINEITEM_V4B
52+
class Quantity:
53+
__counter = 0
54+
55+
def __init__(self):
56+
cls = self.__class__
57+
prefix = cls.__name__
58+
index = cls.__counter
59+
self.storage_name = '_{}_{}'.format(prefix, index)
60+
cls.__counter += 1
61+
62+
def __get__(self, instance, owner):
63+
if instance is None:
64+
return self # <1>
65+
else:
66+
return getattr(instance, self.storage_name) # <2>
67+
68+
def __set__(self, instance, value):
69+
if value > 0:
70+
setattr(instance, self.storage_name, value)
71+
else:
72+
raise ValueError('value must be > 0')
73+
# END LINEITEM_V4B
74+
75+
76+
class LineItem:
77+
weight = Quantity()
78+
price = Quantity()
79+
80+
def __init__(self, description, weight, price):
81+
self.description = description
82+
self.weight = weight
83+
self.price = price
84+
85+
def subtotal(self):
86+
return self.weight * self.price

descriptors/bulkfood_v4c.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
3+
A line item for a bulk food order has description, weight and price fields::
4+
5+
>>> raisins = LineItem('Golden raisins', 10, 6.95)
6+
>>> raisins.weight, raisins.description, raisins.price
7+
(10, 'Golden raisins', 6.95)
8+
9+
A ``subtotal`` method gives the total price for that line item::
10+
11+
>>> raisins.subtotal()
12+
69.5
13+
14+
The weight of a ``LineItem`` must be greater than 0::
15+
16+
>>> raisins.weight = -20
17+
Traceback (most recent call last):
18+
...
19+
ValueError: value must be > 0
20+
21+
No change was made::
22+
23+
>>> raisins.weight
24+
10
25+
26+
The value of the attributes managed by the descriptors are stored in
27+
alternate attributes, created by the descriptors in each ``LineItem``
28+
instance::
29+
30+
>>> raisins = LineItem('Golden raisins', 10, 6.95)
31+
>>> dir(raisins) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
32+
['_Quantity_0', '_Quantity_1', '__class__', ...
33+
'description', 'price', 'subtotal', 'weight']
34+
>>> raisins._Quantity_0
35+
10
36+
>>> raisins._Quantity_1
37+
6.95
38+
39+
If the descriptor is accessed in the class, the descriptor object is
40+
returned:
41+
42+
>>> LineItem.price # doctest: +ELLIPSIS
43+
<model_v4c.Quantity object at 0x...>
44+
>>> br_nuts = LineItem('Brazil nuts', 10, 34.95)
45+
>>> br_nuts.price
46+
34.95
47+
48+
"""
49+
50+
# BEGIN LINEITEM_V4C
51+
import model_v4c as model # <1>
52+
53+
54+
class LineItem:
55+
weight = model.Quantity() # <2>
56+
price = model.Quantity()
57+
58+
def __init__(self, description, weight, price):
59+
self.description = description
60+
self.weight = weight
61+
self.price = price
62+
63+
def subtotal(self):
64+
return self.weight * self.price
65+
# END LINEITEM_V4C

descriptors/model_v4c.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# BEGIN MODEL_V4
2+
class Quantity:
3+
__counter = 0
4+
5+
def __init__(self):
6+
cls = self.__class__
7+
prefix = cls.__name__
8+
index = cls.__counter
9+
self.storage_name = '_{}_{}'.format(prefix, index)
10+
cls.__counter += 1
11+
12+
def __get__(self, instance, owner):
13+
if instance is None:
14+
return self
15+
else:
16+
return getattr(instance, self.storage_name)
17+
18+
def __set__(self, instance, value):
19+
if value > 0:
20+
setattr(instance, self.storage_name, value)
21+
else:
22+
raise ValueError('value must be > 0')
23+
# END MODEL_V4
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
explore0.py: Script to download and explore the OSCON schedule feed
3+
4+
>>> feed = load_json()
5+
>>> sorted(feed['Schedule'].keys())
6+
['conferences', 'events', 'speakers', 'venues']
7+
>>> for key, value in sorted(feed['Schedule'].items()):
8+
... print('{:3} {}'.format(len(value), key))
9+
...
10+
1 conferences
11+
484 events
12+
357 speakers
13+
53 venues
14+
>>> feed['Schedule']['speakers'][-1]['name']
15+
'Carina C. Zona'
16+
>>> carina = feed['Schedule']['speakers'][-1]
17+
>>> carina['twitter']
18+
'cczona'
19+
>>> feed['Schedule']['events'][40]['name']
20+
'There *Will* Be Bugs'
21+
>>> feed['Schedule']['events'][40]['speakers']
22+
[3471, 5199]
23+
24+
"""
25+
26+
from urllib.request import urlopen
27+
import warnings
28+
import os
29+
import json
30+
31+
URL = 'http://www.oreilly.com/pub/sc/osconfeed'
32+
JSON_NAME = 'osconfeed.json'
33+
34+
35+
def load_json():
36+
if not os.path.exists(JSON_NAME):
37+
msg = 'downloading {} to {}'.format(URL, JSON_NAME)
38+
warnings.warn(msg)
39+
with urlopen(URL) as remote, open(JSON_NAME, 'wb') as local:
40+
local.write(remote.read())
41+
42+
with open(JSON_NAME) as fp:
43+
return json.load(fp)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""
2+
explore.py: Script to download and explore the OSCON schedule feed
3+
4+
>>> raw_feed = load_json()
5+
>>> feed = FrozenJSON(raw_feed)
6+
>>> sorted(feed.Schedule.keys())
7+
['conferences', 'events', 'speakers', 'venues']
8+
>>> for key, value in sorted(feed.Schedule.items()):
9+
... print('{:3} {}'.format(len(value), key))
10+
...
11+
1 conferences
12+
484 events
13+
357 speakers
14+
53 venues
15+
>>> feed.Schedule.speakers[-1].name
16+
'Carina C. Zona'
17+
>>> carina = feed.Schedule.speakers[-1]
18+
>>> carina.twitter
19+
'cczona'
20+
>>> feed.Schedule.events[40].name
21+
'There *Will* Be Bugs'
22+
>>> feed.Schedule.events[40].speakers
23+
[3471, 5199]
24+
25+
"""
26+
27+
from urllib.request import urlopen
28+
import warnings
29+
import os
30+
import json
31+
from collections import abc
32+
33+
URL = 'http://www.oreilly.com/pub/sc/osconfeed'
34+
JSON_NAME = 'osconfeed.json'
35+
36+
37+
def load_json():
38+
if not os.path.exists(JSON_NAME):
39+
msg = 'downloading {} to {}'.format(URL, JSON_NAME)
40+
warnings.warn(msg)
41+
with urlopen(URL) as remote, open(JSON_NAME, 'wb') as local:
42+
local.write(remote.read())
43+
44+
with open(JSON_NAME) as fp:
45+
return json.load(fp)
46+
47+
48+
class FrozenJSON:
49+
"""A read-only façade for navigating a JSON-like object
50+
using attribute notation
51+
"""
52+
53+
def __init__(self, mapping):
54+
self._data = dict(mapping)
55+
56+
def __getattr__(self, name):
57+
if hasattr(self._data, name):
58+
return getattr(self._data, name)
59+
else:
60+
return FrozenJSON.build(self._data[name])
61+
62+
@classmethod
63+
def build(cls, obj):
64+
if isinstance(obj, abc.Mapping):
65+
return cls(obj)
66+
elif isinstance(obj, abc.MutableSequence):
67+
return [cls.build(item) for item in obj]
68+
else:
69+
return obj

0 commit comments

Comments
 (0)