1# -*- coding: utf-8 -*- 2# cython: language_level=3, always_allow_keywords=True 3 4## Copyright 2004-2018 by LivingLogic AG, Bayreuth/Germany 5## Copyright 2004-2018 by Walter Dörwald 6## 7## All Rights Reserved 8## 9## See ll/xist/__init__.py for the license 10 11 12""" 13:mod:`ll.misc` contains various utility functions and classes used by the other 14LivingLogic modules and packages. 15""" 16 17 18import sys, os, types, datetime, collections, io, gzip as gzip_, argparse, functools, signal, contextlib, subprocess 19 20from ll import ul4c, color 21 22 23__docformat__ = "reStructuredText" 24 25 26# get the current directory as early as possible to minimize the chance that someone has called ``os.chdir()`` 27_curdir = os.getcwd() 28 29 30notifycmd = os.environ.get("LL_MISC_NOTIFY", "/usr/local/bin/terminal-notifier") 31 32 33# Try to fetch ``xmlescape`` from C implementation 34try: 35 from ll._misc import * 36except ImportError: 37 def xmlescape(string): 38 """ 39 Return a copy of the argument string, where every occurrence of ``<``, 40 ``>``, ``&``, ``\"``, ``'`` and every restricted character has been 41 replaced with their XML character entity or character reference. 42 """ 43 if isinstance(string, str): 44 return string.translate({0x00: '�', 0x01: '', 0x02: '', 0x03: '', 0x04: '', 0x05: '', 0x06: '', 0x07: '', 0x08: '', 0x0b: '', 0x0c: '', 0x0e: '', 0x0f: '', 0x10: '', 0x11: '', 0x12: '', 0x13: '', 0x14: '', 0x15: '', 0x16: '', 0x17: '', 0x18: '', 0x19: '', 0x1a: '', 0x1b: '', 0x1c: '', 0x1d: '', 0x1e: '', 0x1f: '', 0x22: '"', 0x26: '&', 0x27: ''', 0x3c: '<', 0x3e: '>', 0x7f: '', 0x80: '€', 0x81: '', 0x82: '‚', 0x83: 'ƒ', 0x84: '„', 0x86: '†', 0x87: '‡', 0x88: 'ˆ', 0x89: '‰', 0x8a: 'Š', 0x8b: '‹', 0x8c: 'Œ', 0x8d: '', 0x8e: 'Ž', 0x8f: '', 0x90: '', 0x91: '‘', 0x92: '’', 0x93: '“', 0x94: '”', 0x95: '•', 0x96: '–', 0x97: '—', 0x98: '˜', 0x99: '™', 0x9a: 'š', 0x9b: '›', 0x9c: 'œ', 0x9d: '', 0x9e: 'ž', 0x9f: 'Ÿ'}) 45 else: 46 string = string.replace("&", "&") 47 string = string.replace("<", "<") 48 string = string.replace(">", ">") 49 string = string.replace("'", "'") 50 string = string.replace('"', """) 51 for c in "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1f\x7f\x80\x81\x82\x83\x84\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f": 52 string = string.replace(c, f"&#{ord(c)};") 53 return string 54 55 def xmlescape_text(string): 56 """ 57 Return a copy of the argument string, where every occurrence of ``<``, 58 ``>``, ``&``, and every restricted character has been replaced with their 59 XML character entity or character reference. 60 """ 61 if isinstance(string, str): 62 return string.translate({0x00: '�', 0x01: '', 0x02: '', 0x03: '', 0x04: '', 0x05: '', 0x06: '', 0x07: '', 0x08: '', 0x0b: '', 0x0c: '', 0x0e: '', 0x0f: '', 0x10: '', 0x11: '', 0x12: '', 0x13: '', 0x14: '', 0x15: '', 0x16: '', 0x17: '', 0x18: '', 0x19: '', 0x1a: '', 0x1b: '', 0x1c: '', 0x1d: '', 0x1e: '', 0x1f: '', 0x26: '&', 0x3c: '<', 0x3e: '>', 0x7f: '', 0x80: '€', 0x81: '', 0x82: '‚', 0x83: 'ƒ', 0x84: '„', 0x86: '†', 0x87: '‡', 0x88: 'ˆ', 0x89: '‰', 0x8a: 'Š', 0x8b: '‹', 0x8c: 'Œ', 0x8d: '', 0x8e: 'Ž', 0x8f: '', 0x90: '', 0x91: '‘', 0x92: '’', 0x93: '“', 0x94: '”', 0x95: '•', 0x96: '–', 0x97: '—', 0x98: '˜', 0x99: '™', 0x9a: 'š', 0x9b: '›', 0x9c: 'œ', 0x9d: '', 0x9e: 'ž', 0x9f: 'Ÿ'}) 63 else: 64 string = string.replace("&", "&") 65 string = string.replace("<", "<") 66 string = string.replace(">", ">") 67 for c in "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1f\x7f\x80\x81\x82\x83\x84\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f": 68 string = string.replace(c, f"&#{ord(c)};") 69 return string 70 71 def xmlescape_attr(string): 72 """ 73 Return a copy of the argument string, where every occurrence of ``<``, 74 ``>``, ``&``, ``"`` and every restricted character has been replaced with 75 their XML character entity or character reference. 76 """ 77 if isinstance(string, str): 78 return string.translate({0x00: '�', 0x01: '', 0x02: '', 0x03: '', 0x04: '', 0x05: '', 0x06: '', 0x07: '', 0x08: '', 0x0b: '', 0x0c: '', 0x0e: '', 0x0f: '', 0x10: '', 0x11: '', 0x12: '', 0x13: '', 0x14: '', 0x15: '', 0x16: '', 0x17: '', 0x18: '', 0x19: '', 0x1a: '', 0x1b: '', 0x1c: '', 0x1d: '', 0x1e: '', 0x1f: '', 0x22: '"', 0x26: '&', 0x3c: '<', 0x3e: '>', 0x7f: '', 0x80: '€', 0x81: '', 0x82: '‚', 0x83: 'ƒ', 0x84: '„', 0x86: '†', 0x87: '‡', 0x88: 'ˆ', 0x89: '‰', 0x8a: 'Š', 0x8b: '‹', 0x8c: 'Œ', 0x8d: '', 0x8e: 'Ž', 0x8f: '', 0x90: '', 0x91: '‘', 0x92: '’', 0x93: '“', 0x94: '”', 0x95: '•', 0x96: '–', 0x97: '—', 0x98: '˜', 0x99: '™', 0x9a: 'š', 0x9b: '›', 0x9c: 'œ', 0x9d: '', 0x9e: 'ž', 0x9f: 'Ÿ'}) 79 else: 80 string = string.replace("&", "&") 81 string = string.replace("<", "<") 82 string = string.replace(">", ">") 83 string = string.replace('"', """) 84 for c in "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1f\x7f\x80\x81\x82\x83\x84\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f": 85 string = string.replace(c, f"&#{ord(c)};") 86 return string 87 88 89def item(iterable, index, default=None): 90 """ 91 Returns the :obj:`index`'th item from the iterable. :obj:`index` may be 92 negative to count from the end. E.g. 0 returns the first item produced by 93 the iterator, 1 the second, -1 the last one etc. If :obj:`index` is negative 94 the iterator will be completely exhausted, if it's positive it will be 95 exhausted up to the :obj:`index`'th item. If the iterator doesn't produce 96 that many items :obj:`default` will be returned. 97 98 :obj:`index` may also be an iterable of indexes, in which case :meth:`item` 99 will be applied recursively, i.e. ``item(["foo", "bar"], (1, -1))`` returns 100 ``'r'``. 101 """ 102 if isinstance(index, int): 103 index = (index,) 104 for i in index: 105 if i >= 0: 106 for item in iterable: 107 if not i: 108 iterable = item 109 break 110 i -= 1 111 else: 112 return default 113 else: 114 i = -i 115 cache = collections.deque() 116 for item in iterable: 117 cache.append(item) 118 if len(cache) > i: 119 cache.popleft() 120 if len(cache) == i: 121 iterable = cache.popleft() 122 else: 123 return default 124 return iterable 125 126 127def first(iterable, default=None): 128 """ 129 Return the first item from the iterable. If the iterator doesn't 130 produce any items :obj:`default` will be returned. 131 """ 132 for item in iterable: 133 return item 134 return default 135 136 137def last(iterable, default=None): 138 """ 139 Return the last item from the iterable. If the iterator doesn't produce any 140 items :obj:`default` will be returned. 141 """ 142 item = default 143 for item in iterable: 144 pass 145 return item 146 147 148def count(iterable): 149 """ 150 Count the number of items produced by the iterable. Calling this function 151 will exhaust the iterator. 152 """ 153 count = 0 154 for node in iterable: 155 count += 1 156 return count 157 158 159def notimplemented(function): 160 """ 161 A decorator that raises :exc:`NotImplementedError` when the method is called. 162 This saves you the trouble of formatting the error message yourself for each 163 implementation. 164 """ 165 @functools.wraps(function) 166 def wrapper(self, *args, **kwargs): 167 raise NotImplementedError(f"method {function.__name__}() not implemented in {format_class(self)}") 168 return wrapper 169 170 171def withdoc(doc): 172 """ 173 A decorator that adds a docstring to the function it decorates. This can be 174 useful if the docstring is not static, and adding it afterwards is not 175 possible. 176 """ 177 def wrapper(function): 178 function.__doc__ = doc 179 return function 180 return wrapper 181 182 183class _propclass_Meta(type): 184 def __new__(cls, name, bases, dict): 185 if bases == (property,): 186 # create propclass itself normally 187 return super(_propclass_Meta, cls).__new__(cls, name, bases, dict) 188 newdict = dict.copy() 189 newdict.pop("__get__", None) 190 newdict.pop("__set__", None) 191 newdict.pop("__delete__", None) 192 newdict.pop("__metaclass__", None) 193 self = type.__new__(cls, name, bases, newdict) 194 inst = self( 195 dict.get("__get__", None), 196 dict.get("__set__", None), 197 dict.get("__delete__", None), 198 dict.get("__doc__", None) 199 ) 200 inst.__name__ = name 201 return inst 202 203 204class propclass(property, metaclass=_propclass_Meta): 205 ''' 206 :class:`propclass` provides an alternate way to define properties. 207 208 Subclassing :class:`propclass` and defining methods :meth:`__get__`, 209 :meth:`__set__` and :meth:`__delete__` will automatically generate the 210 appropriate property:: 211 212 class name(misc.propclass): 213 """ 214 The name property 215 """ 216 def __get__(self): 217 return self._name 218 def __set__(self, name): 219 self._name = name.lower() 220 def __delete__(self): 221 self._name = None 222 ''' 223 224 225def format_class(obj): 226 """ 227 Format the name of the class of :obj:`obj`:: 228 229 >>> misc.format_class(42) 230 'int' 231 >>> misc.format_class(open('README.rst', 'rb')) 232 '_io.BufferedReader' 233 """ 234 if obj.__class__.__module__ not in ("builtins", "exceptions"): 235 return f"{obj.__class__.__module__}.{obj.__class__.__qualname__}" 236 else: 237 return obj.__class__.__qualname__ 238 239 240def format_exception(exc): 241 """ 242 Format an exception object:: 243 244 >>> misc.format_exception(ValueError("bad value")) 245 'ValueError: bad value' 246 """ 247 try: 248 strexc = str(exc).strip() 249 if strexc: 250 return f"{format_class(exc)}: {strexc}" 251 else: 252 return format_class(exc) 253 except UnicodeError: 254 return f"{format_class(exc)}: ?" 255 256 257def exception_chain(exc): 258 """ 259 Traverses the chain of exceptions. This is a generator. 260 """ 261 while True: 262 yield exc 263 if exc.__cause__ is not None: 264 exc = exc.__cause__ 265 elif exc.__context__ is not None and not exc.__suppress_context__: 266 exc = exc.__context__ 267 else: 268 break 269 270 271class Pool: 272 """ 273 A :class:`Pool` object can be used as an inheritable alternative to modules. 274 The attributes of a module can be put into a pool and each pool can have 275 base pools where lookup continues if an attribute can't be found. 276 """ 277 def __init__(self, *objects): 278 self._attrs = {} 279 self.bases = [] 280 for object in objects: 281 self.register(object) 282 283 def register(self, object): 284 """ 285 Register :obj:`object` in the pool. :obj:`object` can be a module, a 286 dictionary or a :class:`Pool` objects (with registers the pool as a base 287 pool). If :obj:`object` is a module and has an attribute :attr:`__bases__` 288 (being a sequence of other modules) this attribute will be used to 289 initialize :obj:`self`\s base pool. 290 """ 291 if isinstance(object, types.ModuleType): 292 self.register(object.__dict__) 293 elif isinstance(object, dict): 294 for (key, value) in object.items(): 295 if key == "__bases__": 296 for base in value: 297 if not isinstance(base, Pool): 298 base = self.__class__(base) 299 self.bases.append(base) 300 elif not isinstance(value, (types.ModuleType, dict)): 301 try: 302 self._attrs[key] = value 303 except TypeError: 304 pass 305 elif isinstance(object, Pool): 306 self.bases.append(object) 307 elif isinstance(object, type): 308 self._attrs[object.__name__] = object 309 310 def __getitem__(self, key): 311 try: 312 return self._attrs[key] 313 except KeyError: 314 for base in self.bases: 315 return base[key] 316 raise 317 318 def __getattr__(self, key): 319 try: 320 return self.__getitem__(key) 321 except KeyError: 322 raise AttributeError(key) 323 324 def clear(self): 325 """ 326 Make :obj:`self` empty. 327 """ 328 self._attrs.clear() 329 del self.bases[:] 330 331 def clone(self): 332 """ 333 Return a copy of :obj:`self`. 334 """ 335 copy = self.__class__() 336 copy._attrs = self._attrs.copy() 337 copy.bases = self.bases[:] 338 return copy 339 340 def __repr__(self): 341 return f"<{self.__class__.__module__}.{self.__class__.__qualname__} object with {len(self._attrs):,} items at {id(self):#x}>" 342 343 344def iterone(item): 345 """ 346 Return an iterator that will produce one item: :obj:`item`. 347 """ 348 yield item 349 350 351class Iterator: 352 """ 353 :class:`Iterator` adds :meth:`__getitem__` support to an iterator. This is 354 done by calling :func:`item` internally. 355 """ 356 __slots__ = ("iterator", ) 357 358 def __init__(self, iterator): 359 self.iterator = iterator 360 361 def __getitem__(self, index): 362 if isinstance(index, slice): 363 return list(self.iterator)[index] 364 default = object() 365 result = item(self, index, default) 366 if result is default: 367 raise IndexError(index) 368 return result 369 370 def __iter__(self): 371 return self 372 373 def __next__(self): 374 return next(self.iterator) 375 376 # We can't implement :meth:`__len__`, because if such an object is passed to 377 # :class:`list`, :meth:`__len__` would be called, exhausting the iterator 378 379 def __bool__(self): 380 for node in self: 381 return True 382 return False 383 384 def get(self, index, default=None): 385 """ 386 Return the :obj:`index`'th item from the iterator (or :obj:`default` if 387 there's no such item). 388 """ 389 return item(self, index, default) 390 391 392class Queue: 393 """ 394 :class:`Queue` provides FIFO queues: The method :meth:`write` writes to the 395 queue and the method :meth:`read` read from the other end of the queue and 396 remove the characters read. 397 """ 398 def __init__(self): 399 self._buffer = "" 400 401 def write(self, chars): 402 """ 403 Write the string :obj:`chars` to the buffer. 404 """ 405 self._buffer += chars 406 407 def read(self, size=-1): 408 """ 409 Read up to :obj:`size` character from the buffer (or all if :obj:`size` 410 is negative). Those characters will be removed from the buffer. 411 """ 412 if size < 0: 413 s = self._buffer 414 self._buffer = "" 415 return s 416 else: 417 s = self._buffer[:size] 418 self._buffer = self._buffer[size:] 419 return s 420 421 422class Const: 423 """ 424 This class can be used for singleton constants. 425 """ 426 __slots__ = ("_name", "_module") 427 428 def __init__(self, name, module=None): 429 self._name = name 430 self._module = module 431 432 def __repr__(self): 433 return f"{self._module or self.__module__}.{self._name}" 434 435 436class FlagAction(argparse.Action): 437 """ 438 :class:`FlagAction` can be use with :mod:`argparse` for options that 439 represent flags. An options can have a value like ``yes`` or ``no`` for the 440 correspending boolean value, or if the value is omitted it is the inverted 441 default value (i.e. specifying the option toggles it). 442 """ 443 true_choices = ("1", "true", "yes", "on") 444 false_choices = ("0", "false", "no", "off") 445 446 def __init__(self, option_strings, dest, default=False, help=None): 447 super().__init__(option_strings=option_strings, dest=dest, default="yes" if default else "no", help=help, metavar="yes|no", const="no" if default else "yes", type=self.str2bool, nargs="?") 448 449 # implementing this prevents :meth:`__repr__` from generating an infinite recursion 450 def _get_kwargs(self): 451 return [(key, getattr(self, key)) for key in ("option_strings", "dest", "default", "help")] 452 453 def str2bool(self, value): 454 value = value.lower() 455 if value in self.true_choices: 456 return True 457 elif value in self.false_choices: 458 return False 459 else: 460 choices = ", ".join(self.true_choices + self.false_choices) 461 raise argparse.ArgumentTypeError(f"invalid flag value: {value!r} (use any of {choices})") 462 463 def __call__(self, parser, namespace, values, option_string=None): 464 setattr(namespace, self.dest, values) 465 466 467def tokenizepi(string): 468 """ 469 Tokenize the string object :obj:`string` according to the processing 470 instructions in the string. :func:`tokenize` will generate tuples with the 471 first item being the processing instruction target and the second being the 472 PI data. "Text" content (i.e. anything other than PIs) will be returned as 473 ``(None, data)``. 474 """ 475 476 pos = 0 477 while True: 478 pos1 = string.find("<?", pos) 479 if pos1 < 0: 480 part = string[pos:] 481 if part: 482 yield (None, part) 483 return 484 pos2 = string.find("?>", pos1) 485 if pos2 < 0: 486 part = string[pos:] 487 if part: 488 yield (None, part) 489 return 490 part = string[pos:pos1] 491 if part: 492 yield (None, part) 493 part = string[pos1+2: pos2].strip() 494 parts = part.split(None, 1) 495 target = parts[0] 496 if len(parts) > 1: 497 data = parts[1] 498 else: 499 data = "" 500 yield (target, data) 501 pos = pos2+2 502 503 504def itersplitat(string, positions): 505 """ 506 Split :obj:`string` at the positions specified in :obj:`positions`. 507 508 For example:: 509 510 >>> from ll import misc 511 >>> import datetime 512 >>> datetime.datetime(*map(int, misc.itersplitat("20090609172345", (4, 6, 8, 10, 12)))) 513 datetime.datetime(2009, 6, 9, 17, 23, 45) 514 515 This is a generator. 516 """ 517 curpos = 0 518 for pos in positions: 519 part = string[curpos:pos] 520 if part: 521 yield part 522 curpos = pos 523 part = string[curpos:] 524 if part: 525 yield part 526 527 528def module(source, filename="unnamed.py", name=None): 529 """ 530 Create a module from the Python source code :obj:`source`. :obj:`filename` 531 will be used as the filename for the module and :obj:`name` as the module 532 name (defaulting to the filename part of :obj:`filename`). 533 """ 534 if name is None: 535 name = os.path.splitext(os.path.basename(filename))[0] 536 mod = types.ModuleType(name) 537 mod.__file__ = filename 538 code = compile(source, filename, "exec") 539 exec(code, mod.__dict__) 540 return mod 541 542 543def javaexpr(obj): 544 """ 545 Return a Java expression for the object :obj:`obj`. 546 547 Example:: 548 549 >>> print(misc.javaexpr([1, 2, 3])) 550 java.util.Arrays.asList(1, 2, 3) 551 """ 552 553 if obj is None: 554 return "null" 555 elif obj is True: 556 return "true" 557 elif obj is False: 558 return "false" 559 elif isinstance(obj, str): 560 if len(obj) > 10000: # Otherwise javac complains about ``constant string too long`` (the upper limit is 65535 UTF-8 bytes) 561 parts = "".join(f".append({javaexpr(obj[i:i+10000])})" for i in range(0, len(obj), 10000)) 562 return f"new StringBuilder({len(obj)}){parts}.toString()" 563 else: 564 v = [] 565 specialchars = {"\r": "\\r", "\n": "\\n", "\t": "\\t", "\f": "\\f", "\b": "\\b", '"': '\\"', "\\": "\\\\"} 566 for c in obj: 567 try: 568 v.append(specialchars[c]) 569 except KeyError: 570 oc = ord(c) 571 v.append(c if 32 <= oc < 128 else f"\\u{oc:04x}") 572 string = "".join(v) 573 return f'"{string}"' 574 elif isinstance(obj, datetime.datetime): # check ``datetime`` before ``date``, as ``datetime`` is a subclass of ``date`` 575 return f"com.livinglogic.ul4.FunctionDate.call({obj.year}, {obj.month}, {obj.day}, {obj.hour}, {obj.minute}, {obj.second}, {obj.microsecond})" 576 elif isinstance(obj, datetime.date): 577 return f"com.livinglogic.ul4.FunctionDate.call({obj.year}, {obj.month}, {obj.day})" 578 elif isinstance(obj, datetime.timedelta): 579 return f"com.livinglogic.ul4.FunctionTimeDelta.call({obj.days}, {obj.seconds}, {obj.microseconds})" 580 elif isinstance(obj, monthdelta): 581 return f"com.livinglogic.ul4.FunctionMonthDelta.call({obj.months()})" 582 elif isinstance(obj, color.Color): 583 return "new com.livinglogic.ul4.Color({obj[0]}, {obj[1]}, {obj[2]}, {obj[3]})" 584 elif isinstance(obj, float): 585 return repr(obj) 586 elif isinstance(obj, int): 587 if -0x80000000 <= obj <= 0x7fffffff: 588 return repr(obj) 589 elif -0x8000000000000000 <= obj <= 0x7fffffffffffffff: 590 return repr(obj) + "L" 591 else: 592 return f'new java.math.BigInteger("{obj}")' 593 return repr(obj) 594 elif isinstance(obj, collections.Sequence): 595 items = ", ".join(javaexpr(item) for item in obj) 596 return f"java.util.Arrays.asList({items})" 597 elif isinstance(obj, collections.Mapping): 598 items = ", ".join(f"{javaexpr(key)}, {javaexpr(value)}" for (key, value) in obj.items()) 599 return f"com.livinglogic.utils.MapUtils.makeMap({items})" 600 elif isinstance(obj, collections.Set): 601 items = ", ".join(javaexpr(item) for item in obj) 602 return f"com.livinglogic.utils.SetUtils.makeSet({items})" 603 elif isinstance(obj, ul4c.UndefinedKey): 604 return f"new com.livinglogic.ul4.UndefinedKey({javaexpr(obj._key)})" 605 elif isinstance(obj, ul4c.UndefinedVariable): 606 return f"new com.livinglogic.ul4.UndefinedVariable({javaexpr(obj._name)})" 607 elif isinstance(obj, ul4c.Template): 608 return obj.javasource() 609 else: 610 raise TypeError(f"can't handle object of type {type(obj)}") 611 612 613class SysInfo: 614 """ 615 A :class:`SysInfo` object contains information about the host, user, python 616 version and script. Available attributes are ``host_name``, ``host_fqdn``, 617 ``host_ip``, ``host_sysname``, ``host_nodename``, ``host_release``, 618 ``host_version``, ``host_machine``, ``user_name``, ``user_uid``, ``user_gid``, 619 ``user_gecos``, ``user_dir``, ``user_shell``, ``python_executable``, 620 ``python_version``, ``pid``, ``script_name``, ``short_script_name`` and 621 ``script_url``. 622 623 :class:`SysInfo` object also support a mimimal dictionary interface (i.e. 624 :meth:`__getitem__` and :meth:`__iter__`). 625 626 One module global instance named :obj:`sysinfo` is created at module import 627 time. 628 """ 629 630 _keys = {"host_name", "host_fqdn", "host_ip", "host_sysname", "host_nodename", "host_release", "host_version", "host_machine", "user_name", "user_uid", "user_gid", "user_gecos", "user_dir", "user_shell", "python_executable", "python_version", "pid", "script_name", "short_script_name", "script_url"} 631 632 def __init__(self): 633 # Use ``object`` as a marker for "not initialized" 634 self._host_name = object 635 self._host_fqdn = object 636 self._host_ip = object 637 self._host_sysname = object 638 self._host_nodename = object 639 self._host_release = object 640 self._host_version = object 641 self._host_machine = object 642 self._user_name = object 643 self._user_uid = object 644 self._user_gid = object 645 self._user_gecos = object 646 self._user_dir = object 647 self._user_shell = object 648 self._pid = object 649 self._script_name = object 650 self._short_script_name = object 651 self._script_url = object 652 653 @property 654 def host_name(self): 655 if self._host_name is object: 656 import socket 657 self._host_name = socket.gethostname() 658 return self._host_name 659 660 @property 661 def host_fqdn(self): 662 return self.host_name 663 664 @property 665 def host_ip(self): 666 if self._host_ip is object: 667 import socket 668 self._host_ip = socket.gethostbyname(self.host_name) 669 return self._host_ip 670 671 def _make_host_info(self): 672 (self._host_sysname, self._host_nodename, self._host_release, self._host_version, self._host_machine) = os.uname() 673 674 @property 675 def host_sysname(self): 676 if self._host_sysname is object: 677 self._make_host_info() 678 return self._host_sysname 679 680 @property 681 def host_nodename(self): 682 if self._host_nodename is object: 683 self._make_host_info() 684 return self._host_nodename 685 686 @property 687 def host_release(self): 688 if self._host_release is object: 689 self._make_host_info() 690 return self._host_release 691 692 @property 693 def host_version(self): 694 if self._host_version is object: 695 self._make_host_info() 696 return self._host_version 697 698 @property 699 def host_machine(self): 700 if self._host_machine is object: 701 self._make_host_info() 702 return self._host_machine 703 704 def _make_user_info(self): 705 import pwd 706 (self._user_name, _, self._user_uid, self._user_gid, self._user_gecos, self._user_dir, self._user_shell) = pwd.getpwuid(os.getuid()) 707 708 @property 709 def user_name(self): 710 if self._user_name is object: 711 self._make_user_info() 712 return self._user_name 713 714 @property 715 def user_uid(self): 716 if self._user_uid is object: 717 self._make_user_info() 718 return self._user_uid 719 720 @property 721 def user_gid(self): 722 if self._user_gid is object: 723 self._make_user_info() 724 return self._user_gid 725 726 @property 727 def user_gecos(self): 728 if self._user_gecos is object: 729 self._make_user_info() 730 return self._user_gecos 731 732 @property 733 def user_dir(self): 734 if self._user_dir is object: 735 self._make_user_info() 736 return self._user_dir 737 738 @property 739 def user_shell(self): 740 if self._user_shell is object: 741 self._make_user_info() 742 return self._user_shell 743 744 @property 745 def python_executable(self): 746 return sys.executable 747 748 @property 749 def python_version(self): 750 v = sys.version_info 751 if v.releaselevel != 'final': 752 return f"{v.major}.{v.minor}.{v.micro} {v.releaselevel}" 753 elif v.micro: 754 return f"{v.major}.{v.minor}.{v.micro}" 755 else: 756 return f"{v.major}.{v.minor}" 757 758 @property 759 def pid(self): 760 return os.getpid() 761 762 @property 763 def script_name(self): 764 if self._script_name is object: 765 main = sys.modules["__main__"] 766 if hasattr(main, "__file__"): 767 self._script_name = os.path.join(_curdir, main.__file__) 768 else: 769 self._script_name = "<shell>" 770 return self._script_name 771 772 @property 773 def short_script_name(self): 774 if self._short_script_name is object: 775 script_name = self.script_name 776 if script_name != "<shell>": 777 userhome = os.path.expanduser("~") 778 if script_name.startswith(userhome+"/"): 779 script_name = "~" + script_name[len(userhome):] 780 self._short_script_name = script_name 781 return self._short_script_name 782 783 @property 784 def script_url(self): 785 if self._script_url is object: 786 from ll import url 787 u = self.short_script_name 788 if u != "<shell>": 789 u = str(url.Ssh(self.user_name, self.host_fqdn or self.host_name, u)) 790 self._script_url = u 791 return self._script_url 792 793 def __getitem__(self, key): 794 if key in self._keys: 795 return getattr(self, key) 796 raise KeyError(key) 797 798 def __iter__(self): 799 return iter(self._keys) 800 801 802# Single instance 803sysinfo = SysInfo() 804 805 806class monthdelta: 807 """ 808 :class:`monthdelta` objects can be used to add months/years to a 809 :class:`datetime.datetime` or :class:`datetime.date` object. If the resulting 810 day falls out of the range of valid days for the target month, the last day 811 for the target month will be used instead:: 812 813 >>> import datetime 814 >>> from ll import misc 815 >>> datetime.date(2000, 1, 31) + misc.monthdelta(1) 816 datetime.date(2000, 2, 29) 817 """ 818 819 __slots__ = ("_months",) 820 ul4attrs = {"months"} 821 822 def __init__(self, months=0): 823 self._months = months 824 825 def __bool__(self): 826 return self._months != 0 827 828 def __hash__(self): 829 return self._months 830 831 def __eq__(self, other): 832 return isinstance(other, monthdelta) and self._months == other._months 833 834 def __ne__(self, other): 835 return not isinstance(other, monthdelta) or self._months != other._months 836 837 def __lt__(self, other): 838 if not isinstance(other, monthdelta): 839 raise TypeError(f"unorderable types: {format_class(self)}() < {format_class(other)}()") 840 return self._months < other._months 841 842 def __le__(self, other): 843 if not isinstance(other, monthdelta): 844 raise TypeError(f"unorderable types: {format_class(self)}() <= {format_class(other)}()") 845 return self._months <= other._months 846 847 def __gt__(self, other): 848 if not isinstance(other, monthdelta): 849 raise TypeError(f"unorderable types: {format_class(self)}() > {format_class(other)}()") 850 return self._months > other._months 851 852 def __ge__(self, other): 853 if not isinstance(other, monthdelta): 854 raise TypeError(f"unorderable types: {format_class(self)}() >= {format_class(other)}()") 855 return self._months >= other._months 856 857 def __add__(self, other): 858 if isinstance(other, monthdelta): 859 return monthdelta(self._months+other._months) 860 elif isinstance(other, (datetime.datetime, datetime.date)): 861 year = other.year 862 month = other.month + self._months 863 (years_add, month) = divmod(month-1, 12) 864 month += 1 865 year += years_add 866 day = other.day 867 while True: 868 try: 869 return other.replace(year=year, month=month, day=day) 870 except ValueError: 871 day -= 1 872 if day == 1: 873 raise 874 else: 875 return NotImplemented 876 877 def __radd__(self, other): 878 return self.__add__(other) 879 880 def __sub__(self, other): 881 if isinstance(other, monthdelta): 882 return monthdelta(self._months-other._months) 883 else: 884 return NotImplemented 885 886 def __rsub__(self, other): 887 return other + (-self) 888 889 def __neg__(self): 890 return monthdelta(-self._months) 891 892 def __abs__(self): 893 return monthdelta(abs(self._months)) 894 895 def __mul__(self, other): 896 if isinstance(other, int) and not isinstance(other, monthdelta): 897 return monthdelta(self._months*other) 898 else: 899 return NotImplemented 900 901 def __rmul__(self, other): 902 return self.__mul__(other) 903 904 def __floordiv__(self, other): 905 if isinstance(other, int): 906 return monthdelta(self._months//other) 907 elif isinstance(other, monthdelta): 908 return self._months//other._months 909 else: 910 return NotImplemented 911 912 def __truediv__(self, other): 913 if isinstance(other, monthdelta): 914 return self._months/other._months 915 else: 916 return NotImplemented 917 918 def __str__(self): 919 m = self._months 920 return f"{m} month{'s' if m != 1 and m != -1 else ''}" 921 922 def __repr__(self): 923 m = self._months 924 if m: 925 return f"monthdelta({m!r})" 926 else: 927 return f"monthdelta()" 928 929 def months(self): 930 return self._months 931 932 933class Timeout(Exception): 934 """ 935 Exception that is raised when a timeout in :func:`timeout` occurs. 936 """ 937 def __init__(self, seconds): 938 self.seconds = seconds 939 940 def __str__(self): 941 return f"timed out after {self.seconds} seconds" 942 943 944@contextlib.contextmanager 945def timeout(seconds): 946 """ 947 A context manager that limits the runtime of the wrapped code. 948 949 This doesn't work with threads and only on UNIX. 950 """ 951 952 def _timeouthandler(signum, frame): 953 raise Timeout(seconds) 954 955 oldsignal = signal.signal(signal.SIGALRM, _timeouthandler) 956 signal.alarm(seconds) 957 try: 958 yield 959 finally: 960 signal.alarm(0) 961 signal.signal(signal.SIGALRM, oldsignal) 962 963 964def notifystart(): 965 """ 966 Notify OS X of the start of a process by removing the previous notification. 967 """ 968 cmd = [notifycmd, "-remove", sysinfo.script_name] 969 970 with open("/dev/null", "wb") as f: 971 status = subprocess.call(cmd, stdout=f) 972 973 974def notifyfinish(title, subtitle, message): 975 """ 976 Notify OS X of the end of a process. 977 """ 978 cmd = [notifycmd, "-title", title, "-subtitle", subtitle, "-message", message, "-group", sysinfo.script_name] 979 980 with open("/dev/null", "wb") as f: 981 status = subprocess.call(cmd, stdout=f) 982 983 984def prettycsv(rows, padding=" "): 985 """ 986 Format table :obj:`rows`. 987 988 :obj:`rows` must be a list of lists of strings (e.g. as produced by the 989 :mod:`csv` module). :obj:`padding` is the padding between columns. 990 991 :func:`prettycsv` is a generator. 992 """ 993 994 def width(row, i): 995 try: 996 return len(row[i]) 997 except IndexError: 998 return 0 999 1000 maxlen = max(len(row) for row in rows) 1001 lengths = [max(width(row, i) for row in rows) for i in range(maxlen)] 1002 for row in rows: 1003 lasti = len(row)-1 1004 for (i, (w, f)) in enumerate(zip(lengths, row)): 1005 if i: 1006 yield padding 1007 if i == lasti: 1008 f = f.rstrip() # don't add padding to the last column 1009 else: 1010 f = f"{f:<{w}}" 1011 yield f 1012 yield "\n" 1013 1014 1015class JSMinUnterminatedComment(Exception): 1016 pass 1017 1018 1019class JSMinUnterminatedStringLiteral(Exception): 1020 pass 1021 1022 1023class JSMinUnterminatedRegularExpression(Exception): 1024 pass 1025 1026 1027def jsmin(input): 1028 """ 1029 Minimizes the Javascript source :obj:`input`. 1030 """ 1031 indata = iter(input.replace("\r", "\n")) 1032 1033 # Copy the input to the output, deleting the characters which are 1034 # insignificant to JavaScript. Comments will be removed. Tabs will be 1035 # replaced with spaces. Carriage returns will be replaced with linefeeds. 1036 # Most spaces and linefeeds will be removed. 1037 1038 class var: 1039 a = "\n" 1040 b = None 1041 lookahead = None 1042 outdata = [] 1043 1044 def _get(): 1045 # Return the next character from the input. Watch out for lookahead. If 1046 # the character is a control character, translate it to a space or linefeed. 1047 c = var.lookahead 1048 var.lookahead = None 1049 if c is None: 1050 try: 1051 c = next(indata) 1052 except StopIteration: 1053 return "" # EOF 1054 if c >= " " or c == "\n": 1055 return c 1056 return " " 1057 1058 def _peek(): 1059 var.lookahead = _get() 1060 return var.lookahead 1061 1062 def isalphanum(c): 1063 # Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character. 1064 return ('a' <= c <= 'z') or ('0' <= c <= '9') or ('A' <= c <= 'Z') or c in "_$\\" or (c is not None and ord(c) > 126) 1065 1066 def _next(): 1067 # Get the next character, excluding comments. peek() is used to see if an unescaped '/' is followed by a '/' or '*'. 1068 c = _get() 1069 if c == "/" and var.a != "\\": 1070 p = _peek() 1071 if p == "/": 1072 c = _get() 1073 while c > "\n": 1074 c = _get() 1075 return c 1076 if p == "*": 1077 c = _get() 1078 while 1: 1079 c = _get() 1080 if c == "*": 1081 if _peek() == "/": 1082 _get() 1083 return " " 1084 if not c: 1085 raise JSMinUnterminatedComment() 1086 return c 1087 1088 def _action(action): 1089 """ 1090 Do something! What you do is determined by the argument: 1091 1 Output A. Copy B to A. Get the next B. 1092 2 Copy B to A. Get the next B. (Delete A). 1093 3 Get the next B. (Delete B). 1094 action treats a string as a single character. Wow! 1095 action recognizes a regular expression if it is preceded by ( or , or =. 1096 """ 1097 if action <= 1: 1098 outdata.append(var.a) 1099 1100 if action <= 2: 1101 var.a = var.b 1102 if var.a in "'\"": 1103 while True: 1104 outdata.append(var.a) 1105 var.a = _get() 1106 if var.a == var.b: 1107 break 1108 if var.a <= "\n": 1109 raise JSMinUnterminatedStringLiteral() 1110 if var.a == "\\": 1111 outdata.append(var.a) 1112 var.a = _get() 1113 1114 if action <= 3: 1115 var.b = _next() 1116 if var.b == "/" and var.a in "(,=:[?!&|;{}\n": 1117 outdata.append(var.a) 1118 outdata.append(var.b) 1119 while True: 1120 var.a = _get() 1121 if var.a == "/": 1122 break 1123 elif var.a == "\\": 1124 outdata.append(var.a) 1125 var.a = _get() 1126 elif var.a <= "\n": 1127 raise JSMinUnterminatedRegularExpression() 1128 outdata.append(var.a) 1129 var.b = _next() 1130 1131 _action(3) 1132 1133 while var.a: 1134 if var.a == " ": 1135 _action(1 if isalphanum(var.b) else 2) 1136 elif var.a == "\n": 1137 if var.b in "{[(+-": 1138 _action(1) 1139 elif var.b == " ": 1140 _action(3) 1141 else: 1142 _action(1 if isalphanum(var.b) else 2) 1143 else: 1144 if var.b == " ": 1145 _action(1 if isalphanum(var.a) else 3) 1146 elif var.b == "\n": 1147 _action(1 if isalphanum(var.a) or var.a in "}])+-\"'" else 3) 1148 else: 1149 _action(1) 1150 return "".join(outdata).lstrip() 1151