Show More
This diff has been collapsed as it changes many lines, (2012 lines changed) Show them Hide them | |||
@@ -0,0 +1,2012 b'' | |||
|
1 | # -*- coding: iso-8859-1 -*- | |
|
2 | ||
|
3 | """ | |
|
4 | ``ipipe`` provides classes to be used in an interactive Python session. Doing a | |
|
5 | ``from ipipe import *`` is the preferred way to do this. The name of all | |
|
6 | objects imported this way starts with ``i`` to minimize collisions. | |
|
7 | ||
|
8 | ``ipipe`` supports "pipeline expressions", which is something resembling Unix | |
|
9 | pipes. An example is: | |
|
10 | ||
|
11 | iwalk | ifilter("name.endswith('.py')") | isort("size") | |
|
12 | ||
|
13 | This gives a listing of all files in the current directory (and subdirectories) | |
|
14 | whose name ends with '.py' sorted by size. | |
|
15 | ||
|
16 | There are three types of objects in a pipeline expression: | |
|
17 | ||
|
18 | * ``Table``s: These objects produce items. Examples are `�ls`` (listing the | |
|
19 | current directory, ``ienv`` (listing environment variables), ``ipwd`` (listing | |
|
20 | user account) and ``igrp`` (listing user groups). A ``Table`` must be the first | |
|
21 | object in a pipe expression. | |
|
22 | ||
|
23 | * ``Pipe``s: These objects sit in the middle of a pipe expression. They transform | |
|
24 | the input in some way (e.g. filtering or sorting it). Examples are: ``ifilter`` | |
|
25 | (which filters the input pipe), ``isort`` (which sorts the input pipe) and | |
|
26 | ``ieval`` (which evaluates a function or expression for each object in the | |
|
27 | input pipe). | |
|
28 | ||
|
29 | * ``Display``s: These objects can be put as the last object in a pipeline | |
|
30 | expression. There are responsible for displaying the result of the pipeline | |
|
31 | expression. If a pipeline expression doesn't end in a display object a default | |
|
32 | display objects will be used. One example is `�browse`` which is a ``curses`` | |
|
33 | based browser. | |
|
34 | """ | |
|
35 | ||
|
36 | import sys, os, os.path, stat, glob, new, csv, datetime, itertools, mimetypes | |
|
37 | ||
|
38 | try: # Python 2.3 compatibility | |
|
39 | import collections | |
|
40 | except ImportError: | |
|
41 | deque = list | |
|
42 | else: | |
|
43 | deque = collections.deque | |
|
44 | ||
|
45 | try: # Python 2.3 compatibility | |
|
46 | set | |
|
47 | except NameError: | |
|
48 | import sets | |
|
49 | set = sets.Set | |
|
50 | ||
|
51 | try: # Python 2.3 compatibility | |
|
52 | sorted | |
|
53 | except NameError: | |
|
54 | def sorted(iterator, key=None, reverse=False): | |
|
55 | items = list(iterator) | |
|
56 | if key is not None: | |
|
57 | items.sort(lambda i1, i2: cmp(key(i1), key(i2))) | |
|
58 | else: | |
|
59 | items.sort() | |
|
60 | if reverse: | |
|
61 | items.reverse() | |
|
62 | return items | |
|
63 | ||
|
64 | try: | |
|
65 | import pwd | |
|
66 | except ImportError: | |
|
67 | pwd = None | |
|
68 | ||
|
69 | try: | |
|
70 | import grp | |
|
71 | except ImportError: | |
|
72 | grp = None | |
|
73 | ||
|
74 | try: | |
|
75 | import curses | |
|
76 | except ImportError: | |
|
77 | curses = None | |
|
78 | ||
|
79 | ||
|
80 | __all__ = [ | |
|
81 | "ifile", "ils", "iglob", "iwalk", "ipwdentry", "ipwd", "igrpentry", "igrp", | |
|
82 | "icsv", "ix", "ichain", "isort", "ifilter", "ieval", "ienum", "idict", "ienv", | |
|
83 | "idump", "iless" | |
|
84 | ] | |
|
85 | ||
|
86 | ||
|
87 | os.stat_float_times(True) # enable microseconds | |
|
88 | ||
|
89 | ||
|
90 | _default = object() | |
|
91 | ||
|
92 | def item(iterator, index, default=_default): | |
|
93 | """ | |
|
94 | Return the ``index``th item from the iterator ``iterator``. | |
|
95 | ``index`` must be an integer (negative integers are relative to the | |
|
96 | end (i.e. the last item produced by the iterator)). | |
|
97 | ||
|
98 | If ``default`` is given, this will be the default value when | |
|
99 | the iterator doesn't contain an item at this position. Otherwise an | |
|
100 | ``IndexError`` will be raised. | |
|
101 | ||
|
102 | Note that using this function will partially or totally exhaust the | |
|
103 | iterator. | |
|
104 | """ | |
|
105 | i = index | |
|
106 | if i>=0: | |
|
107 | for item in iterator: | |
|
108 | if not i: | |
|
109 | return item | |
|
110 | i -= 1 | |
|
111 | else: | |
|
112 | i = -index | |
|
113 | cache = deque() | |
|
114 | for item in iterator: | |
|
115 | cache.append(item) | |
|
116 | if len(cache)>i: | |
|
117 | cache.popleft() | |
|
118 | if len(cache)==i: | |
|
119 | return cache.popleft() | |
|
120 | if default is _default: | |
|
121 | raise IndexError(index) | |
|
122 | else: | |
|
123 | return default | |
|
124 | ||
|
125 | ||
|
126 | class _AttrNamespace(object): | |
|
127 | """ | |
|
128 | Internal helper that is used for providing a namespace for evaluating | |
|
129 | expressions containg attribute names of an object. | |
|
130 | """ | |
|
131 | def __init__(self, wrapped): | |
|
132 | self.wrapped = wrapped | |
|
133 | ||
|
134 | def __getitem__(self, name): | |
|
135 | if name == "_": | |
|
136 | return self.wrapped | |
|
137 | try: | |
|
138 | return getattr(self.wrapped, name) | |
|
139 | except AttributeError: | |
|
140 | raise KeyError(name) | |
|
141 | ||
|
142 | ||
|
143 | class Table(object): | |
|
144 | """ | |
|
145 | A ``Table`` is an object that produces items (just like a normal Python | |
|
146 | iterator/generator does) and can be used as the first object in a pipeline | |
|
147 | expression. The displayhook will open the default browser for such an object | |
|
148 | (instead of simply printing the ``repr()`` result). | |
|
149 | """ | |
|
150 | class __metaclass__(type): | |
|
151 | def __iter__(self): | |
|
152 | return iter(self()) | |
|
153 | ||
|
154 | def __or__(self, other): | |
|
155 | return self() | other | |
|
156 | ||
|
157 | def __add__(self, other): | |
|
158 | return self() + other | |
|
159 | ||
|
160 | def __radd__(self, other): | |
|
161 | return other + self() | |
|
162 | ||
|
163 | def __getitem__(self, index): | |
|
164 | return self()[index] | |
|
165 | ||
|
166 | def __getitem__(self, index): | |
|
167 | return item(self, index) | |
|
168 | ||
|
169 | def __contains__(self, item): | |
|
170 | for haveitem in self: | |
|
171 | if item == haveitem: | |
|
172 | return True | |
|
173 | return False | |
|
174 | ||
|
175 | def __or__(self, other): | |
|
176 | if isinstance(other, type) and (issubclass(other, Table) or issubclass(other, Display)): | |
|
177 | other = other() | |
|
178 | elif not isinstance(other, Display) and not isinstance(other, Table): | |
|
179 | other = ieval(other) | |
|
180 | return other.__ror__(self) | |
|
181 | ||
|
182 | def __add__(self, other): | |
|
183 | if isinstance(other, type) and issubclass(other, Table): | |
|
184 | other = other() | |
|
185 | return ichain(self, other) | |
|
186 | ||
|
187 | def __radd__(self, other): | |
|
188 | if isinstance(other, type) and issubclass(other, Table): | |
|
189 | other = other() | |
|
190 | return ichain(other, self) | |
|
191 | ||
|
192 | def __iter__(self): | |
|
193 | return xiter(self, "default") | |
|
194 | ||
|
195 | ||
|
196 | class Pipe(Table): | |
|
197 | """ | |
|
198 | A ``Pipe`` is an object that can be used in a pipeline expression. It processes | |
|
199 | the objects it gets from its input ``Table``/``Pipe``. Note that a ``Pipe`` | |
|
200 | object can't be used as the first object in a pipeline expression, as it | |
|
201 | doesn't produces items itself. | |
|
202 | """ | |
|
203 | class __metaclass__(Table.__metaclass__): | |
|
204 | def __ror__(self, input): | |
|
205 | return input | self() | |
|
206 | ||
|
207 | def __ror__(self, input): | |
|
208 | if isinstance(input, type) and issubclass(input, Table): | |
|
209 | input = input() | |
|
210 | self.input = input | |
|
211 | return self | |
|
212 | ||
|
213 | ||
|
214 | def _getattr(obj, name, default=_default): | |
|
215 | """ | |
|
216 | Internal helper for getting an attribute of an item. If ``name`` is ``None`` | |
|
217 | return the object itself. If ``name`` is an integer, use ``__getitem__`` | |
|
218 | instead. If the attribute or item does not exist, return ``default``. | |
|
219 | """ | |
|
220 | if name is None: | |
|
221 | return obj | |
|
222 | elif isinstance(name, basestring): | |
|
223 | return getattr(obj, name, default) | |
|
224 | elif callable(name): | |
|
225 | return name(obj) | |
|
226 | else: | |
|
227 | try: | |
|
228 | return obj[name] | |
|
229 | except IndexError: | |
|
230 | return default | |
|
231 | ||
|
232 | ||
|
233 | def _attrname(name): | |
|
234 | """ | |
|
235 | Internal helper that gives a proper name for the attribute ``name`` | |
|
236 | (which might be ``None`` or an ``int``). | |
|
237 | """ | |
|
238 | if name is None: | |
|
239 | return "_" | |
|
240 | elif isinstance(name, basestring): | |
|
241 | return name | |
|
242 | elif callable(name): | |
|
243 | return name.__name__ | |
|
244 | else: | |
|
245 | return str(name) | |
|
246 | ||
|
247 | ||
|
248 | def xrepr(item, mode): | |
|
249 | try: | |
|
250 | func = item.__xrepr__ | |
|
251 | except (KeyboardInterrupt, SystemExit): | |
|
252 | raise | |
|
253 | except Exception: | |
|
254 | return repr(item) | |
|
255 | else: | |
|
256 | return func(mode) | |
|
257 | ||
|
258 | ||
|
259 | def xattrs(item, mode): | |
|
260 | try: | |
|
261 | func = item.__xattrs__ | |
|
262 | except AttributeError: | |
|
263 | if isinstance(item, (list, tuple)): | |
|
264 | return xrange(len(item)) | |
|
265 | return (None,) | |
|
266 | else: | |
|
267 | return func(mode) | |
|
268 | ||
|
269 | ||
|
270 | def xiter(item, mode): | |
|
271 | if mode == "detail": | |
|
272 | def items(): | |
|
273 | for name in xattrs(item, mode): | |
|
274 | yield XAttr(item, name) | |
|
275 | return items() | |
|
276 | try: | |
|
277 | func = item.__xiter__ | |
|
278 | except AttributeError: | |
|
279 | if isinstance(item, dict): | |
|
280 | return xiter(idict(item), mode) | |
|
281 | elif isinstance(item, new.module): | |
|
282 | def items(item): | |
|
283 | for key in sorted(item.__dict__): | |
|
284 | yield idictentry(key, getattr(item, key)) | |
|
285 | return items(item) | |
|
286 | elif isinstance(item, basestring): | |
|
287 | if not len(item): | |
|
288 | raise ValueError("can't enter empty string") | |
|
289 | lines = item.splitlines() | |
|
290 | if len(lines) <= 1: | |
|
291 | raise ValueError("can't enter one line string") | |
|
292 | return iter(lines) | |
|
293 | return iter(item) | |
|
294 | else: | |
|
295 | return iter(func(mode)) # iter() just to be safe | |
|
296 | ||
|
297 | ||
|
298 | class ichain(Pipe): | |
|
299 | """ | |
|
300 | Chains multiple ``Table``s into one. | |
|
301 | """ | |
|
302 | ||
|
303 | def __init__(self, *iters): | |
|
304 | self.iters = iters | |
|
305 | ||
|
306 | def __xiter__(self, mode): | |
|
307 | return itertools.chain(*self.iters) | |
|
308 | ||
|
309 | def __xrepr__(self, mode): | |
|
310 | if mode == "header" or mode == "footer": | |
|
311 | parts = [] | |
|
312 | for item in self.iters: | |
|
313 | part = xrepr(item, mode) | |
|
314 | if isinstance(item, Pipe): | |
|
315 | part = "(%s)" % part | |
|
316 | parts.append(part) | |
|
317 | return "+".join(parts) | |
|
318 | return repr(self) | |
|
319 | ||
|
320 | def __repr__(self): | |
|
321 | return "%s.%s(%s)" % (self.__class__.__module__, self.__class__.__name__, ", ".join([repr(it) for it in self.iters])) | |
|
322 | ||
|
323 | ||
|
324 | class ifile(object): | |
|
325 | __slots__ = ("name", "_abspath", "_realpath", "_stat", "_lstat") | |
|
326 | ||
|
327 | def __init__(self, name): | |
|
328 | if isinstance(name, ifile): # copying files | |
|
329 | self.name = name.name | |
|
330 | self._abspath = name._abspath | |
|
331 | self._realpath = name._realpath | |
|
332 | self._stat = name._stat | |
|
333 | self._lstat = name._lstat | |
|
334 | else: | |
|
335 | self.name = os.path.normpath(name) | |
|
336 | self._abspath = None | |
|
337 | self._realpath = None | |
|
338 | self._stat = None | |
|
339 | self._lstat = None | |
|
340 | ||
|
341 | def __repr__(self): | |
|
342 | return "%s.%s(%r)" % (self.__class__.__module__, self.__class__.__name__, self.name) | |
|
343 | ||
|
344 | def open(self, mode="rb", buffer=None): | |
|
345 | if buffer is None: | |
|
346 | return open(self.abspath, mode) | |
|
347 | else: | |
|
348 | return open(self.abspath, mode, buffer) | |
|
349 | ||
|
350 | def remove(self): | |
|
351 | os.remove(self.abspath) | |
|
352 | ||
|
353 | def getabspath(self): | |
|
354 | if self._abspath is None: | |
|
355 | self._abspath = os.path.abspath(self.name) | |
|
356 | return self._abspath | |
|
357 | abspath = property(getabspath, None, None, "Path to file") | |
|
358 | ||
|
359 | def getrealpath(self): | |
|
360 | if self._realpath is None: | |
|
361 | self._realpath = os.path.realpath(self.name) | |
|
362 | return self._realpath | |
|
363 | realpath = property(getrealpath, None, None, "Path with links resolved") | |
|
364 | ||
|
365 | def getbasename(self): | |
|
366 | return os.path.basename(self.abspath) | |
|
367 | basename = property(getbasename, None, None, "File name without directory") | |
|
368 | ||
|
369 | def getstat(self): | |
|
370 | if self._stat is None: | |
|
371 | self._stat = os.stat(self.abspath) | |
|
372 | return self._stat | |
|
373 | stat = property(getstat, None, None, "os.stat() result") | |
|
374 | ||
|
375 | def getlstat(self): | |
|
376 | if self._lstat is None: | |
|
377 | self._lstat = os.lstat(self.abspath) | |
|
378 | return self._lstat | |
|
379 | lstat = property(getlstat, None, None, "os.lstat() result") | |
|
380 | ||
|
381 | def getmode(self): | |
|
382 | return self.stat.st_mode | |
|
383 | mode = property(getmode, None, None, "Access mode") | |
|
384 | ||
|
385 | def gettype(self): | |
|
386 | data = [ | |
|
387 | (stat.S_ISREG, "file"), | |
|
388 | (stat.S_ISDIR, "dir"), | |
|
389 | (stat.S_ISCHR, "chardev"), | |
|
390 | (stat.S_ISBLK, "blockdev"), | |
|
391 | (stat.S_ISFIFO, "fifo"), | |
|
392 | (stat.S_ISLNK, "symlink"), | |
|
393 | (stat.S_ISSOCK,"socket"), | |
|
394 | ] | |
|
395 | lstat = self.lstat | |
|
396 | if lstat is not None: | |
|
397 | types = set([text for (func, text) in data if func(lstat.st_mode)]) | |
|
398 | else: | |
|
399 | types = set() | |
|
400 | m = self.mode | |
|
401 | types.update([text for (func, text) in data if func(m)]) | |
|
402 | return ", ".join(types) | |
|
403 | type = property(gettype, None, None, "file type") | |
|
404 | ||
|
405 | def getaccess(self): | |
|
406 | m = self.mode | |
|
407 | data = [ | |
|
408 | (stat.S_IRUSR, "-r"), | |
|
409 | (stat.S_IWUSR, "-w"), | |
|
410 | (stat.S_IXUSR, "-x"), | |
|
411 | (stat.S_IRGRP, "-r"), | |
|
412 | (stat.S_IWGRP, "-w"), | |
|
413 | (stat.S_IXGRP, "-x"), | |
|
414 | (stat.S_IROTH, "-r"), | |
|
415 | (stat.S_IWOTH, "-w"), | |
|
416 | (stat.S_IXOTH, "-x"), | |
|
417 | ] | |
|
418 | return "".join([text[bool(m&bit)] for (bit, text) in data]) | |
|
419 | ||
|
420 | access = property(getaccess, None, None, "Access mode as string") | |
|
421 | ||
|
422 | def getsize(self): | |
|
423 | return int(self.stat.st_size) | |
|
424 | size = property(getsize, None, None, "File size in bytes") | |
|
425 | ||
|
426 | def getblocks(self): | |
|
427 | return self.stat.st_blocks | |
|
428 | blocks = property(getblocks, None, None, "File size in blocks") | |
|
429 | ||
|
430 | def getblksize(self): | |
|
431 | return self.stat.st_blksize | |
|
432 | blksize = property(getblksize, None, None, "Filesystem block size") | |
|
433 | ||
|
434 | def getdev(self): | |
|
435 | return self.stat.st_dev | |
|
436 | dev = property(getdev) | |
|
437 | ||
|
438 | def getnlink(self): | |
|
439 | return self.stat.st_nlink | |
|
440 | nlink = property(getnlink, None, None, "Number of links") | |
|
441 | ||
|
442 | def getuid(self): | |
|
443 | return self.stat.st_uid | |
|
444 | uid = property(getuid, None, None, "User id of file owner") | |
|
445 | ||
|
446 | def getgid(self): | |
|
447 | return self.stat.st_gid | |
|
448 | gid = property(getgid, None, None, "Group id of file owner") | |
|
449 | ||
|
450 | def getowner(self): | |
|
451 | try: | |
|
452 | return pwd.getpwuid(self.stat.st_uid).pw_name | |
|
453 | except KeyError: | |
|
454 | return self.stat.st_uid | |
|
455 | owner = property(getowner, None, None, "Owner name (or id)") | |
|
456 | ||
|
457 | def getgroup(self): | |
|
458 | try: | |
|
459 | return grp.getgrgid(self.stat.st_gid).gr_name | |
|
460 | except KeyError: | |
|
461 | return self.stat.st_gid | |
|
462 | group = property(getgroup, None, None, "Group name (or id)") | |
|
463 | ||
|
464 | def getatime(self): | |
|
465 | return self.stat.st_atime | |
|
466 | atime = property(getatime, None, None, "Access date") | |
|
467 | ||
|
468 | def getadate(self): | |
|
469 | return datetime.datetime.utcfromtimestamp(self.atime) | |
|
470 | adate = property(getadate, None, None, "Access date") | |
|
471 | ||
|
472 | def getctime(self): | |
|
473 | return self.stat.st_ctime | |
|
474 | ctime = property(getctime, None, None, "Creation date") | |
|
475 | ||
|
476 | def getcdate(self): | |
|
477 | return datetime.datetime.utcfromtimestamp(self.ctime) | |
|
478 | cdate = property(getcdate, None, None, "Creation date") | |
|
479 | ||
|
480 | def getmtime(self): | |
|
481 | return self.stat.st_mtime | |
|
482 | mtime = property(getmtime, None, None, "Modification date") | |
|
483 | ||
|
484 | def getmdate(self): | |
|
485 | return datetime.datetime.utcfromtimestamp(self.mtime) | |
|
486 | mdate = property(getmdate, None, None, "Modification date") | |
|
487 | ||
|
488 | def getmimetype(self): | |
|
489 | return mimetypes.guess_type(self.basename)[0] | |
|
490 | mimetype = property(getmimetype, None, None, "MIME type") | |
|
491 | ||
|
492 | def getencoding(self): | |
|
493 | return mimetypes.guess_type(self.basename)[1] | |
|
494 | encoding = property(getencoding, None, None, "Compression") | |
|
495 | ||
|
496 | def getisdir(self): | |
|
497 | return os.path.isdir(self.abspath) | |
|
498 | isdir = property(getisdir, None, None, "Is this a directory?") | |
|
499 | ||
|
500 | def getislink(self): | |
|
501 | return os.path.islink(self.abspath) | |
|
502 | islink = property(getislink, None, None, "Is this a link?") | |
|
503 | ||
|
504 | def __eq__(self, other): | |
|
505 | return self.abspath == other.abspath | |
|
506 | ||
|
507 | def __neq__(self, other): | |
|
508 | return self.abspath != other.abspath | |
|
509 | ||
|
510 | def __xattrs__(self, mode): | |
|
511 | if mode == "detail": | |
|
512 | return ("name", "basename", "abspath", "realpath", "mode", "type", "access", "stat", "lstat", "uid", "gid", "owner", "group", "dev", "nlink", "ctime", "mtime", "atime", "cdate", "mdate", "adate", "size", "blocks", "blksize", "isdir", "islink", "mimetype", "encoding") | |
|
513 | return ("type", "access", "owner", "group", "mdate", "size", "name") | |
|
514 | ||
|
515 | def __xrepr__(self, mode): | |
|
516 | if mode == "header" or mode == "footer": | |
|
517 | name = "ifile" | |
|
518 | try: | |
|
519 | if self.isdir: | |
|
520 | name = "idir" | |
|
521 | except IOError: | |
|
522 | pass | |
|
523 | return "%s(%r)" % (name, self.abspath) | |
|
524 | return repr(self) | |
|
525 | ||
|
526 | def __xiter__(self, mode): | |
|
527 | if self.isdir: | |
|
528 | abspath = self.abspath | |
|
529 | if abspath != os.path.abspath(os.path.join(abspath, os.pardir)): | |
|
530 | yield iparentdir(abspath) | |
|
531 | for name in sorted(os.listdir(abspath), key=lambda n: n.lower()): | |
|
532 | if self.name != os.curdir: | |
|
533 | name = os.path.join(abspath, name) | |
|
534 | yield ifile(name) | |
|
535 | else: | |
|
536 | f = self.open("rb") | |
|
537 | for line in f: | |
|
538 | yield line | |
|
539 | f.close() | |
|
540 | ||
|
541 | def __repr__(self): | |
|
542 | return "%s.%s(%r)" % (self.__class__.__module__, self.__class__.__name__, self.abspath) | |
|
543 | ||
|
544 | ||
|
545 | class iparentdir(ifile): | |
|
546 | def __init__(self, base): | |
|
547 | self._base = base | |
|
548 | self.name = os.pardir | |
|
549 | self._abspath = None | |
|
550 | self._realpath = None | |
|
551 | self._stat = None | |
|
552 | self._lstat = None | |
|
553 | ||
|
554 | def getabspath(self): | |
|
555 | if self._abspath is None: | |
|
556 | self._abspath = os.path.abspath(os.path.join(self._base, self.name)) | |
|
557 | return self._abspath | |
|
558 | abspath = property(getabspath, None, None, "Path to file") | |
|
559 | ||
|
560 | def getrealpath(self): | |
|
561 | if self._realpath is None: | |
|
562 | self._realpath = os.path.realpath(os.path.join(self._base, self.name)) | |
|
563 | return self._realpath | |
|
564 | realpath = property(getrealpath, None, None, "Path with links resolved") | |
|
565 | ||
|
566 | ||
|
567 | class ils(Table): | |
|
568 | def __init__(self, base=os.curdir): | |
|
569 | self.base = os.path.expanduser(base) | |
|
570 | ||
|
571 | def __xiter__(self, mode): | |
|
572 | return xiter(ifile(self.base), mode) | |
|
573 | ||
|
574 | def __xrepr__(self, mode): | |
|
575 | if mode == "header" or mode == "footer": | |
|
576 | return "idir(%r)" % (os.path.abspath(self.base)) | |
|
577 | return repr(self) | |
|
578 | ||
|
579 | def __repr__(self): | |
|
580 | return "%s.%s(%r)" % (self.__class__.__module__, self.__class__.__name__, self.base) | |
|
581 | ||
|
582 | ||
|
583 | class iglob(Table): | |
|
584 | def __init__(self, glob): | |
|
585 | self.glob = glob | |
|
586 | ||
|
587 | def __xiter__(self, mode): | |
|
588 | for name in glob.glob(self.glob): | |
|
589 | yield ifile(name) | |
|
590 | ||
|
591 | def __xrepr__(self, mode): | |
|
592 | if mode == "header" or mode == "footer": | |
|
593 | return "%s(%r)" % (self.__class__.__name__, self.glob) | |
|
594 | return repr(self) | |
|
595 | ||
|
596 | def __repr__(self): | |
|
597 | return "%s.%s(%r)" % (self.__class__.__module__, self.__class__.__name__, self.glob) | |
|
598 | ||
|
599 | ||
|
600 | class iwalk(Table): | |
|
601 | def __init__(self, base=os.curdir, dirs=True, files=True): | |
|
602 | self.base = os.path.expanduser(base) | |
|
603 | self.dirs = dirs | |
|
604 | self.files = files | |
|
605 | ||
|
606 | def __xiter__(self, mode): | |
|
607 | for (dirpath, dirnames, filenames) in os.walk(self.base): | |
|
608 | if self.dirs: | |
|
609 | for name in sorted(dirnames): | |
|
610 | yield ifile(os.path.join(dirpath, name)) | |
|
611 | if self.files: | |
|
612 | for name in sorted(filenames): | |
|
613 | yield ifile(os.path.join(dirpath, name)) | |
|
614 | ||
|
615 | def __xrepr__(self, mode): | |
|
616 | if mode == "header" or mode == "footer": | |
|
617 | return "%s(%r)" % (self.__class__.__name__, self.base) | |
|
618 | return repr(self) | |
|
619 | ||
|
620 | def __repr__(self): | |
|
621 | return "%s.%s(%r)" % (self.__class__.__module__, self.__class__.__name__, self.base) | |
|
622 | ||
|
623 | ||
|
624 | class ipwdentry(object): | |
|
625 | def __init__(self, id): | |
|
626 | self._id = id | |
|
627 | self._entry = None | |
|
628 | ||
|
629 | def _getentry(self): | |
|
630 | if self._entry is None: | |
|
631 | if isinstance(self._id, basestring): | |
|
632 | self._entry = pwd.getpwnam(self._id) | |
|
633 | else: | |
|
634 | self._entry = pwd.getpwuid(self._id) | |
|
635 | return self._entry | |
|
636 | ||
|
637 | def getname(self): | |
|
638 | if isinstance(self._id, basestring): | |
|
639 | return self._id | |
|
640 | else: | |
|
641 | return self._getentry().pw_name | |
|
642 | name = property(getname, None, None, "User name") | |
|
643 | ||
|
644 | def getpasswd(self): | |
|
645 | return self._getentry().pw_passwd | |
|
646 | passwd = property(getpasswd, None, None, "Password") | |
|
647 | ||
|
648 | def getuid(self): | |
|
649 | if isinstance(self._id, basestring): | |
|
650 | return self._getentry().pw_uid | |
|
651 | else: | |
|
652 | return self._id | |
|
653 | uid = property(getuid, None, None, "User id") | |
|
654 | ||
|
655 | def getgid(self): | |
|
656 | return self._getentry().pw_gid | |
|
657 | gid = property(getgid, None, None, "Primary group id") | |
|
658 | ||
|
659 | def getgroup(self): | |
|
660 | return igrpentry(self.gid) | |
|
661 | group = property(getgroup, None, None, "Group") | |
|
662 | ||
|
663 | def getgecos(self): | |
|
664 | return self._getentry().pw_gecos | |
|
665 | gecos = property(getgecos, None, None, "Information (e.g. full user name)") | |
|
666 | ||
|
667 | def getdir(self): | |
|
668 | return self._getentry().pw_dir | |
|
669 | dir = property(getdir, None, None, "$HOME directory") | |
|
670 | ||
|
671 | def getshell(self): | |
|
672 | return self._getentry().pw_shell | |
|
673 | shell = property(getshell, None, None, "Login shell") | |
|
674 | ||
|
675 | def __xattrs__(self, mode): | |
|
676 | return ("name", "passwd", "uid", "gid", "gecos", "dir", "shell") | |
|
677 | ||
|
678 | def __repr__(self): | |
|
679 | return "%s.%s(%r)" % (self.__class__.__module__, self.__class__.__name__, self._id) | |
|
680 | ||
|
681 | ||
|
682 | class ipwd(Table): | |
|
683 | def __iter__(self): | |
|
684 | for entry in pwd.getpwall(): | |
|
685 | yield ipwdentry(entry.pw_name) | |
|
686 | ||
|
687 | def __xrepr__(self, mode): | |
|
688 | if mode == "header" or mode == "footer": | |
|
689 | return "%s()" % self.__class__.__name__ | |
|
690 | return repr(self) | |
|
691 | ||
|
692 | ||
|
693 | class igrpentry(object): | |
|
694 | def __init__(self, id): | |
|
695 | self._id = id | |
|
696 | self._entry = None | |
|
697 | ||
|
698 | def _getentry(self): | |
|
699 | if self._entry is None: | |
|
700 | if isinstance(self._id, basestring): | |
|
701 | self._entry = grp.getgrnam(self._id) | |
|
702 | else: | |
|
703 | self._entry = grp.getgrgid(self._id) | |
|
704 | return self._entry | |
|
705 | ||
|
706 | def getname(self): | |
|
707 | if isinstance(self._id, basestring): | |
|
708 | return self._id | |
|
709 | else: | |
|
710 | return self._getentry().gr_name | |
|
711 | name = property(getname, None, None, "Group name") | |
|
712 | ||
|
713 | def getpasswd(self): | |
|
714 | return self._getentry().gr_passwd | |
|
715 | passwd = property(getpasswd, None, None, "Password") | |
|
716 | ||
|
717 | def getgid(self): | |
|
718 | if isinstance(self._id, basestring): | |
|
719 | return self._getentry().gr_gid | |
|
720 | else: | |
|
721 | return self._id | |
|
722 | gid = property(getgid, None, None, "Group id") | |
|
723 | ||
|
724 | def getmem(self): | |
|
725 | return self._getentry().gr_mem | |
|
726 | mem = property(getmem, None, None, "Members") | |
|
727 | ||
|
728 | def __xattrs__(self, mode): | |
|
729 | return ("name", "passwd", "gid", "mem") | |
|
730 | ||
|
731 | def __xrepr__(self, mode): | |
|
732 | if mode == "header" or mode == "footer": | |
|
733 | return "group %s" % self.name | |
|
734 | return repr(self) | |
|
735 | ||
|
736 | def __xiter__(self, mode): | |
|
737 | for member in self.mem: | |
|
738 | yield ipwdentry(member) | |
|
739 | ||
|
740 | def __repr__(self): | |
|
741 | return "%s.%s(%r)" % (self.__class__.__module__, self.__class__.__name__, self._id) | |
|
742 | ||
|
743 | ||
|
744 | class igrp(Table): | |
|
745 | def __xiter__(self, mode): | |
|
746 | for entry in grp.getgrall(): | |
|
747 | yield igrpentry(entry.gr_name) | |
|
748 | ||
|
749 | def __xrepr__(self, mode): | |
|
750 | if mode == "header" or mode == "footer": | |
|
751 | return "%s()" % self.__class__.__name__ | |
|
752 | return repr(self) | |
|
753 | ||
|
754 | ||
|
755 | class idictentry(object): | |
|
756 | __slots__ = ("key", "value") | |
|
757 | ||
|
758 | def __xattrs__(self, mode): | |
|
759 | return ("key", "value") | |
|
760 | ||
|
761 | def __init__(self, key, value): | |
|
762 | self.key = key | |
|
763 | self.value = value | |
|
764 | ||
|
765 | def __repr__(self): | |
|
766 | return "%s.%s(%r, %r)" % (self.__class__.__module__, self.__class__.__name__, self.key, self.value) | |
|
767 | ||
|
768 | ||
|
769 | class idict(Table): | |
|
770 | def __init__(self, dict): | |
|
771 | self.dict = dict | |
|
772 | ||
|
773 | def __xiter__(self, mode): | |
|
774 | for (key, val) in self.dict.iteritems(): | |
|
775 | yield idictentry(key, val) | |
|
776 | ||
|
777 | ||
|
778 | class ienv(Table): | |
|
779 | def __xiter__(self, mode): | |
|
780 | for (key, val) in os.environ.iteritems(): | |
|
781 | yield idictentry(key, val) | |
|
782 | ||
|
783 | def __xrepr__(self, mode): | |
|
784 | if mode == "header" or mode == "footer": | |
|
785 | return "%s()" % self.__class__.__name__ | |
|
786 | return repr(self) | |
|
787 | ||
|
788 | ||
|
789 | class icsventry(list): | |
|
790 | def __xattrs__(self, mode): | |
|
791 | return xrange(len(self)) | |
|
792 | ||
|
793 | ||
|
794 | class icsv(Pipe): | |
|
795 | def __init__(self, **csvargs): | |
|
796 | self.csvargs = csvargs | |
|
797 | ||
|
798 | def __xiter__(self, mode): | |
|
799 | input = self.input | |
|
800 | if isinstance(input, ifile): | |
|
801 | input = input.open("rb") | |
|
802 | reader = csv.reader(input, **self.csvargs) | |
|
803 | for line in reader: | |
|
804 | yield icsventry(line) | |
|
805 | ||
|
806 | def __xrepr__(self, mode): | |
|
807 | if mode == "header" or mode == "footer": | |
|
808 | input = getattr(self, "input", None) | |
|
809 | if input is not None: | |
|
810 | prefix = "%s | " % xrepr(input, mode) | |
|
811 | else: | |
|
812 | prefix = "" | |
|
813 | return "%s%s(%s)" % (prefix, self.__class__.__name__, ", ".join(["%s=%r" % (key, value) for (key, value) in self.csvargs.iteritems()])) | |
|
814 | return repr(self) | |
|
815 | ||
|
816 | def __repr__(self): | |
|
817 | return "<%s.%s %s at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, ", ".join(["%s=%r" % (key, value) for (key, value) in self.csvargs.iteritems()]), id(self)) | |
|
818 | ||
|
819 | ||
|
820 | class ix(Table): | |
|
821 | def __init__(self, cmd): | |
|
822 | self.cmd = cmd | |
|
823 | self._pipe = None | |
|
824 | ||
|
825 | def __xiter__(self, mode): | |
|
826 | self._pipe = os.popen(self.cmd) | |
|
827 | for l in self._pipe: | |
|
828 | yield l.rstrip("\r\n") | |
|
829 | ||
|
830 | def __del__(self): | |
|
831 | if self._pipe is not None and not self._pipe.closed: | |
|
832 | self._pipe.close() | |
|
833 | self._pipe = None | |
|
834 | ||
|
835 | def __xrepr__(self, mode): | |
|
836 | if mode == "header" or mode == "footer": | |
|
837 | return "%s(%r)" % (self.__class__.__name__, self.cmd) | |
|
838 | return repr(self) | |
|
839 | ||
|
840 | def __repr__(self): | |
|
841 | return "%s.%s(%r)" % (self.__class__.__module__, self.__class__.__name__, self.cmd) | |
|
842 | ||
|
843 | ||
|
844 | class ifilter(Pipe): | |
|
845 | def __init__(self, expr): | |
|
846 | self.expr = expr | |
|
847 | ||
|
848 | def __xiter__(self, mode): | |
|
849 | if callable(self.expr): | |
|
850 | for item in xiter(self.input, mode): | |
|
851 | try: | |
|
852 | if self.expr(item): | |
|
853 | yield item | |
|
854 | except (KeyboardInterrupt, SystemExit): | |
|
855 | raise | |
|
856 | except Exception: | |
|
857 | pass # Ignore errors | |
|
858 | else: | |
|
859 | for item in xiter(self.input, mode): | |
|
860 | try: | |
|
861 | if eval(self.expr, globals(), _AttrNamespace(item)): | |
|
862 | yield item | |
|
863 | except (KeyboardInterrupt, SystemExit): | |
|
864 | raise | |
|
865 | except Exception: | |
|
866 | pass # Ignore errors | |
|
867 | ||
|
868 | def __xrepr__(self, mode): | |
|
869 | if mode == "header" or mode == "footer": | |
|
870 | input = getattr(self, "input", None) | |
|
871 | if input is not None: | |
|
872 | prefix = "%s | " % xrepr(input, mode) | |
|
873 | else: | |
|
874 | prefix = "" | |
|
875 | return "%s%s(%s)"% (prefix, self.__class__.__name__, xrepr(self.expr, mode)) | |
|
876 | return repr(self) | |
|
877 | ||
|
878 | def __repr__(self): | |
|
879 | return "<%s.%s expr=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.expr, id(self)) | |
|
880 | ||
|
881 | ||
|
882 | class ieval(Pipe): | |
|
883 | def __init__(self, expr): | |
|
884 | self.expr = expr | |
|
885 | ||
|
886 | def __xiter__(self, mode): | |
|
887 | if callable(self.expr): | |
|
888 | for item in xiter(self.input, mode): | |
|
889 | try: | |
|
890 | yield self.expr(item) | |
|
891 | except (KeyboardInterrupt, SystemExit): | |
|
892 | raise | |
|
893 | except Exception: | |
|
894 | pass # Ignore errors | |
|
895 | else: | |
|
896 | for item in xiter(self.input, mode): | |
|
897 | try: | |
|
898 | yield eval(self.expr, globals(), _AttrNamespace(item)) | |
|
899 | except (KeyboardInterrupt, SystemExit): | |
|
900 | raise | |
|
901 | except Exception: | |
|
902 | pass # Ignore errors | |
|
903 | ||
|
904 | def __xrepr__(self, mode): | |
|
905 | if mode == "header" or mode == "footer": | |
|
906 | input = getattr(self, "input", None) | |
|
907 | if input is not None: | |
|
908 | prefix = "%s | " % xrepr(input, mode) | |
|
909 | else: | |
|
910 | prefix = "" | |
|
911 | return "%s%s(%s)"% (prefix, self.__class__.__name__, xrepr(self.expr, mode)) | |
|
912 | return repr(self) | |
|
913 | ||
|
914 | def __repr__(self): | |
|
915 | return "<%s.%s expr=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.expr, id(self)) | |
|
916 | ||
|
917 | ||
|
918 | class ienumentry(object): | |
|
919 | __slots__ = ("index", "object") | |
|
920 | ||
|
921 | def __init__(self, index, object): | |
|
922 | self.index = index | |
|
923 | self.object = object | |
|
924 | ||
|
925 | def __xattrs__(self, mode): | |
|
926 | return ("index", "object") | |
|
927 | ||
|
928 | def __xrepr__(self, mode): | |
|
929 | if mode == "header" or mode == "footer": | |
|
930 | input = getattr(self, "input", None) | |
|
931 | if input is not None: | |
|
932 | prefix = "%s | " % xrepr(input, mode) | |
|
933 | else: | |
|
934 | prefix = "" | |
|
935 | return "%s%s()"% (prefix, self.__class__.__name__) | |
|
936 | return repr(self) | |
|
937 | ||
|
938 | ||
|
939 | class ienum(Pipe): | |
|
940 | def __xiter__(self, mode): | |
|
941 | for (index, object) in enumerate(xiter(self.input, mode)): | |
|
942 | yield ienumentry(index, object) | |
|
943 | ||
|
944 | ||
|
945 | class isort(Pipe): | |
|
946 | def __init__(self, key, reverse=False): | |
|
947 | self.key = key | |
|
948 | self.reverse = reverse | |
|
949 | ||
|
950 | def __xiter__(self, mode): | |
|
951 | if callable(self.key): | |
|
952 | items = sorted(xiter(self.input, mode), key=self.key, reverse=self.reverse) | |
|
953 | else: | |
|
954 | def key(item): | |
|
955 | return eval(self.key, globals(), _AttrNamespace(item)) | |
|
956 | items = sorted(xiter(self.input, mode), key=key, reverse=self.reverse) | |
|
957 | for item in items: | |
|
958 | yield item | |
|
959 | ||
|
960 | def __xrepr__(self, mode): | |
|
961 | if mode == "header" or mode == "footer": | |
|
962 | input = getattr(self, "input", None) | |
|
963 | if input is not None: | |
|
964 | prefix = "%s | " % xrepr(input, mode) | |
|
965 | else: | |
|
966 | prefix = "" | |
|
967 | if self.reverse: | |
|
968 | return "%s%s(%r, %r)" % (prefix, self.__class__.__name__, self.key, self.reverse) | |
|
969 | else: | |
|
970 | return "%s%s(%r)" % (prefix, self.__class__.__name__, self.key) | |
|
971 | return repr(self) | |
|
972 | ||
|
973 | def __repr__(self): | |
|
974 | return "<%s.%s key=%r reverse=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.key, self.reverse, id(self)) | |
|
975 | ||
|
976 | ||
|
977 | tab = 3 # for expandtabs() | |
|
978 | ||
|
979 | def _format(field): | |
|
980 | if isinstance(field, str): | |
|
981 | text = repr(field.expandtabs(tab))[1:-1] | |
|
982 | elif isinstance(field, unicode): | |
|
983 | text = repr(field.expandtabs(tab))[2:-1] | |
|
984 | elif isinstance(field, datetime.datetime): | |
|
985 | # Don't use strftime() here, as this requires year >= 1900 | |
|
986 | text = "%04d-%02d-%02d %02d:%02d:%02d.%06d" % (field.year, field.month, field.day, field.hour, field.minute, field.second, field.microsecond) | |
|
987 | elif isinstance(field, datetime.date): | |
|
988 | text = "%04d-%02d-%02d" % (field.year, field.month, field.day) | |
|
989 | else: | |
|
990 | text = repr(field) | |
|
991 | return text | |
|
992 | ||
|
993 | ||
|
994 | class Display(object): | |
|
995 | class __metaclass__(type): | |
|
996 | def __ror__(self, input): | |
|
997 | return input | self() | |
|
998 | ||
|
999 | def __ror__(self, input): | |
|
1000 | self.input = input | |
|
1001 | return self | |
|
1002 | ||
|
1003 | def display(self): | |
|
1004 | pass | |
|
1005 | ||
|
1006 | ||
|
1007 | class iless(Display): | |
|
1008 | cmd = "less --quit-if-one-screen --LONG-PROMPT --LINE-NUMBERS --chop-long-lines --shift=8 --RAW-CONTROL-CHARS" | |
|
1009 | ||
|
1010 | def display(self): | |
|
1011 | try: | |
|
1012 | pager = os.popen(self.cmd, "w") | |
|
1013 | try: | |
|
1014 | for item in xiter(self.input, "default"): | |
|
1015 | attrs = xattrs(item, "default") | |
|
1016 | pager.write(" ".join(["%s=%s" % (a, _format(_getattr(item, a))) for a in attrs])) | |
|
1017 | pager.write("\n") | |
|
1018 | finally: | |
|
1019 | pager.close() | |
|
1020 | except Exception, exc: | |
|
1021 | print "%s: %s" % (exc.__class__.__name__, str(exc)) | |
|
1022 | ||
|
1023 | ||
|
1024 | class idump(Display): | |
|
1025 | def __init__(self, *attrs): | |
|
1026 | self.attrs = attrs | |
|
1027 | self.headerpadchar = " " | |
|
1028 | self.headersepchar = "|" | |
|
1029 | self.datapadchar = " " | |
|
1030 | self.datasepchar = "|" | |
|
1031 | ||
|
1032 | def display(self): | |
|
1033 | stream = sys.stdout | |
|
1034 | if self.attrs: | |
|
1035 | rows = [] | |
|
1036 | colwidths = dict([(attrname, len(_attrname(attrname))) for attrname in self.attrs]) | |
|
1037 | ||
|
1038 | for item in xiter(self.input, "default"): | |
|
1039 | row = {} | |
|
1040 | for attrname in self.attrs: | |
|
1041 | value = _getattr(item, attrname, None) | |
|
1042 | text = _format(value) | |
|
1043 | colwidths[attrname] = max(colwidths[attrname], width) | |
|
1044 | row[attrname] = (value, text) | |
|
1045 | rows.append(row) | |
|
1046 | ||
|
1047 | for (i, attrname) in enumerate(self.attrs): | |
|
1048 | stream.write(_attrname(attrname)) | |
|
1049 | spc = colwidths[attrname] - len(_attrname(attrname)) | |
|
1050 | if i < len(colwidths)-1: | |
|
1051 | if spc>0: | |
|
1052 | stream.write(self.headerpadchar * spc) | |
|
1053 | stream.write(self.headersepchar) | |
|
1054 | stream.write("\n") | |
|
1055 | ||
|
1056 | for row in rows: | |
|
1057 | for (i, attrname) in enumerate(self.attrs): | |
|
1058 | (value, text) = row[attrname] | |
|
1059 | spc = colwidths[attrname] - len(text) | |
|
1060 | if isinstance(value, (int, long)): | |
|
1061 | if spc>0: | |
|
1062 | stream.write(self.datapadchar*spc) | |
|
1063 | stream.write(text) | |
|
1064 | else: | |
|
1065 | stream.write(text) | |
|
1066 | if i < len(colwidths)-1: | |
|
1067 | if spc>0: | |
|
1068 | stream.write(self.datapadchar*spc) | |
|
1069 | if i < len(colwidths)-1: | |
|
1070 | stream.write(self.datasepchar) | |
|
1071 | stream.write("\n") | |
|
1072 | else: | |
|
1073 | allattrs = [] | |
|
1074 | allattrset = set() | |
|
1075 | colwidths = {} | |
|
1076 | rows = [] | |
|
1077 | for item in xiter(self.input, "default"): | |
|
1078 | row = {} | |
|
1079 | attrs = xattrs(item, "default") | |
|
1080 | for attrname in attrs: | |
|
1081 | if attrname not in allattrset: | |
|
1082 | allattrs.append(attrname) | |
|
1083 | allattrset.add(attrname) | |
|
1084 | colwidths[attrname] = len(_attrname(attrname)) | |
|
1085 | value = _getattr(item, attrname, None) | |
|
1086 | text = _format(value) | |
|
1087 | colwidths[attrname] = max(colwidths[attrname], len(text)) | |
|
1088 | row[attrname] = (value, text) | |
|
1089 | rows.append(row) | |
|
1090 | ||
|
1091 | for (i, attrname) in enumerate(allattrs): | |
|
1092 | stream.write(_attrname(attrname)) | |
|
1093 | spc = colwidths[attrname] - len(_attrname(attrname)) | |
|
1094 | if i < len(colwidths)-1: | |
|
1095 | if spc>0: | |
|
1096 | stream.write(self.headerpadchar*spc) | |
|
1097 | stream.write(self.headersepchar) | |
|
1098 | stream.write("\n") | |
|
1099 | ||
|
1100 | for row in rows: | |
|
1101 | for (i, attrname) in enumerate(attrs): | |
|
1102 | (value, text) = row.get(attrname, ("", "")) | |
|
1103 | spc = colwidths[attrname] - len(text) | |
|
1104 | if isinstance(value, (int, long)): | |
|
1105 | if spc>0: | |
|
1106 | stream.write(self.datapadchar*spc) | |
|
1107 | stream.write(text) | |
|
1108 | else: | |
|
1109 | stream.write(text) | |
|
1110 | if i < len(colwidths)-1: | |
|
1111 | if spc>0: | |
|
1112 | stream.write(self.datapadchar*spc) | |
|
1113 | if i < len(colwidths)-1: | |
|
1114 | stream.write(self.datasepchar) | |
|
1115 | stream.write("\n") | |
|
1116 | ||
|
1117 | ||
|
1118 | class XMode(object): | |
|
1119 | def __init__(self, object, mode, title=None, description=None): | |
|
1120 | self.object = object | |
|
1121 | self.mode = mode | |
|
1122 | self.title = title | |
|
1123 | self.description = description | |
|
1124 | ||
|
1125 | def __repr__(self): | |
|
1126 | return "<%s.%s object mode=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.mode, id(self)) | |
|
1127 | ||
|
1128 | def __xrepr__(self, mode): | |
|
1129 | if mode == "header" or mode == "footer": | |
|
1130 | return self.title | |
|
1131 | return repr(self) | |
|
1132 | ||
|
1133 | def __xattrs__(self, mode): | |
|
1134 | if mode == "detail": | |
|
1135 | return ("object", "mode", "title", "description") | |
|
1136 | return ("title", "description") | |
|
1137 | ||
|
1138 | def __xiter__(self, mode): | |
|
1139 | return xiter(self.object, self.mode) | |
|
1140 | ||
|
1141 | ||
|
1142 | class XAttr(object): | |
|
1143 | def __init__(self, object, name): | |
|
1144 | self.name = _attrname(name) | |
|
1145 | ||
|
1146 | try: | |
|
1147 | self.value = _getattr(object, name) | |
|
1148 | except (KeyboardInterrupt, SystemExit): | |
|
1149 | raise | |
|
1150 | except Exception, exc: | |
|
1151 | if exc.__class__.__module__ == "exceptions": | |
|
1152 | self.value = exc.__class__.__name__ | |
|
1153 | else: | |
|
1154 | self.value = "%s.%s" % (exc.__class__.__module__, exc.__class__.__name__) | |
|
1155 | self.type = self.value | |
|
1156 | else: | |
|
1157 | t = type(self.value) | |
|
1158 | if t.__module__ == "__builtin__": | |
|
1159 | self.type = t.__name__ | |
|
1160 | else: | |
|
1161 | self.type = "%s.%s" % (t.__module__, t.__name__) | |
|
1162 | ||
|
1163 | doc = None | |
|
1164 | if isinstance(name, basestring): | |
|
1165 | try: | |
|
1166 | meta = getattr(type(object), name) | |
|
1167 | except AttributeError: | |
|
1168 | pass | |
|
1169 | else: | |
|
1170 | if isinstance(meta, property): | |
|
1171 | self.doc = getattr(meta, "__doc__", None) | |
|
1172 | elif callable(name): | |
|
1173 | try: | |
|
1174 | self.doc = name.__doc__ | |
|
1175 | except AttributeError: | |
|
1176 | pass | |
|
1177 | ||
|
1178 | def __xattrs__(self, mode): | |
|
1179 | return ("name", "type", "doc", "value") | |
|
1180 | ||
|
1181 | ||
|
1182 | if curses is not None: | |
|
1183 | class UnassignedKeyError(Exception): | |
|
1184 | pass | |
|
1185 | ||
|
1186 | ||
|
1187 | class UnknownCommandError(Exception): | |
|
1188 | pass | |
|
1189 | ||
|
1190 | ||
|
1191 | class CommandError(Exception): | |
|
1192 | pass | |
|
1193 | ||
|
1194 | ||
|
1195 | class Style(object): | |
|
1196 | __slots__ = ("fg", "bg", "attrs") | |
|
1197 | ||
|
1198 | def __init__(self, fg, bg, attrs=0): | |
|
1199 | self.fg = fg | |
|
1200 | self.bg = bg | |
|
1201 | self.attrs = attrs | |
|
1202 | ||
|
1203 | ||
|
1204 | class _BrowserCachedItem(object): | |
|
1205 | __slots__ = ("item", "marked") | |
|
1206 | ||
|
1207 | def __init__(self, item): | |
|
1208 | self.item = item | |
|
1209 | self.marked = False | |
|
1210 | ||
|
1211 | ||
|
1212 | class _BrowserHelp(list): | |
|
1213 | def __init__(self, browser, *items): | |
|
1214 | list.__init__(self, items) | |
|
1215 | self.browser = browser | |
|
1216 | ||
|
1217 | def __xrepr__(self, mode): | |
|
1218 | if mode == "header" or mode == "footer": | |
|
1219 | return "ibrowse help screen" | |
|
1220 | return repr(self) | |
|
1221 | ||
|
1222 | def __xiter__(self, mode): | |
|
1223 | # Get reverse key mapping | |
|
1224 | allkeys = {} | |
|
1225 | for (key, cmd) in self.browser.keymap.iteritems(): | |
|
1226 | allkeys.setdefault(cmd, []).append(key) | |
|
1227 | ||
|
1228 | for (cmd, description) in list.__iter__(self): | |
|
1229 | if not description: | |
|
1230 | yield _BrowserHelpLine("", "", "") | |
|
1231 | else: | |
|
1232 | keys = allkeys.get(cmd, []) | |
|
1233 | lines = description.splitlines(False) | |
|
1234 | for i in xrange(max(len(keys), len(lines))): | |
|
1235 | if i: | |
|
1236 | cmd = "" | |
|
1237 | try: | |
|
1238 | key = self.browser.keylabel(keys[i]) | |
|
1239 | except IndexError: | |
|
1240 | key = "" | |
|
1241 | try: | |
|
1242 | line = lines[i] | |
|
1243 | except IndexError: | |
|
1244 | line = "" | |
|
1245 | yield _BrowserHelpLine(key, cmd, line) | |
|
1246 | ||
|
1247 | ||
|
1248 | class _BrowserHelpLine(object): | |
|
1249 | def __init__(self, key, command, description): | |
|
1250 | self.key = key | |
|
1251 | self.command = command | |
|
1252 | self.description = description | |
|
1253 | ||
|
1254 | def __xattrs__(self, mode): | |
|
1255 | return ("key", "command", "description") | |
|
1256 | ||
|
1257 | ||
|
1258 | class _BrowserLevel(object): | |
|
1259 | def __init__(self, browser, input, iterator, mainsizey, *attrs): | |
|
1260 | self.browser = browser | |
|
1261 | self.input = input | |
|
1262 | self.header = xrepr(input, "header") | |
|
1263 | self.iterator = iterator # iterator for the input | |
|
1264 | self.exhausted = False # is the iterator exhausted? | |
|
1265 | self.attrs = attrs | |
|
1266 | self.items = deque() | |
|
1267 | self.marked = 0 # Number of marked objects | |
|
1268 | self.cury = 0 # Vertical cursor position | |
|
1269 | self.curx = 0 # Horizontal cursor position | |
|
1270 | self.datastarty = 0 # Index of first data line | |
|
1271 | self.datastartx = 0 # Index of first data column | |
|
1272 | self.mainsizey = mainsizey # height of the data display area | |
|
1273 | self.mainsizex = 0 # width of the data display area | |
|
1274 | self.numbersizex = 0 # Length of the number at the left edge of the screen | |
|
1275 | self.displayattrs = [] # Names of attributes to display (in this order) | |
|
1276 | self.displayattr = _default # Name of attribute under the cursor | |
|
1277 | self.colwidths = {} # Maps attribute names to column widths | |
|
1278 | ||
|
1279 | self.fetch(mainsizey) | |
|
1280 | self.calcdisplayattrs() | |
|
1281 | # formatted attributes for the items on screen (i.e. self.items[self.datastarty:self.datastarty+self.mainsizey]) | |
|
1282 | self.displayrows = [self.getrow(i) for i in xrange(len(self.items))] | |
|
1283 | self.calcwidths() | |
|
1284 | self.calcdisplayattr() | |
|
1285 | ||
|
1286 | def fetch(self, count): | |
|
1287 | have = len(self.items) | |
|
1288 | while not self.exhausted and have < count: | |
|
1289 | try: | |
|
1290 | item = self.iterator.next() | |
|
1291 | except StopIteration: | |
|
1292 | self.exhausted = True | |
|
1293 | break | |
|
1294 | else: | |
|
1295 | have += 1 | |
|
1296 | self.items.append(_BrowserCachedItem(item)) | |
|
1297 | ||
|
1298 | def calcdisplayattrs(self): | |
|
1299 | attrnames = set() | |
|
1300 | if self.attrs: | |
|
1301 | self.displayattrs = self.attrs | |
|
1302 | else: | |
|
1303 | self.displayattrs = [] | |
|
1304 | for i in xrange(self.datastarty, min(self.datastarty+self.mainsizey, len(self.items))): | |
|
1305 | for attrname in xattrs(self.items[i].item, "default"): | |
|
1306 | if attrname not in attrnames: | |
|
1307 | self.displayattrs.append(attrname) | |
|
1308 | attrnames.add(attrname) | |
|
1309 | ||
|
1310 | def getrow(self, i): | |
|
1311 | row = {} | |
|
1312 | item = self.items[i].item | |
|
1313 | for attrname in self.displayattrs: | |
|
1314 | try: | |
|
1315 | value = _getattr(item, attrname, _default) | |
|
1316 | except (KeyboardInterrupt, SystemExit): | |
|
1317 | raise | |
|
1318 | except Exception, exc: | |
|
1319 | row[attrname] = self.browser.format(exc) | |
|
1320 | else: | |
|
1321 | if value is not _default: # only store attribute if it exists | |
|
1322 | row[attrname] = self.browser.format(value) | |
|
1323 | return row | |
|
1324 | ||
|
1325 | def calcwidths(self): | |
|
1326 | # Recalculate the displayed fields and their width | |
|
1327 | self.colwidths = {} | |
|
1328 | for row in self.displayrows: | |
|
1329 | for attrname in self.displayattrs: | |
|
1330 | (align, text, style) = row.get(attrname, (2, "", None)) | |
|
1331 | if attrname not in self.colwidths: # always add attribute to colwidths, even if the attribute doesn't exist | |
|
1332 | self.colwidths[attrname] = len(_attrname(attrname)) | |
|
1333 | self.colwidths[attrname] = max(self.colwidths[attrname], len(text)) | |
|
1334 | ||
|
1335 | self.numbersizex = len(str(self.datastarty+self.mainsizey-1)) # How many characters do we need to paint the item number? | |
|
1336 | self.mainsizex = self.browser.scrsizex-self.numbersizex-3 # How must space have we got to display data? | |
|
1337 | self.datasizex = sum(self.colwidths.itervalues()) + len(self.colwidths) # width of all columns | |
|
1338 | ||
|
1339 | def calcdisplayattr(self): | |
|
1340 | # Find out on which attribute the cursor is | |
|
1341 | pos = 0 | |
|
1342 | for attrname in self.displayattrs: | |
|
1343 | if pos+self.colwidths[attrname] >= self.curx: | |
|
1344 | self.displayattr = attrname | |
|
1345 | break | |
|
1346 | pos += self.colwidths[attrname]+1 | |
|
1347 | else: | |
|
1348 | self.displayattr = None | |
|
1349 | ||
|
1350 | def moveto(self, x, y, refresh=False): | |
|
1351 | olddatastarty = self.datastarty | |
|
1352 | oldx = self.curx | |
|
1353 | oldy = self.cury | |
|
1354 | x = int(x+0.5) | |
|
1355 | y = int(y+0.5) | |
|
1356 | ||
|
1357 | scrollbordery = min(self.browser.scrollbordery, self.mainsizey//2) | |
|
1358 | scrollborderx = min(self.browser.scrollborderx, self.mainsizex//2) | |
|
1359 | ||
|
1360 | # Make sure that the cursor didn't leave the main area vertically | |
|
1361 | if y < 0: | |
|
1362 | y = 0 | |
|
1363 | self.fetch(y+scrollbordery+1) # try to get more items | |
|
1364 | if y >= len(self.items): | |
|
1365 | y = max(0, len(self.items)-1) | |
|
1366 | ||
|
1367 | # Make sure that the cursor stays on screen vertically | |
|
1368 | if y < self.datastarty+scrollbordery: | |
|
1369 | self.datastarty = max(0, y-scrollbordery) | |
|
1370 | elif y >= self.datastarty+self.mainsizey-scrollbordery: | |
|
1371 | self.datastarty = max(0, min(y-self.mainsizey+scrollbordery+1, len(self.items)-self.mainsizey)) | |
|
1372 | ||
|
1373 | if refresh: # Do we need to refresh the complete display? | |
|
1374 | self.calcdisplayattrs() | |
|
1375 | self.displayrows = [self.getrow(i) for i in xrange(self.datastarty, min(self.datastarty+self.mainsizey, len(self.items)))] | |
|
1376 | self.calcwidths() | |
|
1377 | # Did we scroll vertically => update displayrows and various other attributes | |
|
1378 | elif self.datastarty != olddatastarty: | |
|
1379 | # Recalculate which attributes we have to display | |
|
1380 | olddisplayattrs = self.displayattrs | |
|
1381 | self.calcdisplayattrs() | |
|
1382 | if self.displayattrs != olddisplayattrs: # There are new attributes => recreate cache | |
|
1383 | self.displayrows = [self.getrow(i) for i in xrange(self.datastarty, min(self.datastarty+self.mainsizey, len(self.items)))] | |
|
1384 | elif self.datastarty<olddatastarty: # we did scroll up | |
|
1385 | del self.displayrows[self.datastarty-olddatastarty:] # drop rows from the end | |
|
1386 | for i in xrange(olddatastarty-1, self.datastarty-1, -1): # fetch new items | |
|
1387 | try: | |
|
1388 | row = self.getrow(i) | |
|
1389 | except IndexError: # we don't have enough objects to fill the screen | |
|
1390 | break | |
|
1391 | self.displayrows.insert(0, row) | |
|
1392 | else: # we did scroll down | |
|
1393 | del self.displayrows[:self.datastarty-olddatastarty] # drop rows from the start | |
|
1394 | for i in xrange(olddatastarty+self.mainsizey, self.datastarty+self.mainsizey): # fetch new items | |
|
1395 | try: | |
|
1396 | row = self.getrow(i) | |
|
1397 | except IndexError: # we don't have enough objects to fill the screen | |
|
1398 | break | |
|
1399 | self.displayrows.append(row) | |
|
1400 | self.calcwidths() | |
|
1401 | ||
|
1402 | # Make sure that the cursor didn't leave the data area horizontally | |
|
1403 | if x < 0: | |
|
1404 | x = 0 | |
|
1405 | elif x >= self.datasizex: | |
|
1406 | x = max(0, self.datasizex-1) | |
|
1407 | ||
|
1408 | # Make sure that the cursor stays on screen horizontally | |
|
1409 | if x < self.datastartx+scrollborderx: | |
|
1410 | self.datastartx = max(0, x-scrollborderx) | |
|
1411 | elif x >= self.datastartx+self.mainsizex-scrollborderx: | |
|
1412 | self.datastartx = max(0, min(x-self.mainsizex+scrollborderx+1, self.datasizex-self.mainsizex)) | |
|
1413 | ||
|
1414 | if x == oldx and y == oldy: # couldn't move | |
|
1415 | if self.browser._dobeep: | |
|
1416 | curses.beep() | |
|
1417 | self.browser._dobeep = False # don't beep again (as long as the same key is pressed) | |
|
1418 | else: | |
|
1419 | self.curx = x | |
|
1420 | self.cury = y | |
|
1421 | self.calcdisplayattr() | |
|
1422 | ||
|
1423 | def sort(self, key, reverse=False): | |
|
1424 | """ | |
|
1425 | Sort the currently list of items using the key function ``key``. If | |
|
1426 | ``reverse`` is true the sort order is reversed. | |
|
1427 | """ | |
|
1428 | curitem = self.items[self.cury] # Remember where the cursor is now | |
|
1429 | ||
|
1430 | # Sort items | |
|
1431 | def realkey(item): | |
|
1432 | return key(item.item) | |
|
1433 | self.items = deque(sorted(self.items, key=realkey, reverse=reverse)) | |
|
1434 | ||
|
1435 | # Find out where the object under the cursor went | |
|
1436 | cury = self.cury | |
|
1437 | for (i, item) in enumerate(self.items): | |
|
1438 | if item is curitem: | |
|
1439 | cury = i | |
|
1440 | break | |
|
1441 | ||
|
1442 | self.moveto(self.curx, cury, refresh=True) | |
|
1443 | ||
|
1444 | ||
|
1445 | class ibrowse(Display): | |
|
1446 | pageoverlapx = 1 # Show this many lines from the previous screen when paging horizontally | |
|
1447 | pageoverlapy = 1 # Show this many lines from the previous screen when paging vertically | |
|
1448 | scrollborderx = 10 # Start scrolling when the cursor is less than this number of columns away from the left or right screen edge | |
|
1449 | scrollbordery = 5 # Start scrolling when the cursor is less than this number of lines away from the top or bottom screen edge | |
|
1450 | acceleratex = 1.05 # Accelerate by this factor when scrolling horizontally | |
|
1451 | acceleratey = 1.05 # Accelerate by this factor when scrolling vertically | |
|
1452 | maxspeedx = 0.5 # The maximum horizontal scroll speed (as a factor of the screen width (i.e. 0.5 == half a screen width) | |
|
1453 | maxspeedy = 0.5 # The maximum vertical scroll speed (as a factor of the screen height (i.e. 0.5 == half a screen height) | |
|
1454 | maxheaders = 5 # How many header lines (for nested browser levels) do we want? | |
|
1455 | ||
|
1456 | style_objheadertext = Style(curses.COLOR_WHITE, curses.COLOR_BLACK, curses.A_BOLD|curses.A_REVERSE) | |
|
1457 | style_objheadernumber = Style(curses.COLOR_WHITE, curses.COLOR_BLUE, curses.A_BOLD|curses.A_REVERSE) | |
|
1458 | style_objheaderobject = Style(curses.COLOR_WHITE, curses.COLOR_BLACK, curses.A_REVERSE) | |
|
1459 | style_colheader = Style(curses.COLOR_BLUE, curses.COLOR_WHITE, curses.A_REVERSE) | |
|
1460 | style_colheaderhere = Style(curses.COLOR_GREEN, curses.COLOR_BLACK, curses.A_BOLD|curses.A_REVERSE) | |
|
1461 | style_colheadersep = Style(curses.COLOR_BLUE, curses.COLOR_BLACK, curses.A_REVERSE) | |
|
1462 | style_number = Style(curses.COLOR_BLUE, curses.COLOR_WHITE, curses.A_REVERSE) | |
|
1463 | style_numberhere = Style(curses.COLOR_GREEN, curses.COLOR_BLACK, curses.A_BOLD|curses.A_REVERSE) | |
|
1464 | style_sep = Style(curses.COLOR_BLUE, curses.COLOR_BLACK) | |
|
1465 | style_data = Style(curses.COLOR_WHITE, curses.COLOR_BLACK) | |
|
1466 | style_datapad = Style(curses.COLOR_BLUE, curses.COLOR_BLACK, curses.A_BOLD) | |
|
1467 | style_footer = Style(curses.COLOR_BLACK, curses.COLOR_WHITE) | |
|
1468 | style_noattr = Style(curses.COLOR_RED, curses.COLOR_BLACK) | |
|
1469 | style_error = Style(curses.COLOR_RED, curses.COLOR_BLACK) | |
|
1470 | style_default = Style(curses.COLOR_WHITE, curses.COLOR_BLACK) | |
|
1471 | style_report = Style(curses.COLOR_WHITE, curses.COLOR_BLACK) | |
|
1472 | ||
|
1473 | # Styles for datatype display | |
|
1474 | style_type_none = Style(curses.COLOR_MAGENTA, curses.COLOR_BLACK) | |
|
1475 | style_type_bool = Style(curses.COLOR_GREEN, curses.COLOR_BLACK) | |
|
1476 | style_type_number = Style(curses.COLOR_YELLOW, curses.COLOR_BLACK) | |
|
1477 | style_type_datetime = Style(curses.COLOR_CYAN, curses.COLOR_BLACK) | |
|
1478 | ||
|
1479 | headersepchar = "|" # Column separator in header | |
|
1480 | datapadchar = "." # Chararacter for padding data cell entries | |
|
1481 | datasepchar = "|" # Column separator | |
|
1482 | nodatachar = "-" # Character to use for "empty" cell (i.e. for non-existing attributes) | |
|
1483 | ||
|
1484 | # Maps curses key codes to "function" names | |
|
1485 | keymap = { | |
|
1486 | ord("q"): "quit", | |
|
1487 | curses.KEY_UP: "up", | |
|
1488 | curses.KEY_DOWN: "down", | |
|
1489 | curses.KEY_PPAGE: "pageup", | |
|
1490 | curses.KEY_NPAGE: "pagedown", | |
|
1491 | curses.KEY_LEFT: "left", | |
|
1492 | curses.KEY_RIGHT: "right", | |
|
1493 | curses.KEY_HOME: "home", | |
|
1494 | curses.KEY_END: "end", | |
|
1495 | ord("p"): "pick", | |
|
1496 | ord("P"): "pickattr", | |
|
1497 | ord("m"): "pickmarked", | |
|
1498 | ord("M"): "pickmarkedattr", | |
|
1499 | ord("\n"): "enterdefault", | |
|
1500 | # FIXME: What's happening here? | |
|
1501 | 8: "leave", | |
|
1502 | 127: "leave", | |
|
1503 | curses.KEY_BACKSPACE: "leave", | |
|
1504 | ord("x"): "leave", | |
|
1505 | ord("h"): "help", | |
|
1506 | ord("e"): "enter", | |
|
1507 | ord("E"): "enterattr", | |
|
1508 | ord("d"): "detail", | |
|
1509 | ord(" "): "tooglemark", | |
|
1510 | ord("v"): "sortcolumnasc", | |
|
1511 | ord("V"): "sortcolumndesc", | |
|
1512 | } | |
|
1513 | ||
|
1514 | def __init__(self, *attrs): | |
|
1515 | self.attrs = attrs | |
|
1516 | self.levels = [] | |
|
1517 | self.stepx = 1. # how many colums to scroll | |
|
1518 | self.stepy = 1. # how many rows to scroll | |
|
1519 | self._dobeep = True # Beep on the edges of the data area? | |
|
1520 | self._colors = {} | |
|
1521 | self._maxcolor = 1 | |
|
1522 | self._headerlines = 1 # How many header lines do we want to paint (the numbers of levels we have, but with an upper bound) | |
|
1523 | self._firstheaderline = 0 # Index of first header line | |
|
1524 | self.scr = None # curses window | |
|
1525 | self._report = None # report in the footer line | |
|
1526 | ||
|
1527 | def nextstepx(self, step): | |
|
1528 | return max(1., min(step*self.acceleratex, self.maxspeedx*self.levels[-1].mainsizex)) | |
|
1529 | ||
|
1530 | def nextstepy(self, step): | |
|
1531 | return max(1., min(step*self.acceleratey, self.maxspeedy*self.levels[-1].mainsizey)) | |
|
1532 | ||
|
1533 | def getstyle(self, style): | |
|
1534 | try: | |
|
1535 | return self._colors[style.fg, style.bg] | style.attrs | |
|
1536 | except KeyError: | |
|
1537 | curses.init_pair(self._maxcolor, style.fg, style.bg) | |
|
1538 | pair = curses.color_pair(self._maxcolor) | |
|
1539 | self._colors[style.fg, style.bg] = pair | |
|
1540 | c = pair | style.attrs | |
|
1541 | self._maxcolor += 1 | |
|
1542 | return c | |
|
1543 | ||
|
1544 | def addstr(self, y, x, begx, endx, s, style): | |
|
1545 | s2 = s[max(0, begx-x):max(0, endx-x)] | |
|
1546 | if s2: | |
|
1547 | self.scr.addstr(y, max(x, begx), s2, self.getstyle(style)) | |
|
1548 | return len(s) | |
|
1549 | ||
|
1550 | def format(self, value): | |
|
1551 | if value is None: | |
|
1552 | return (-1, repr(value), self.style_type_none) | |
|
1553 | elif isinstance(value, str): | |
|
1554 | return (-1, repr(value.expandtabs(tab))[1:-1], self.style_default) | |
|
1555 | elif isinstance(value, unicode): | |
|
1556 | return (-1, repr(value.expandtabs(tab))[2:-1], self.style_default) | |
|
1557 | elif isinstance(value, datetime.datetime): | |
|
1558 | # Don't use strftime() here, as this requires year >= 1900 | |
|
1559 | return (-1, "%04d-%02d-%02d %02d:%02d:%02d.%06d" % (value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond), self.style_type_datetime) | |
|
1560 | elif isinstance(value, datetime.date): | |
|
1561 | return (-1, "%04d-%02d-%02d" % (value.year, value.month, value.day), self.style_type_datetime) | |
|
1562 | elif isinstance(value, datetime.time): | |
|
1563 | return (-1, "%02d:%02d:%02d.%06d" % (value.hour, value.minute, value.second, value.microsecond), self.style_type_datetime) | |
|
1564 | elif isinstance(value, datetime.timedelta): | |
|
1565 | return (-1, repr(value), self.style_type_datetime) | |
|
1566 | elif isinstance(value, bool): | |
|
1567 | return (-1, repr(value), self.style_type_bool) | |
|
1568 | elif isinstance(value, (int, long, float)): | |
|
1569 | return (1, repr(value), self.style_type_number) | |
|
1570 | elif isinstance(value, complex): | |
|
1571 | return (-1, repr(value), self.style_type_number) | |
|
1572 | elif isinstance(value, Exception): | |
|
1573 | if value.__class__.__module__ == "exceptions": | |
|
1574 | value = "%s: %s" % (value.__class__.__name__, value) | |
|
1575 | else: | |
|
1576 | value = "%s.%s: %s" % (value.__class__.__module__, value.__class__.__name__, value) | |
|
1577 | return (-1, value, self.style_error) | |
|
1578 | return (-1, repr(value), self.style_default) | |
|
1579 | ||
|
1580 | def _calcheaderlines(self, levels): | |
|
1581 | if levels is None: | |
|
1582 | levels = len(self.levels) | |
|
1583 | self._headerlines = min(self.maxheaders, levels) | |
|
1584 | self._firstheaderline = levels-self._headerlines | |
|
1585 | ||
|
1586 | def getstylehere(self, style): | |
|
1587 | """ | |
|
1588 | Return a style for displaying the original style ``style`` in the row | |
|
1589 | the cursor is on. | |
|
1590 | """ | |
|
1591 | return Style(style.fg, style.bg, style.attrs | curses.A_BOLD) | |
|
1592 | ||
|
1593 | def report(self, msg): | |
|
1594 | self._report = msg | |
|
1595 | ||
|
1596 | def enter(self, item, mode, *attrs): | |
|
1597 | try: | |
|
1598 | iterator = xiter(item, mode) | |
|
1599 | except (KeyboardInterrupt, SystemExit): | |
|
1600 | raise | |
|
1601 | except Exception, exc: | |
|
1602 | curses.beep() | |
|
1603 | self.report(exc) | |
|
1604 | else: | |
|
1605 | self._calcheaderlines(len(self.levels)+1) | |
|
1606 | self.levels.append(_BrowserLevel(self, item, iterator, self.scrsizey-1-self._headerlines-2, *attrs)) | |
|
1607 | ||
|
1608 | def keylabel(self, keycode): | |
|
1609 | if keycode <= 0xff: | |
|
1610 | specialsnames = { | |
|
1611 | ord("\n"): "RETURN", | |
|
1612 | ord(" "): "SPACE", | |
|
1613 | ord("\t"): "TAB", | |
|
1614 | ord("\x7f"): "DELETE", | |
|
1615 | ord("\x08"): "BACKSPACE", | |
|
1616 | } | |
|
1617 | if keycode in specialsnames: | |
|
1618 | return specialsnames[keycode] | |
|
1619 | return repr(chr(keycode)) | |
|
1620 | for name in dir(curses): | |
|
1621 | if name.startswith("KEY_") and getattr(curses, name) == keycode: | |
|
1622 | return name | |
|
1623 | return str(keycode) | |
|
1624 | ||
|
1625 | def cmd_help(self): | |
|
1626 | for level in self.levels: | |
|
1627 | if isinstance(level.input, _BrowserHelp): | |
|
1628 | curses.beep() | |
|
1629 | self.report(CommandError("help already active")) | |
|
1630 | return | |
|
1631 | ||
|
1632 | if self.pageoverlapy: | |
|
1633 | if self.pageoverlapy > 1: | |
|
1634 | overlapmsg = " (pages will overlap %d lines)" % self.pageoverlapy | |
|
1635 | else: | |
|
1636 | overlapmsg = " (pages will overlap one line)" | |
|
1637 | else: | |
|
1638 | overlapmsg = "" | |
|
1639 | help = _BrowserHelp(self, | |
|
1640 | ("up" , "Move the cursor to the previous line."), | |
|
1641 | ("" , ""), | |
|
1642 | ("down" , "Move the cursor to the next line."), | |
|
1643 | ("" , ""), | |
|
1644 | ("pageup" , "Move the cursor up one page%s." % overlapmsg), | |
|
1645 | ("" , ""), | |
|
1646 | ("pagedown" , "Move the cursor down one page%s." % overlapmsg), | |
|
1647 | ("" , ""), | |
|
1648 | ("left" , "Move the cursor left."), | |
|
1649 | ("" , ""), | |
|
1650 | ("right" , "Move the cursor right."), | |
|
1651 | ("" , ""), | |
|
1652 | ("home" , "Move the cursor to the first column."), | |
|
1653 | ("" , ""), | |
|
1654 | ("end" , "Move the cursor to the last column."), | |
|
1655 | ("" , ""), | |
|
1656 | ("pick" , "'Pick' the object under the cursor (i.e. the row the cursor is on).\n" | |
|
1657 | "This leaves the browser and returns the picked object to the caller.\n" | |
|
1658 | "(In IPython this object will be available as the '_' variable.)"), | |
|
1659 | ("" , ""), | |
|
1660 | ("pickattr" , "'Pick' the attribute under the cursor (i.e. the row/column the cursor is on).\n"), | |
|
1661 | ("" , ""), | |
|
1662 | ("pickmarked" , "'Pick' marked objects.\n" | |
|
1663 | "Marked objects (those with a '!' after the row number) will be returned as a list"), | |
|
1664 | ("" , ""), | |
|
1665 | ("pickmarkedattr" , "Leave the browser and return the atrribute under the cursor from all\n" | |
|
1666 | "marked objects as a list to the caller (i.e. the shell)"), | |
|
1667 | ("" , ""), | |
|
1668 | ("enterdefault" , "Enter the object under the cursor (what this mean depends on the object itself).\n" | |
|
1669 | "This opens a new browser 'level'."), | |
|
1670 | ("" , ""), | |
|
1671 | ("enter" , "Enter the object under the cursor. If the object provides different enter modes\n" | |
|
1672 | "a menu of all modes will be presented, choice one and enter it (via the 'enter' or\n" | |
|
1673 | "'enterdefault' command"), | |
|
1674 | ("" , ""), | |
|
1675 | ("enterattr" , "Enter the attribute under the cursor."), | |
|
1676 | ("" , ""), | |
|
1677 | ("leave" , "Leave the current browser level and go back to the previous one."), | |
|
1678 | ("" , ""), | |
|
1679 | ("detail" , "Show a detail view of the object under the cursor.\n" | |
|
1680 | "This shows the name, type, doc string and value of the object attributes\n" | |
|
1681 | "(and it might show more attributes than in the list view;\n" | |
|
1682 | "depending on the object)."), | |
|
1683 | ("" , ""), | |
|
1684 | ("tooglemark" , "Mark/unmark the object under the cursor. Marked objects have a '!'\n" | |
|
1685 | "after the row number)."), | |
|
1686 | ("" , ""), | |
|
1687 | ("sortcolumnasc" , "Sort the objects (in ascending order) using the column under the cursor\n" | |
|
1688 | "as the sort key."), | |
|
1689 | ("" , ""), | |
|
1690 | ("sortcolumndesc" , "Sort the objects (in descending order) using the column under the cursor\n" | |
|
1691 | "as the sort key."), | |
|
1692 | ("" , ""), | |
|
1693 | ("help" , "This screen"), | |
|
1694 | ) | |
|
1695 | ||
|
1696 | self.enter(help, "default") | |
|
1697 | ||
|
1698 | def _dodisplay(self, scr): | |
|
1699 | self.scr = scr | |
|
1700 | curses.halfdelay(1) | |
|
1701 | footery = 2 | |
|
1702 | ||
|
1703 | keys = [] | |
|
1704 | for (key, cmd) in self.keymap.iteritems(): | |
|
1705 | if cmd == "quit": | |
|
1706 | keys.append("%s=%s" % (self.keylabel(key), cmd)) | |
|
1707 | for (key, cmd) in self.keymap.iteritems(): | |
|
1708 | if cmd == "help": | |
|
1709 | keys.append("%s=%s" % (self.keylabel(key), cmd)) | |
|
1710 | helpmsg = " %s" % " ".join(keys) | |
|
1711 | ||
|
1712 | scr.clear() | |
|
1713 | msg = "Fetching first batch of objects..." | |
|
1714 | (self.scrsizey, self.scrsizex) = scr.getmaxyx() | |
|
1715 | scr.addstr(self.scrsizey//2, (self.scrsizex-len(msg))//2, msg) | |
|
1716 | scr.refresh() | |
|
1717 | ||
|
1718 | lastc = -1 | |
|
1719 | ||
|
1720 | self.levels = [] | |
|
1721 | self.enter(self.input, xiter(self.input, "default"), *self.attrs) # the first level | |
|
1722 | ||
|
1723 | self._calcheaderlines(None) | |
|
1724 | ||
|
1725 | while True: | |
|
1726 | level = self.levels[-1] | |
|
1727 | (self.scrsizey, self.scrsizex) = scr.getmaxyx() | |
|
1728 | level.mainsizey = self.scrsizey-1-self._headerlines-footery | |
|
1729 | ||
|
1730 | # Paint object header | |
|
1731 | for i in xrange(self._firstheaderline, self._firstheaderline+self._headerlines): | |
|
1732 | lv = self.levels[i] | |
|
1733 | posx = 0 | |
|
1734 | posx += self.addstr(i-self._firstheaderline, posx, 0, self.scrsizex, " ibrowse #%d: " % i, self.style_objheadertext) | |
|
1735 | posx += self.addstr(i-self._firstheaderline, posx, 0, self.scrsizex, lv.header, self.style_objheaderobject) | |
|
1736 | if i: # not the first level | |
|
1737 | posx += self.addstr(i-self._firstheaderline, posx, 0, self.scrsizex, " == ", self.style_objheadertext) | |
|
1738 | msg = "%d/%d" % (self.levels[i-1].cury, len(self.levels[i-1].items)) | |
|
1739 | if not self.levels[i-1].exhausted: | |
|
1740 | msg += "+" | |
|
1741 | posx += self.addstr(i-self._firstheaderline, posx, 0, self.scrsizex, msg, self.style_objheadernumber) | |
|
1742 | if posx < self.scrsizex: | |
|
1743 | scr.addstr(" "*(self.scrsizex-posx), self.getstyle(self.style_objheadertext)) | |
|
1744 | ||
|
1745 | # Paint column headers | |
|
1746 | scr.move(self._headerlines, 0) | |
|
1747 | scr.addstr(" %*s " % (level.numbersizex, "#"), self.getstyle(self.style_colheader)) | |
|
1748 | scr.addstr(self.headersepchar, self.getstyle(self.style_colheadersep)) | |
|
1749 | begx = level.numbersizex+3 | |
|
1750 | posx = begx-level.datastartx | |
|
1751 | for attrname in level.displayattrs: | |
|
1752 | strattrname = _attrname(attrname) | |
|
1753 | cwidth = level.colwidths[attrname] | |
|
1754 | header = strattrname.ljust(cwidth) | |
|
1755 | if attrname == level.displayattr: | |
|
1756 | style = self.style_colheaderhere | |
|
1757 | else: | |
|
1758 | style = self.style_colheader | |
|
1759 | posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, header, style) | |
|
1760 | posx += self.addstr(self._headerlines, posx, begx, self.scrsizex, self.headersepchar, self.style_colheadersep) | |
|
1761 | if posx >= self.scrsizex: | |
|
1762 | break | |
|
1763 | else: | |
|
1764 | scr.addstr(" "*(self.scrsizex-posx), self.getstyle(self.style_colheader)) | |
|
1765 | ||
|
1766 | # Paint rows | |
|
1767 | for i in xrange(level.datastarty, min(level.datastarty+level.mainsizey, len(level.items))): | |
|
1768 | cache = level.items[i] | |
|
1769 | if i == level.cury: | |
|
1770 | style = self.style_numberhere | |
|
1771 | else: | |
|
1772 | style = self.style_number | |
|
1773 | ||
|
1774 | posy = self._headerlines+1+i-level.datastarty | |
|
1775 | posx = begx-level.datastartx | |
|
1776 | ||
|
1777 | scr.move(posy, 0) | |
|
1778 | scr.addstr(" %*d%s" % (level.numbersizex, i, " !"[cache.marked]), self.getstyle(style)) | |
|
1779 | scr.addstr(self.headersepchar, self.getstyle(self.style_sep)) | |
|
1780 | ||
|
1781 | for attrname in level.displayattrs: | |
|
1782 | cwidth = level.colwidths[attrname] | |
|
1783 | (align, text, style) = level.displayrows[i-level.datastarty].get(attrname, (2, None, self.style_noattr)) | |
|
1784 | padstyle = self.style_datapad | |
|
1785 | sepstyle = self.style_sep | |
|
1786 | if i == level.cury: | |
|
1787 | style = self.getstylehere(style) | |
|
1788 | padstyle = self.getstylehere(padstyle) | |
|
1789 | sepstyle = self.getstylehere(sepstyle) | |
|
1790 | if align == 2: | |
|
1791 | text = self.nodatachar*cwidth | |
|
1792 | posx += self.addstr(posy, posx, begx, self.scrsizex, text, style) | |
|
1793 | elif align == -1: | |
|
1794 | pad = self.datapadchar*(cwidth-len(text)) | |
|
1795 | posx += self.addstr(posy, posx, begx, self.scrsizex, text, style) | |
|
1796 | posx += self.addstr(posy, posx, begx, self.scrsizex, pad, padstyle) | |
|
1797 | elif align == 0: | |
|
1798 | pad1 = self.datapadchar*((cwidth-len(text))//2) | |
|
1799 | pad2 = self.datapadchar*(cwidth-len(text)-len(pad1)) | |
|
1800 | posx += self.addstr(posy, posx, begx, self.scrsizex, pad1, padstyle) | |
|
1801 | posx += self.addstr(posy, posx, begx, self.scrsizex, text, style) | |
|
1802 | posx += self.addstr(posy, posx, begx, self.scrsizex, pad2, padstyle) | |
|
1803 | elif align == 1: | |
|
1804 | pad = self.datapadchar*(cwidth-len(text)) | |
|
1805 | posx += self.addstr(posy, posx, begx, self.scrsizex, pad, padstyle) | |
|
1806 | posx += self.addstr(posy, posx, begx, self.scrsizex, text, style) | |
|
1807 | posx += self.addstr(posy, posx, begx, self.scrsizex, self.datasepchar, sepstyle) | |
|
1808 | if posx >= self.scrsizex: | |
|
1809 | break | |
|
1810 | else: | |
|
1811 | scr.clrtoeol() | |
|
1812 | # Add blank row headers for the rest of the screen | |
|
1813 | for posy in xrange(posy+1, self.scrsizey-2): | |
|
1814 | scr.addstr(posy, 0, " " * (level.numbersizex+2), self.getstyle(self.style_colheader)) | |
|
1815 | scr.clrtoeol() | |
|
1816 | ||
|
1817 | # Display footer | |
|
1818 | scr.addstr(self.scrsizey-footery, 0, " "*self.scrsizex, self.getstyle(self.style_footer)) | |
|
1819 | ||
|
1820 | if level.exhausted: | |
|
1821 | flag = "" | |
|
1822 | else: | |
|
1823 | flag = "+" | |
|
1824 | ||
|
1825 | scr.addstr(self.scrsizey-footery, self.scrsizex-len(helpmsg)-1, helpmsg, self.getstyle(self.style_footer)) | |
|
1826 | ||
|
1827 | msg = "%d%s objects (%d marked)" % (len(level.items), flag, level.marked) | |
|
1828 | try: | |
|
1829 | msg += ": %s > %s" % (xrepr(level.items[level.cury].item, "footer"), _attrname(level.displayattr)) | |
|
1830 | except IndexError: # empty | |
|
1831 | pass | |
|
1832 | self.addstr(self.scrsizey-footery, 1, 1, self.scrsizex-len(helpmsg)-1, msg, self.style_footer) | |
|
1833 | ||
|
1834 | # Display report | |
|
1835 | if self._report is not None: | |
|
1836 | if isinstance(self._report, Exception): | |
|
1837 | style = self.getstyle(self.style_error) | |
|
1838 | if self._report.__class__.__module__ == "exceptions": | |
|
1839 | msg = "%s: %s" % (self._report.__class__.__name__, str(self._report)) | |
|
1840 | else: | |
|
1841 | msg = "%s.%s: %s" % (self._report.__class__.__module__, self._report.__class__.__name__, str(self._report)) | |
|
1842 | else: | |
|
1843 | style = self.getstyle(self.style_report) | |
|
1844 | msg = self._report | |
|
1845 | try: | |
|
1846 | scr.addstr(self.scrsizey-1, 0, msg[:self.scrsizex], style) | |
|
1847 | except curses.err: # Protect against error from writing to the last line | |
|
1848 | pass | |
|
1849 | self._report = None | |
|
1850 | else: | |
|
1851 | scr.move(self.scrsizey-1, 0) | |
|
1852 | scr.clrtoeol() | |
|
1853 | ||
|
1854 | # Position cursor | |
|
1855 | scr.move(1+self._headerlines+level.cury-level.datastarty, level.numbersizex+3+level.curx-level.datastartx) # Position cursor | |
|
1856 | scr.refresh() | |
|
1857 | ||
|
1858 | # Check keyboard | |
|
1859 | while True: | |
|
1860 | c = scr.getch() | |
|
1861 | if c == -1: # if no key is pressed slow down and beep again | |
|
1862 | self.stepx = 1. | |
|
1863 | self.stepy = 1. | |
|
1864 | self._dobeep = True | |
|
1865 | else: | |
|
1866 | if c != lastc: # if a different key was pressed slow down and beep too | |
|
1867 | lastc = c | |
|
1868 | self.stepx = 1. | |
|
1869 | self.stepy = 1. | |
|
1870 | self._dobeep = True | |
|
1871 | cmd = self.keymap.get(c, None) | |
|
1872 | if cmd == "quit": | |
|
1873 | return | |
|
1874 | elif cmd == "up": | |
|
1875 | self.report("up") | |
|
1876 | level.moveto(level.curx, level.cury-self.stepy) | |
|
1877 | elif cmd == "down": | |
|
1878 | self.report("down") | |
|
1879 | level.moveto(level.curx, level.cury+self.stepy) | |
|
1880 | elif cmd == "pageup": | |
|
1881 | self.report("page up") | |
|
1882 | level.moveto(level.curx, level.cury-level.mainsizey+self.pageoverlapy) | |
|
1883 | elif cmd == "pagedown": | |
|
1884 | self.report("page down") | |
|
1885 | level.moveto(level.curx, level.cury+level.mainsizey-self.pageoverlapy) | |
|
1886 | elif cmd == "left": | |
|
1887 | self.report("left") | |
|
1888 | level.moveto(level.curx-self.stepx, level.cury) | |
|
1889 | elif cmd == "right": | |
|
1890 | self.report("right") | |
|
1891 | level.moveto(level.curx+self.stepx, level.cury) | |
|
1892 | elif cmd == "home": | |
|
1893 | self.report("home") | |
|
1894 | level.moveto(0, level.cury) | |
|
1895 | elif cmd == "end": | |
|
1896 | self.report("end") | |
|
1897 | level.moveto(level.datasizex+level.mainsizey-self.pageoverlapx, level.cury) | |
|
1898 | elif cmd == "pick": | |
|
1899 | return level.items[level.cury].item | |
|
1900 | elif cmd == "pickattr": | |
|
1901 | attr = _getattr(level.items[level.cury].item, level.displayattr) | |
|
1902 | if attr is _default: | |
|
1903 | curses.beep() | |
|
1904 | self.report(AttributeError(_attrname(level.displayattr))) | |
|
1905 | else: | |
|
1906 | return attr | |
|
1907 | elif cmd == "pickmarked": | |
|
1908 | return [cache.item for cache in level.items if cache.marked] | |
|
1909 | elif cmd == "pickmarkedattr": | |
|
1910 | result = [] | |
|
1911 | for cache in level.items: | |
|
1912 | if cache.marked: | |
|
1913 | attr = _getattr(cache.item, level.displayattr) | |
|
1914 | if attr is not _default: | |
|
1915 | result.append(attr) | |
|
1916 | return result | |
|
1917 | elif cmd == "enterdefault": | |
|
1918 | self.report("entering object (default mode)...") | |
|
1919 | self.enter(level.items[level.cury].item, "default") | |
|
1920 | elif cmd == "leave": | |
|
1921 | self.report("leave") | |
|
1922 | if len(self.levels) > 1: | |
|
1923 | self._calcheaderlines(len(self.levels)-1) | |
|
1924 | self.levels.pop(-1) | |
|
1925 | else: | |
|
1926 | curses.beep() | |
|
1927 | elif cmd == "enter": | |
|
1928 | self.report("entering object...") | |
|
1929 | self.enter(level.items[level.cury].item, None) | |
|
1930 | elif cmd == "enterattr": | |
|
1931 | self.report("entering object attribute...") | |
|
1932 | self.enter(_getattr(level.items[level.cury].item, level.displayattr), None) | |
|
1933 | elif cmd == "detail": | |
|
1934 | self.enter(level.items[level.cury].item, "detail") | |
|
1935 | elif cmd == "tooglemark": | |
|
1936 | self.report("toggle mark") | |
|
1937 | try: | |
|
1938 | item = level.items[level.cury] | |
|
1939 | except IndexError: # no items? | |
|
1940 | pass | |
|
1941 | else: | |
|
1942 | if item.marked: | |
|
1943 | item.marked = False | |
|
1944 | level.marked -= 1 | |
|
1945 | else: | |
|
1946 | item.marked = True | |
|
1947 | level.marked += 1 | |
|
1948 | elif cmd == "sortcolumnasc": | |
|
1949 | self.report("sort by %s (ascending)" % _attrname(level.displayattr)) | |
|
1950 | def key(item): | |
|
1951 | return _getattr(item, level.displayattr) | |
|
1952 | level.sort(key) | |
|
1953 | elif cmd == "sortcolumndesc": | |
|
1954 | self.report("sort by %s (descending)" % _attrname(level.displayattr)) | |
|
1955 | def key(item): | |
|
1956 | return _getattr(item, level.displayattr) | |
|
1957 | level.sort(key, reverse=True) | |
|
1958 | elif cmd == "help": | |
|
1959 | self.cmd_help() | |
|
1960 | elif cmd is not None: | |
|
1961 | self.report(UnknownCommandError("Unknown command %r" % (cmd,))) | |
|
1962 | else: | |
|
1963 | self.report(UnassignedKeyError("Unassigned key %s" % self.keylabel(c))) | |
|
1964 | self.stepx = self.nextstepx(self.stepx) | |
|
1965 | self.stepy = self.nextstepy(self.stepy) | |
|
1966 | curses.flushinp() # get rid of type ahead | |
|
1967 | break # Redisplay | |
|
1968 | self.scr = None | |
|
1969 | ||
|
1970 | def display(self): | |
|
1971 | return curses.wrapper(self._dodisplay) | |
|
1972 | ||
|
1973 | defaultdisplay = ibrowse | |
|
1974 | __all__.append("ibrowse") | |
|
1975 | else: | |
|
1976 | defaultdisplay = idump | |
|
1977 | ||
|
1978 | ||
|
1979 | # If we're running under IPython, install an IPython displayhook that | |
|
1980 | # returns the object from Display.display(), else install a displayhook | |
|
1981 | # directly as sys.displayhook | |
|
1982 | try: | |
|
1983 | from IPython import ipapi | |
|
1984 | api = ipapi.get() | |
|
1985 | except (ImportError, AttributeError): | |
|
1986 | api = None | |
|
1987 | ||
|
1988 | if api is not None: | |
|
1989 | def displayhook(self, obj): | |
|
1990 | if isinstance(obj, type) and issubclass(obj, Table): | |
|
1991 | obj = obj() | |
|
1992 | if isinstance(obj, Table): | |
|
1993 | obj = obj | defaultdisplay | |
|
1994 | if isinstance(obj, Display): | |
|
1995 | return obj.display() | |
|
1996 | else: | |
|
1997 | raise ipapi.TryNext | |
|
1998 | api.set_hook("result_display", displayhook) | |
|
1999 | else: | |
|
2000 | def installdisplayhook(): | |
|
2001 | _originalhook = sys.displayhook | |
|
2002 | def displayhook(obj): | |
|
2003 | if isinstance(obj, type) and issubclass(obj, Table): | |
|
2004 | obj = obj() | |
|
2005 | if isinstance(obj, Table): | |
|
2006 | obj = obj | defaultdisplay | |
|
2007 | if isinstance(obj, Display): | |
|
2008 | return obj.display() | |
|
2009 | else: | |
|
2010 | _originalhook(obj) | |
|
2011 | sys.displayhook = displayhook | |
|
2012 | installdisplayhook() |
General Comments 0
You need to be logged in to leave comments.
Login now