##// END OF EJS Templates
Added Walter Dorwald's "ipipe.py" to Extensions.
vivainio -
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()
@@ -1,3 +1,8 b''
1 2006-03-01 Ville Vainio <vivainio@gmail.com>
2
3 * Extensions/ipipe.py: Added Walter Doerwald's "ipipe" module.
4 To use, do "from ipipe import *".
5
1 6 2006-02-24 Ville Vainio <vivainio@gmail.com>
2 7
3 8 * Magic.py, upgrade_dir.py: %upgrade magic added. Does things more
General Comments 0
You need to be logged in to leave comments. Login now