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