Mercurial > cgi-bin > hgweb.cgi > curlyq
comparison workspace.py @ 4:7a83e82e65a6
Remove some deadwood.
author | David Barts <n5jrn@me.com> |
---|---|
date | Thu, 26 Dec 2019 20:04:04 -0800 |
parents | 091c03f1b2e8 |
children |
comparison
equal
deleted
inserted
replaced
3:091c03f1b2e8 | 4:7a83e82e65a6 |
---|---|
1 #!/usr/bin/env python3 | 1 #!/usr/bin/env python3 |
2 # -*- coding: utf-8 -*- | 2 # -*- coding: utf-8 -*- |
3 | |
4 # Classes that implement a workspace for curly-quoting a text, and views | |
5 # into the same. | |
6 | 3 |
7 # I m p o r t s | 4 # I m p o r t s |
8 | 5 |
9 import os, sys | 6 import os, sys |
10 import io | 7 import io |
180 """ | 177 """ |
181 Context manager: close on exit. | 178 Context manager: close on exit. |
182 """ | 179 """ |
183 self.close() | 180 self.close() |
184 return False | 181 return False |
185 | |
186 class Bounds(object): | |
187 """ | |
188 A set of index bounds. | |
189 """ | |
190 def __init__(self, start, stop): | |
191 if start > stop or start < 0 or stop < 0: | |
192 raise ValueError("invalid bounds") | |
193 self.start = int(start) | |
194 self.stop = int(stop) | |
195 | |
196 @classmethod | |
197 def from_object(cls, obj): | |
198 if isinstance(obj, slice): | |
199 return self(slice.start, slice.stop) | |
200 return self(obj[0], obj[1]) | |
201 | |
202 def __lt__(self, other): | |
203 return self.start < other.start | |
204 | |
205 def __le__(self, other): | |
206 return self.start <= other.start | |
207 | |
208 def __eq__(self, other): | |
209 return self.start == other.start | |
210 | |
211 def __ne__(self, other): | |
212 return self.start != other.start | |
213 | |
214 def __gt__(self, other): | |
215 return self.start > other.start | |
216 | |
217 def __ge__(self, other): | |
218 return self.start >= other.start | |
219 | |
220 def __contains__(self, scalar): | |
221 return self.start <= scalar < self.stop | |
222 | |
223 def __repr__(self): | |
224 return "{0}({1!r}, {2!r})".format(self.__class__.__name__, self.start, self.stop) | |
225 | |
226 class Mapping(object): | |
227 """ | |
228 Represents a mapping of a single view segment into an indexable | |
229 object. | |
230 """ | |
231 def __init__(self, bounds, offset): | |
232 if not isinstance(bounds, Bounds): | |
233 raise TypeError("bounds must be a Bounds object") | |
234 if not isinstance(offset, int): | |
235 raise TypeError("offset must be an int") | |
236 self.bounds = bounds | |
237 self.offset = offset | |
238 self.delta = self.offset - self.bounds.start | |
239 | |
240 def __repr__(self): | |
241 return "{0}({1!r}, {2!r})".format(self.__class__.__name__, self.bounds, self.offset) | |
242 | |
243 class SegmentedView(object): | |
244 """ | |
245 Implements a view on a subscriptable object. The view is composed of | |
246 zero or more segments of the source object. Has the same idiosyncratic | |
247 behavior for out-of-bounds indices that Workspace has (and for the | |
248 same reason). Mutating this object causes the parent object to also | |
249 be mutated. | |
250 """ | |
251 def __init__(self, indexable, bounds): | |
252 self.indexable = indexable | |
253 self._mmap = [ Mapping(Bounds(0, 0), 0) ] | |
254 pos = 0 | |
255 for r in sorted(bounds): | |
256 if pos is not None and r.start <= pos and r.stop > pos: | |
257 # merge ranges | |
258 self._mmap[-1].bounds.stop = r.stop | |
259 pos = r.stop | |
260 continue | |
261 opos = pos | |
262 pos += r.stop - r.start | |
263 self._mmap.append(Mapping(Bounds(opos, pos), r.start)) | |
264 self._length = pos | |
265 | |
266 def _mapped(self, index): | |
267 mmap_index = self._binsch(index) | |
268 if mmap_index is None: | |
269 raise IndexError("index {0} out of range".format(index)) | |
270 return index + self._mmap[mmap_index].delta | |
271 | |
272 def _binsch(self, index): | |
273 a = 0 | |
274 z = len(self._mmap) - 1 | |
275 while a <= z: | |
276 m = (a + z) // 2 | |
277 if index in self._mmap[m].bounds: | |
278 return m | |
279 if index < self._mmap[m].bounds.start: | |
280 z = m - 1 | |
281 else: | |
282 assert index >= self._mmap[m].bounds.stop | |
283 a = m + 1 | |
284 return None | |
285 | |
286 def __setitem__(self, key, value): | |
287 """ | |
288 Direct access to replace a single character. | |
289 """ | |
290 if not isinstance(key, int): | |
291 raise TypeError("__setitem__ only supports integers") | |
292 self.indexable[self._mapped(key)] = value | |
293 | |
294 def __getitem__(self, key): | |
295 """ | |
296 Direct access to a single character or range of characters. | |
297 """ | |
298 # Trivial cases | |
299 if isinstance(key, int): | |
300 return self._get1(key) | |
301 if not isinstance(key, slice): | |
302 raise TypeError("expecting int or slice") | |
303 if key.step is not None: | |
304 raise ValueError("__getitem__ does not support steps in slices") | |
305 | |
306 # Loop up the starting segment. | |
307 mi = self._binsch(key.start) | |
308 if mi is None: | |
309 return "" | |
310 m = self._mmap[mi] | |
311 | |
312 # Horray! There's only one segment, so we can optimize. | |
313 if key.stop <= m.bounds.stop: | |
314 start = key.start + m.delta | |
315 stop = key.stop + m.delta | |
316 return self.indexable[start:stop] | |
317 | |
318 # The most involved (multi-segment) case. | |
319 with io.StringIO() as buf: | |
320 for m in self._mmap[mi:]: | |
321 if m.bounds.start >= key.stop: | |
322 break | |
323 start = max(key.start, m.bounds.start) + m.delta | |
324 stop = min(key.stop, m.bounds.stop) + m.delta | |
325 buf.write(self.indexable[start:stop]) | |
326 return buf.getvalue() | |
327 | |
328 def __len__(self): | |
329 return self._length | |
330 | |
331 def _get1(self, index): | |
332 try: | |
333 return self.indexable[self._mapped(index)] | |
334 except IndexError: | |
335 return "" | |
336 | |
337 def getvalue(self): | |
338 return self[0:len(self)] |