comparison piecrust/data/linker.py @ 237:879fe1457e48

data: `Linker` refactor. * Unify the `Linker` and `RecursiveLinker`. * When a page and a directory share the same name, merge their entries in the returned iterator. * Tentative new templating interface.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 15 Feb 2015 22:46:23 -0800
parents d6d0e4976beb
children f130365568ff
comparison
equal deleted inserted replaced
236:eaf18442bff8 237:879fe1457e48
7 7
8 logger = logging.getLogger(__name__) 8 logger = logging.getLogger(__name__)
9 9
10 10
11 class LinkedPageData(PaginationData): 11 class LinkedPageData(PaginationData):
12 debug_render = ['name', 'is_dir', 'is_self'] 12 """ Class whose instances get returned when iterating on a `Linker`
13 13 or `RecursiveLinker`. It's just like what gets usually returned by
14 def __init__(self, name, page, is_self=False): 14 `Paginator` and other page iterators, but with a few additional data
15 like hierarchical data.
16 """
17 debug_render = ['is_dir', 'is_self'] + PaginationData.debug_render
18
19 def __init__(self, page):
15 super(LinkedPageData, self).__init__(page) 20 super(LinkedPageData, self).__init__(page)
16 self.name = name 21 self.name = page.config.get('__linker_name')
17 self.is_self = is_self 22 self.is_self = page.config.get('__linker_is_self')
18 23 self.children = page.config.get('__linker_child')
19 @property 24 self.is_dir = (self.children is not None)
20 def is_dir(self): 25 self.is_page = True
21 return False 26
27 self.mapLoader('*', self._linkerChildLoader)
28
29 def _linkerChildLoader(self, name):
30 return getattr(self.children, name)
31
32
33 class LinkedPageDataBuilderIterator(object):
34 """ Iterator that builds `LinkedPageData` out of pages.
35 """
36 def __init__(self, it):
37 self.it = it
38
39 def __iter__(self):
40 for item in self.it:
41 yield LinkedPageData(item)
22 42
23 43
24 class LinkerSource(IPaginationSource): 44 class LinkerSource(IPaginationSource):
25 def __init__(self, pages): 45 """ Source iterator that returns pages given by `Linker`.
26 self._pages = pages 46 """
47 def __init__(self, pages, orig_source):
48 self._pages = list(pages)
49 self._orig_source = None
50 if isinstance(orig_source, IPaginationSource):
51 self._orig_source = orig_source
27 52
28 def getItemsPerPage(self): 53 def getItemsPerPage(self):
29 raise NotImplementedError() 54 raise NotImplementedError()
30 55
31 def getSourceIterator(self): 56 def getSourceIterator(self):
32 return self._pages 57 return self._pages
33 58
34 def getSorterIterator(self, it): 59 def getSorterIterator(self, it):
60 # We don't want to sort the pages -- we expect the original source
61 # to return hierarchical items in the order it wants already.
35 return None 62 return None
36 63
37 def getTailIterator(self, it): 64 def getTailIterator(self, it):
38 return None 65 return LinkedPageDataBuilderIterator(it)
39 66
40 def getPaginationFilter(self, page): 67 def getPaginationFilter(self, page):
41 return None 68 return None
42 69
43 def getSettingAccessor(self): 70 def getSettingAccessor(self):
44 return lambda i, n: i.get(n) 71 if self._orig_source:
45 72 return self._orig_source.getSettingAccessor()
46 73 return None
47 class LinkedPageDataIterator(object):
48 def __init__(self, items):
49 self._items = list(items)
50 self._index = -1
51
52 def __iter__(self):
53 return self
54
55 def __next__(self):
56 self._index += 1
57 if self._index >= len(self._items):
58 raise StopIteration()
59 return self._items[self._index]
60
61 def sort(self, name):
62 def key_getter(item):
63 return item[name]
64 self._items = sorted(self._item, key=key_getter)
65 return self
66 74
67 75
68 class Linker(object): 76 class Linker(object):
69 debug_render_doc = """Provides access to sibling and children pages.""" 77 debug_render_doc = """Provides access to sibling and children pages."""
70 78
71 def __init__(self, source, *, name=None, dir_path=None, page_path=None): 79 def __init__(self, source, *, name=None, dir_path=None, page_path=None):
72 self.source = source 80 self._source = source
73 self._name = name 81 self._name = name
74 self._dir_path = dir_path 82 self._dir_path = dir_path
75 self._root_page_path = page_path 83 self._root_page_path = page_path
76 self._cache = None 84 self._items = None
77 self._is_listable = None 85
86 self.is_dir = True
87 self.is_page = False
88 self.is_self = False
78 89
79 def __iter__(self): 90 def __iter__(self):
80 self._load() 91 return iter(self.pages)
81 return LinkedPageDataIterator(self._cache.values())
82 92
83 def __getattr__(self, name): 93 def __getattr__(self, name):
84 self._load() 94 self._load()
85 try: 95 try:
86 return self._cache[name] 96 item = self._items[name]
87 except KeyError: 97 except KeyError:
88 raise AttributeError() 98 raise AttributeError()
89 99
100 if isinstance(item, Linker):
101 return item
102
103 return LinkedPageData(item)
104
90 @property 105 @property
91 def name(self): 106 def name(self):
92 if not self._name: 107 if self._name is None:
93 self._load() 108 self._load()
94 return self._name 109 return self._name
95 110
96 @property 111 @property
97 def is_dir(self): 112 def children(self):
98 return True 113 return self._iterItems(0)
99 114
100 @property 115 @property
101 def is_self(self): 116 def pages(self):
102 return False 117 return self._iterItems(0, filter_page_items)
118
119 @property
120 def directories(self):
121 return self._iterItems(0, filter_directory_items)
122
123 @property
124 def all(self):
125 return self._iterItems()
126
127 @property
128 def allpages(self):
129 return self._iterItems(-1, filter_page_items)
130
131 @property
132 def alldirectories(self):
133 return self._iterItems(-1, filter_directory_items)
134
135 @property
136 def root(self):
137 return self.forpath('/')
138
139 def forpath(self, rel_path):
140 return Linker(self._source,
141 name='.', dir_path=rel_path,
142 page_path=self._root_page_path)
143
144 def _iterItems(self, max_depth=-1, filter_func=None):
145 items = walk_linkers(self, max_depth=max_depth,
146 filter_func=filter_func)
147 src = LinkerSource(items, self._source)
148 return PageIterator(src)
103 149
104 def _load(self): 150 def _load(self):
105 if self._cache is not None: 151 if self._items is not None:
106 return 152 return
107 153
108 self._is_listable = isinstance(self.source, IListableSource) 154 is_listable = isinstance(self._source, IListableSource)
109 if self._is_listable and self._root_page_path is not None: 155 if not is_listable:
156 raise Exception("Source '%s' can't be listed." % self._source.name)
157
158 if self._root_page_path is not None:
110 if self._name is None: 159 if self._name is None:
111 self._name = self.source.getBasename(self._root_page_path) 160 self._name = self._source.getBasename(self._root_page_path)
112 if self._dir_path is None: 161 if self._dir_path is None:
113 self._dir_path = self.source.getDirpath(self._root_page_path) 162 self._dir_path = self._source.getDirpath(self._root_page_path)
114 163
115 self._cache = collections.OrderedDict() 164 if self._dir_path is None:
116 if not self._is_listable or self._dir_path is None: 165 raise Exception("This linker has no directory to start from.")
117 return 166
118 167 items = list(self._source.listPath(self._dir_path))
119 items = self.source.listPath(self._dir_path) 168 self._items = collections.OrderedDict()
120 with self.source.app.env.page_repository.startBatchGet(): 169 with self._source.app.env.page_repository.startBatchGet():
121 for is_dir, name, data in items: 170 for is_dir, name, data in items:
171 # If `is_dir` is true, `data` will be the directory's source
172 # path. If not, it will be a page factory.
122 if is_dir: 173 if is_dir:
123 self._cache[name] = Linker(self.source, 174 item = Linker(self._source,
124 name=name, dir_path=data) 175 name=name, dir_path=data)
125 else: 176 else:
126 page = data.buildPage() 177 item = data.buildPage()
127 is_root_page = (self._root_page_path == data.rel_path) 178 item.config.set('__linker_name', name)
128 self._cache[name] = LinkedPageData(name, page, 179 item.config.set('__linker_is_self',
129 is_root_page) 180 item.rel_path == self._root_page_path)
130 181
131 182 existing = self._items.get(name)
132 class RecursiveLinker(Linker): 183 if existing is None:
133 def __init__(self, source, *args, **kwargs): 184 self._items[name] = item
134 super(RecursiveLinker, self).__init__(source, *args, **kwargs) 185 elif is_dir:
135 186 # The current item is a directory. The existing item
136 def __iter__(self): 187 # should be a page.
137 return iter(self.pages) 188 existing.config.set('__linker_child', item)
138 189 else:
139 def __getattr__(self, name): 190 # The current item is a page. The existing item should
140 if name == 'pages': 191 # be a directory.
141 return self.getpages() 192 item.config.set('__linker_child', existing)
142 if name == 'siblings': 193 self._items[name] = item
143 return self.getsiblings() 194
144 raise AttributeError() 195
145 196 def filter_page_items(item):
146 def getpages(self): 197 return not isinstance(item, Linker)
147 src = LinkerSource(self._iterateLinkers()) 198
148 return PageIterator(src) 199
149 200 def filter_directory_items(item):
150 def getsiblings(self): 201 return isinstance(item, linker)
151 src = LinkerSource(self._iterateLinkers(0)) 202
152 return PageIterator(src) 203
153 204 def walk_linkers(linker, depth=0, max_depth=-1, filter_func=None):
154 def frompath(self, rel_path):
155 return RecursiveLinker(self.source, name='.', dir_path=rel_path)
156
157 def _iterateLinkers(self, max_depth=-1):
158 self._load()
159 if not self._is_listable:
160 return
161 yield from walk_linkers(self, 0, max_depth)
162
163
164 def walk_linkers(linker, depth=0, max_depth=-1):
165 linker._load() 205 linker._load()
166 for item in linker._cache.values(): 206 for item in linker._items.values():
167 if item.is_dir: 207 if not filter_func or filter_func(item):
168 if max_depth < 0 or depth + 1 <= max_depth:
169 yield from walk_linkers(item, depth + 1, max_depth)
170 else:
171 yield item 208 yield item
172 209
210 if (isinstance(item, Linker) and
211 (max_depth < 0 or depth + 1 <= max_depth)):
212 yield from walk_linkers(item, depth + 1, max_depth)
213