jlib.py 73 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307
  1. import calendar
  2. import codecs
  3. import inspect
  4. import io
  5. import os
  6. import platform
  7. import re
  8. import shlex
  9. import shutil
  10. import subprocess
  11. import sys
  12. import tarfile
  13. import textwrap
  14. import time
  15. import traceback
  16. import types
  17. import typing
  18. def place( frame_record=1):
  19. '''
  20. Useful debugging function - returns representation of source position of
  21. caller.
  22. frame_record:
  23. Integer number of frames up stack, or a `FrameInfo` (for example from
  24. `inspect.stack()`).
  25. '''
  26. if isinstance( frame_record, int):
  27. frame_record = inspect.stack( context=0)[ frame_record+1]
  28. filename = frame_record.filename
  29. line = frame_record.lineno
  30. function = frame_record.function
  31. ret = os.path.split( filename)[1] + ':' + str( line) + ':' + function + ':'
  32. if 0: # lgtm [py/unreachable-statement]
  33. tid = str( threading.currentThread())
  34. ret = '[' + tid + '] ' + ret
  35. return ret
  36. def text_nv( text, caller=1):
  37. '''
  38. Returns `text` with special handling of `{<expression>}` items
  39. constituting an enhanced and deferred form of Python f-strings
  40. (https://docs.python.org/3/reference/lexical_analysis.html#f-strings).
  41. text:
  42. String containing `{<expression>}` items.
  43. caller:
  44. If an `int`, the number of frames to step up when looking for file:line
  45. information or evaluating expressions.
  46. Otherwise should be a frame record as returned by `inspect.stack()[]`.
  47. `<expression>` items are evaluated in `caller`'s context using `eval()`.
  48. If `expression` ends with `=` or has a `=` before `!` or `:`, this
  49. character is removed and we prefix the result with `<expression>`=.
  50. >>> x = 45
  51. >>> y = 'hello'
  52. >>> text_nv( 'foo {x} {y=}')
  53. "foo 45 y='hello'"
  54. `<expression>` can also use ':' and '!' to control formatting, like
  55. `str.format()`. We support '=' being before (PEP 501) or after the ':' or
  56. `'!'.
  57. >>> x = 45
  58. >>> y = 'hello'
  59. >>> text_nv( 'foo {x} {y} {y!r=}')
  60. "foo 45 hello y='hello'"
  61. >>> text_nv( 'foo {x} {y=!r}')
  62. "foo 45 y='hello'"
  63. If `<expression>` starts with '=', this character is removed and we show
  64. each space-separated item in the remaining text as though it was appended
  65. with '='.
  66. >>> foo = 45
  67. >>> y = 'hello'
  68. >>> text_nv('{=foo y}')
  69. "foo=45 y='hello'"
  70. Also see https://peps.python.org/pep-0501/.
  71. Check handling of ':' within brackets:
  72. >>> text_nv('{time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(1670059297))=}')
  73. 'time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(1670059297))=\\'2022-12-03 09:21:37\\''
  74. '''
  75. if isinstance( caller, int):
  76. frame_record = inspect.stack()[ caller]
  77. else:
  78. frame_record = caller
  79. frame = frame_record.frame
  80. try:
  81. def get_items():
  82. '''
  83. Yields `(pre, item)`, where `item` is contents of next `{...}` or
  84. `None`, and `pre` is preceding text.
  85. '''
  86. pos = 0
  87. pre = ''
  88. while 1:
  89. if pos == len( text):
  90. yield pre, None
  91. break
  92. rest = text[ pos:]
  93. if rest.startswith( '{{') or rest.startswith( '}}'):
  94. pre += rest[0]
  95. pos += 2
  96. elif text[ pos] == '{':
  97. close = text.find( '}', pos)
  98. if close < 0:
  99. raise Exception( 'After "{" at offset %s, cannot find closing "}". text is: %r' % (
  100. pos, text))
  101. text2 = text[ pos+1 : close]
  102. if text2.startswith('='):
  103. text2 = text2[1:]
  104. for i, text3 in enumerate(text2.split()):
  105. pre2 = ' ' if i else pre
  106. yield pre2, text3 + '='
  107. else:
  108. yield pre, text[ pos+1 : close]
  109. pre = ''
  110. pos = close + 1
  111. else:
  112. pre += text[ pos]
  113. pos += 1
  114. ret = ''
  115. for pre, item in get_items():
  116. ret += pre
  117. nv = False
  118. if item:
  119. if item.endswith( '='):
  120. nv = True
  121. item = item[:-1]
  122. expression, tail = text_split_last_of( item, ')]!:')
  123. if tail.startswith( (')', ']')):
  124. expression, tail = item, ''
  125. if expression.endswith('='):
  126. # Basic PEP 501 support.
  127. nv = True
  128. expression = expression[:-1]
  129. if nv and not tail:
  130. # Default to !r as in PEP 501.
  131. tail = '!r'
  132. try:
  133. value = eval( expression, frame.f_globals, frame.f_locals)
  134. value_text = ('{0%s}' % tail).format( value)
  135. except Exception as e:
  136. value_text = '{??Failed to evaluate %r in context %s:%s; expression=%r tail=%r: %s}' % (
  137. expression,
  138. frame_record.filename,
  139. frame_record.lineno,
  140. expression,
  141. tail,
  142. e,
  143. )
  144. if nv:
  145. ret += '%s=' % expression
  146. ret += value_text
  147. return ret
  148. finally:
  149. del frame # lgtm [py/unnecessary-delete]
  150. class LogPrefixTime:
  151. def __init__( self, date=False, time_=True, elapsed=False):
  152. self.date = date
  153. self.time = time_
  154. self.elapsed = elapsed
  155. self.t0 = time.time()
  156. def __call__( self):
  157. ret = ''
  158. if self.date:
  159. ret += time.strftime( ' %F')
  160. if self.time:
  161. ret += time.strftime( ' %T')
  162. if self.elapsed:
  163. ret += ' (+%s)' % time_duration( time.time() - self.t0, s_format='%.1f')
  164. if ret:
  165. ret = ret.strip() + ': '
  166. return ret
  167. class LogPrefixFileLine:
  168. def __call__( self, caller):
  169. if isinstance( caller, int):
  170. caller = inspect.stack()[ caller]
  171. return place( caller) + ' '
  172. class LogPrefixScopes:
  173. '''
  174. Internal use only.
  175. '''
  176. def __init__( self):
  177. self.items = []
  178. def __call__( self):
  179. ret = ''
  180. for item in self.items:
  181. if callable( item):
  182. item = item()
  183. ret += item
  184. return ret
  185. class LogPrefixScope:
  186. '''
  187. Can be used to insert scoped prefix to log output.
  188. '''
  189. def __init__( self, prefix):
  190. self.prefix = prefix
  191. def __enter__( self):
  192. g_log_prefix_scopes.items.append( self.prefix)
  193. def __exit__( self, exc_type, exc_value, traceback):
  194. global g_log_prefix
  195. g_log_prefix_scopes.items.pop()
  196. g_log_delta = 0
  197. class LogDeltaScope:
  198. '''
  199. Can be used to temporarily change verbose level of logging.
  200. E.g to temporarily increase logging::
  201. with jlib.LogDeltaScope(-1):
  202. ...
  203. '''
  204. def __init__( self, delta):
  205. self.delta = delta
  206. global g_log_delta
  207. g_log_delta += self.delta
  208. def __enter__( self):
  209. pass
  210. def __exit__( self, exc_type, exc_value, traceback):
  211. global g_log_delta
  212. g_log_delta -= self.delta
  213. # Special item that can be inserted into <g_log_prefixes> to enable
  214. # temporary addition of text into log prefixes.
  215. #
  216. g_log_prefix_scopes = LogPrefixScopes()
  217. # List of items that form prefix for all output from log().
  218. #
  219. g_log_prefixes = [
  220. LogPrefixTime( time_=False, elapsed=True),
  221. g_log_prefix_scopes,
  222. LogPrefixFileLine(),
  223. ]
  224. _log_text_line_start = True
  225. def log_text( text=None, caller=1, nv=True, raw=False, nl=True):
  226. '''
  227. Returns log text, prepending all lines with text from `g_log_prefixes`.
  228. text:
  229. The text to output.
  230. caller:
  231. If an int, the number of frames to step up when looking for file:line
  232. information or evaluating expressions.
  233. Otherwise should be a frame record as returned by `inspect.stack()[]`.
  234. nv:
  235. If true, we expand `{...}` in `text` using `jlib.text_nv()`.
  236. raw:
  237. If true we don't terminate with newlines and store state in
  238. `_log_text_line_start` so that we generate correct content if sent sent
  239. partial lines.
  240. nl:
  241. If true (the default) we terminate text with a newline if not already
  242. present. Ignored if `raw` is true.
  243. '''
  244. if isinstance( caller, int):
  245. caller += 1
  246. # Construct line prefix.
  247. prefix = ''
  248. for p in g_log_prefixes:
  249. if callable( p):
  250. if isinstance( p, LogPrefixFileLine):
  251. p = p(caller)
  252. else:
  253. p = p()
  254. prefix += p
  255. if text is None:
  256. return prefix
  257. # Expand {...} using our enhanced f-string support.
  258. if nv:
  259. text = text_nv( text, caller)
  260. # Prefix each line. If <raw> is false, we terminate the last line with a
  261. # newline. Otherwise we use _log_text_line_start to remember whether we are
  262. # at the beginning of a line.
  263. #
  264. global _log_text_line_start
  265. text2 = ''
  266. pos = 0
  267. while 1:
  268. if pos == len(text):
  269. break
  270. if not raw or _log_text_line_start:
  271. text2 += prefix
  272. nlp = text.find('\n', pos)
  273. if nlp == -1:
  274. text2 += text[pos:]
  275. if not raw and nl:
  276. text2 += '\n'
  277. pos = len(text)
  278. else:
  279. text2 += text[pos:nlp+1]
  280. pos = nlp+1
  281. if raw:
  282. _log_text_line_start = (nlp >= 0)
  283. return text2
  284. s_log_levels_cache = dict()
  285. s_log_levels_items = []
  286. def log_levels_find( caller):
  287. if not s_log_levels_items:
  288. return 0
  289. tb = traceback.extract_stack( None, 1+caller)
  290. if len(tb) == 0:
  291. return 0
  292. filename, line, function, text = tb[0]
  293. key = function, filename, line,
  294. delta = s_log_levels_cache.get( key)
  295. if delta is None:
  296. # Calculate and populate cache.
  297. delta = 0
  298. for item_function, item_filename, item_delta in s_log_levels_items:
  299. if item_function and not function.startswith( item_function):
  300. continue
  301. if item_filename and not filename.startswith( item_filename):
  302. continue
  303. delta = item_delta
  304. break
  305. s_log_levels_cache[ key] = delta
  306. return delta
  307. def log_levels_add( delta, filename_prefix, function_prefix):
  308. '''
  309. `jlib.log()` calls from locations with filenames starting with
  310. `filename_prefix` and/or function names starting with `function_prefix`
  311. will have `delta` added to their level.
  312. Use -ve `delta` to increase verbosity from particular filename or function
  313. prefixes.
  314. '''
  315. log( 'adding level: {filename_prefix=!r} {function_prefix=!r}')
  316. # Sort in reverse order so that long functions and filename specs come
  317. # first.
  318. #
  319. s_log_levels_items.append( (function_prefix, filename_prefix, delta))
  320. s_log_levels_items.sort( reverse=True)
  321. s_log_out = sys.stdout
  322. def log( text, level=0, caller=1, nv=True, out=None, raw=False):
  323. '''
  324. Writes log text, with special handling of `{<expression>}` items in `text`
  325. similar to python3's f-strings.
  326. text:
  327. The text to output.
  328. level:
  329. Lower values are more verbose.
  330. caller:
  331. How many frames to step up to get caller's context when evaluating
  332. file:line information and/or expressions. Or frame record as returned
  333. by `inspect.stack()[]`.
  334. nv:
  335. If true, we expand `{...}` in `text` using `jlib.text_nv()`.
  336. out:
  337. Where to send output. If None we use sys.stdout.
  338. raw:
  339. If true we don't ensure output text is terminated with a newline. E.g.
  340. use by `jlib.system()` when sending us raw output which is not
  341. line-based.
  342. `<expression>` is evaluated in our caller's context (`n` stack frames up)
  343. using `eval()`, and expanded to `<expression>` or `<expression>=<value>`.
  344. If `<expression>` ends with '=', this character is removed and we prefix
  345. the result with <expression>=.
  346. E.g.::
  347. x = 45
  348. y = 'hello'
  349. text_nv( 'foo {x} {y=}')
  350. returns::
  351. foo 45 y=hello
  352. `<expression>` can also use ':' and '!' to control formatting, like
  353. `str.format()`.
  354. '''
  355. if out is None:
  356. out = s_log_out
  357. level += g_log_delta
  358. if isinstance( caller, int):
  359. caller += 1
  360. level += log_levels_find( caller)
  361. if level <= 0:
  362. text = log_text( text, caller, nv=nv, raw=raw)
  363. try:
  364. out.write( text)
  365. except UnicodeEncodeError:
  366. # Retry, ignoring errors by encoding then decoding with
  367. # errors='replace'.
  368. #
  369. out.write('[***write encoding error***]')
  370. text_encoded = codecs.encode(text, out.encoding, errors='replace')
  371. text_encoded_decoded = codecs.decode(text_encoded, out.encoding, errors='replace')
  372. out.write(text_encoded_decoded)
  373. out.write('[/***write encoding error***]')
  374. out.flush()
  375. def log_raw( text, level=0, caller=1, nv=False, out=None):
  376. '''
  377. Like `jlib.log()` but defaults to `nv=False` so any `{...}` are not
  378. evaluated as expressions.
  379. Useful for things like::
  380. jlib.system(..., out=jlib.log_raw)
  381. '''
  382. log( text, level=0, caller=caller+1, nv=nv, out=out)
  383. def log0( text, caller=1, nv=True, out=None):
  384. '''
  385. Most verbose log. Same as log().
  386. '''
  387. log( text, level=0, caller=caller+1, nv=nv, out=out)
  388. def log1( text, caller=1, nv=True, out=None):
  389. log( text, level=1, caller=caller+1, nv=nv, out=out)
  390. def log2( text, caller=1, nv=True, out=None):
  391. log( text, level=2, caller=caller+1, nv=nv, out=out)
  392. def log3( text, caller=1, nv=True, out=None):
  393. log( text, level=3, caller=caller+1, nv=nv, out=out)
  394. def log4( text, caller=1, nv=True, out=None):
  395. log( text, level=4, caller=caller+1, nv=nv, out=out)
  396. def log5( text, caller=1, nv=True, out=None):
  397. '''
  398. Least verbose log.
  399. '''
  400. log( text, level=5, caller=caller+1, nv=nv, out=out)
  401. def logx( text, caller=1, nv=True, out=None):
  402. '''
  403. Does nothing, useful when commenting out a log().
  404. '''
  405. pass
  406. _log_interval_t0 = 0
  407. def log_interval( text, level=0, caller=1, nv=True, out=None, raw=False, interval=10):
  408. '''
  409. Like `jlib.log()` but outputs no more than one diagnostic every `interval`
  410. seconds, and `text` can be a callable taking no args and returning a
  411. string.
  412. '''
  413. global _log_interval_t0
  414. t = time.time()
  415. if t - _log_interval_t0 > interval:
  416. _log_interval_t0 = t
  417. if callable( text):
  418. text = text()
  419. log( text, level=level, caller=caller+1, nv=nv, out=out, raw=raw)
  420. def log_levels_add_env( name='JLIB_log_levels'):
  421. '''
  422. Added log levels encoded in an environmental variable.
  423. '''
  424. t = os.environ.get( name)
  425. if t:
  426. for ffll in t.split( ','):
  427. ffl, delta = ffll.split( '=', 1)
  428. delta = int( delta)
  429. ffl = ffl.split( ':')
  430. if 0: # lgtm [py/unreachable-statement]
  431. pass
  432. elif len( ffl) == 1:
  433. filename = ffl
  434. function = None
  435. elif len( ffl) == 2:
  436. filename, function = ffl
  437. else:
  438. assert 0
  439. log_levels_add( delta, filename, function)
  440. class TimingsItem:
  441. '''
  442. Helper for `Timings` class.
  443. '''
  444. def __init__( self, name):
  445. self.name = name
  446. self.children = dict()
  447. self.t_begin = None
  448. self.t = 0
  449. self.n = 0
  450. def begin( self, t):
  451. assert self.t_begin is None
  452. self.t_begin = t
  453. def end( self, t):
  454. assert self.t_begin is not None, f't_begin is None, .name={self.name}'
  455. self.t += t - self.t_begin
  456. self.n += 1
  457. self.t_begin = None
  458. def __str__( self):
  459. return f'[name={self.name} t={self.t} n={self.n} t_begin={self.t_begin}]'
  460. def __repr__( self):
  461. return self.__str__()
  462. class Timings:
  463. '''
  464. Allows gathering of hierarchical timing information. Can also generate
  465. useful diagnostics.
  466. Caller can generate a tree of `TimingsItem` items via our `begin()` and
  467. `end()` methods.
  468. >>> ts = Timings()
  469. >>> ts.begin('a')
  470. >>> time.sleep(0.1)
  471. >>> ts.begin('b')
  472. >>> time.sleep(0.2)
  473. >>> ts.begin('c')
  474. >>> time.sleep(0.3)
  475. >>> ts.end('c')
  476. >>> ts.begin('c')
  477. >>> time.sleep(0.3)
  478. >>> ts.end('b') # will also end 'c'.
  479. >>> ts.begin('d')
  480. >>> ts.begin('e')
  481. >>> time.sleep(0.1)
  482. >>> ts.end_all() # will end everything.
  483. >>> print(ts)
  484. Timings (in seconds):
  485. 1.0 a
  486. 0.8 b
  487. 0.6/2 c
  488. 0.1 d
  489. 0.1 e
  490. <BLANKLINE>
  491. One can also use as a context manager:
  492. >>> ts = Timings()
  493. >>> with ts( 'foo'):
  494. ... time.sleep(1)
  495. ... with ts( 'bar'):
  496. ... time.sleep(1)
  497. >>> print( ts)
  498. Timings (in seconds):
  499. 2.0 foo
  500. 1.0 bar
  501. <BLANKLINE>
  502. Must specify name, otherwise we assert-fail.
  503. >>> with ts:
  504. ... pass
  505. Traceback (most recent call last):
  506. AssertionError: Must specify <name> etc when using "with ...".
  507. '''
  508. def __init__( self, name='', active=True):
  509. '''
  510. If `active` is False, returned instance does nothing.
  511. '''
  512. self.active = active
  513. self.root_item = TimingsItem( name)
  514. self.nest = [ self.root_item]
  515. self.nest[0].begin( time.time())
  516. self.name_max_len = 0
  517. self.call_enter_state = None
  518. self.call_enter_stack = []
  519. def begin( self, name=None, text=None, level=0, t=None):
  520. '''
  521. Starts a new timing item as child of most recent in-progress timing
  522. item.
  523. name:
  524. Used in final statistics. If `None`, we use `jlib.place()`.
  525. text:
  526. If not `None`, this is output here with `jlib.log()`.
  527. level:
  528. Verbosity. Added to `g_verbose`.
  529. '''
  530. if not self.active:
  531. return
  532. if t is None:
  533. t = time.time()
  534. if name is None:
  535. name = place(2)
  536. self.name_max_len = max( self.name_max_len, len(name))
  537. leaf = self.nest[-1].children.setdefault( name, TimingsItem( name))
  538. self.nest.append( leaf)
  539. leaf.begin( t)
  540. if text:
  541. log( text, nv=0)
  542. def end( self, name=None, t=None):
  543. '''
  544. Repeatedly ends the most recent item until we have ended item called
  545. `name`. Ends just the most recent item if name is `None`.
  546. '''
  547. if not self.active:
  548. return
  549. if t is None:
  550. t = time.time()
  551. if name is None:
  552. name = self.nest[-1].name
  553. while self.nest:
  554. leaf = self.nest.pop()
  555. leaf.end( t)
  556. if leaf.name == name:
  557. break
  558. else:
  559. if name is not None:
  560. log( f'*** Warning: cannot end timing item called {name} because not found.')
  561. def end_all( self):
  562. self.end( self.nest[0].name)
  563. def mid( self, name=None):
  564. '''
  565. Ends current leaf item and starts a new item called `name`. Useful to
  566. define multiple timing blocks at same level.
  567. '''
  568. if not self.active:
  569. return
  570. t = time.time()
  571. if len( self.nest) > 1:
  572. self.end( self.nest[-1].name, t)
  573. self.begin( name, t=t)
  574. def __enter__( self):
  575. if not self.active:
  576. return
  577. assert self.call_enter_state, 'Must specify <name> etc when using "with ...".'
  578. name, text, level = self.call_enter_state
  579. self.begin( name, text, level)
  580. self.call_enter_state = None
  581. self.call_enter_stack.append( name)
  582. def __exit__( self, type, value, traceback):
  583. if not self.active:
  584. return
  585. assert not self.call_enter_state, f'self.call_enter_state is not false: {self.call_enter_state}'
  586. name = self.call_enter_stack.pop()
  587. self.end( name)
  588. def __call__( self, name=None, text=None, level=0):
  589. '''
  590. Allow scoped timing.
  591. '''
  592. if not self.active:
  593. return self
  594. assert not self.call_enter_state, f'self.call_enter_state is not false: {self.call_enter_state}'
  595. self.call_enter_state = ( name, text, level)
  596. return self
  597. def text( self, item, depth=0, precision=1):
  598. '''
  599. Returns text showing hierarchical timing information.
  600. '''
  601. if not self.active:
  602. return ''
  603. if item is self.root_item and not item.name:
  604. # Don't show top-level.
  605. ret = ''
  606. else:
  607. tt = ' None' if item.t is None else f'{item.t:6.{precision}f}'
  608. n = f'/{item.n}' if item.n >= 2 else ''
  609. ret = f'{" " * 4 * depth} {tt}{n} {item.name}\n'
  610. depth += 1
  611. for _, timing2 in item.children.items():
  612. ret += self.text( timing2, depth, precision)
  613. return ret
  614. def __str__( self):
  615. ret = 'Timings (in seconds):\n'
  616. ret += self.text( self.root_item, 0)
  617. return ret
  618. def text_strpbrk_reverse( text, substrings):
  619. '''
  620. Finds last occurrence of any item in `substrings` in `text`.
  621. Returns `(pos, substring)` or `(len(text), None)` if not found.
  622. '''
  623. ret_pos = -1
  624. ret_substring = None
  625. for substring in substrings:
  626. pos = text.rfind( substring)
  627. if pos >= 0 and pos > ret_pos:
  628. ret_pos = pos
  629. ret_substring = substring
  630. if ret_pos == -1:
  631. ret_pos = len( text)
  632. return ret_pos, ret_substring
  633. def text_split_last_of( text, substrings):
  634. '''
  635. Returns `(pre, post)`, where `pre` doesn't contain any item in `substrings`
  636. and `post` is empty or starts with an item in `substrings`.
  637. '''
  638. pos, _ = text_strpbrk_reverse( text, substrings)
  639. return text[ :pos], text[ pos:]
  640. log_levels_add_env()
  641. def force_line_buffering():
  642. '''
  643. Ensure `sys.stdout` and `sys.stderr` are line-buffered. E.g. makes things
  644. work better if output is piped to a file via 'tee'.
  645. Returns original out,err streams.
  646. '''
  647. stdout0 = sys.stdout
  648. stderr0 = sys.stderr
  649. sys.stdout = os.fdopen( sys.stdout.fileno(), 'w', 1)
  650. sys.stderr = os.fdopen( sys.stderr.fileno(), 'w', 1)
  651. return stdout0, stderr0
  652. def exception_info(
  653. exception_or_traceback=None,
  654. limit=None,
  655. file=None,
  656. chain=True,
  657. outer=True,
  658. show_exception_type=True,
  659. _filelinefn=True,
  660. ):
  661. '''
  662. Shows an exception and/or backtrace.
  663. Alternative to `traceback.*` functions that print/return information about
  664. exceptions and backtraces, such as:
  665. * `traceback.format_exc()`
  666. * `traceback.format_exception()`
  667. * `traceback.print_exc()`
  668. * `traceback.print_exception()`
  669. Install as system default with:
  670. `sys.excepthook = lambda type_, exception, traceback: jlib.exception_info( exception)`
  671. Returns `None`, or the generated text if `file` is 'return'.
  672. Args:
  673. exception_or_traceback:
  674. `None`, a `BaseException`, a `types.TracebackType` (typically from
  675. an exception's `.__traceback__` member) or an `inspect.FrameInfo`.
  676. If `None` we use current exception from `sys.exc_info()` if set,
  677. otherwise the current backtrace from `inspect.stack()`.
  678. limit:
  679. As in `traceback.*` functions: `None` to show all frames, positive
  680. to show last `limit` frames, negative to exclude outermost `-limit`
  681. frames. Zero to not show any backtraces.
  682. file:
  683. As in `traceback.*` functions: file-like object to which we write
  684. output, or `sys.stderr` if `None`. Special value 'return' makes us
  685. return our output as a string.
  686. chain:
  687. As in `traceback.*` functions: if true (the default) we show
  688. chained exceptions as described in PEP-3134. Special value
  689. 'because' reverses the usual ordering, showing higher-level
  690. exceptions first and joining with 'Because:' text.
  691. outer:
  692. If true (the default) we also show an exception's outer frames
  693. above the `catch` block (see next section for details). We
  694. use `outer=false` internally for chained exceptions to avoid
  695. duplication.
  696. show_exception_type:
  697. Controls whether exception text is prefixed by
  698. `f'{type(exception)}: '`. If callable we only include this prefix
  699. if `show_exception_type(exception)` is true. Otherwise if true (the
  700. default) we include the prefix for all exceptions (this mimcs the
  701. behaviour of `traceback.*` functions). Otherwise we exclude the
  702. prefix for all exceptions.
  703. _filelinefn:
  704. Internal only; makes us omit file:line: information to allow simple
  705. doctest comparison with expected output.
  706. Differences from `traceback.*` functions:
  707. Frames are displayed as one line in the form::
  708. <file>:<line>:<function>: <text>
  709. Filenames are displayed as relative to the current directory if
  710. applicable.
  711. Inclusion of outer frames:
  712. Unlike `traceback.*` functions, stack traces for exceptions include
  713. outer stack frames above the point at which an exception was caught
  714. - i.e. frames from the top-level <module> or thread creation to the
  715. catch block. [Search for 'sys.exc_info backtrace incomplete' for
  716. more details.]
  717. We separate the two parts of the backtrace using a marker line
  718. '^except raise:' where '^except' points upwards to the frame that
  719. caught the exception and 'raise:' refers downwards to the frame
  720. that raised the exception.
  721. So the backtrace for an exception looks like this::
  722. <file>:<line>:<fn>: <text> [in root module.]
  723. ... [... other frames]
  724. <file>:<line>:<fn>: <text> [in except: block where exception was caught.]
  725. ^except raise: [marker line]
  726. <file>:<line>:<fn>: <text> [in try: block.]
  727. ... [... other frames]
  728. <file>:<line>:<fn>: <text> [where the exception was raised.]
  729. Examples:
  730. In these examples we use `file=sys.stdout` so we can check the output
  731. with `doctest`, and set `_filelinefn=0` so that the output can be
  732. matched easily. We also use `+ELLIPSIS` and `...` to match arbitrary
  733. outer frames from the doctest code itself.
  734. Basic handling of an exception:
  735. >>> def c():
  736. ... raise Exception( 'c() failed')
  737. >>> def b():
  738. ... try:
  739. ... c()
  740. ... except Exception as e:
  741. ... exception_info( e, file=sys.stdout, _filelinefn=0)
  742. >>> def a():
  743. ... b()
  744. >>> a() # doctest: +REPORT_UDIFF +ELLIPSIS
  745. Traceback (most recent call last):
  746. ...
  747. a(): b()
  748. b(): exception_info( e, file=sys.stdout, _filelinefn=0)
  749. ^except raise:
  750. b(): c()
  751. c(): raise Exception( 'c() failed')
  752. Exception: c() failed
  753. Handling of chained exceptions:
  754. >>> def e():
  755. ... raise Exception( 'e(): deliberate error')
  756. >>> def d():
  757. ... e()
  758. >>> def c():
  759. ... try:
  760. ... d()
  761. ... except Exception as e:
  762. ... raise Exception( 'c: d() failed') from e
  763. >>> def b():
  764. ... try:
  765. ... c()
  766. ... except Exception as e:
  767. ... exception_info( file=sys.stdout, chain=g_chain, _filelinefn=0)
  768. >>> def a():
  769. ... b()
  770. With `chain=True` (the default), we output low-level exceptions
  771. first, matching the behaviour of `traceback.*` functions:
  772. >>> g_chain = True
  773. >>> a() # doctest: +REPORT_UDIFF +ELLIPSIS
  774. Traceback (most recent call last):
  775. c(): d()
  776. d(): e()
  777. e(): raise Exception( 'e(): deliberate error')
  778. Exception: e(): deliberate error
  779. <BLANKLINE>
  780. The above exception was the direct cause of the following exception:
  781. Traceback (most recent call last):
  782. ...
  783. <module>(): a() # doctest: +REPORT_UDIFF +ELLIPSIS
  784. a(): b()
  785. b(): exception_info( file=sys.stdout, chain=g_chain, _filelinefn=0)
  786. ^except raise:
  787. b(): c()
  788. c(): raise Exception( 'c: d() failed') from e
  789. Exception: c: d() failed
  790. With `chain='because'`, we output high-level exceptions first:
  791. >>> g_chain = 'because'
  792. >>> a() # doctest: +REPORT_UDIFF +ELLIPSIS
  793. Traceback (most recent call last):
  794. ...
  795. <module>(): a() # doctest: +REPORT_UDIFF +ELLIPSIS
  796. a(): b()
  797. b(): exception_info( file=sys.stdout, chain=g_chain, _filelinefn=0)
  798. ^except raise:
  799. b(): c()
  800. c(): raise Exception( 'c: d() failed') from e
  801. Exception: c: d() failed
  802. <BLANKLINE>
  803. Because:
  804. Traceback (most recent call last):
  805. c(): d()
  806. d(): e()
  807. e(): raise Exception( 'e(): deliberate error')
  808. Exception: e(): deliberate error
  809. Show current backtrace by passing `exception_or_traceback=None`:
  810. >>> def c():
  811. ... exception_info( None, file=sys.stdout, _filelinefn=0)
  812. >>> def b():
  813. ... return c()
  814. >>> def a():
  815. ... return b()
  816. >>> a() # doctest: +REPORT_UDIFF +ELLIPSIS
  817. Traceback (most recent call last):
  818. ...
  819. <module>(): a() # doctest: +REPORT_UDIFF +ELLIPSIS
  820. a(): return b()
  821. b(): return c()
  822. c(): exception_info( None, file=sys.stdout, _filelinefn=0)
  823. Show an exception's `.__traceback__` backtrace:
  824. >>> def c():
  825. ... raise Exception( 'foo') # raise
  826. >>> def b():
  827. ... return c() # call c
  828. >>> def a():
  829. ... try:
  830. ... b() # call b
  831. ... except Exception as e:
  832. ... exception_info( e.__traceback__, file=sys.stdout, _filelinefn=0)
  833. >>> a() # doctest: +REPORT_UDIFF +ELLIPSIS
  834. Traceback (most recent call last):
  835. ...
  836. a(): b() # call b
  837. b(): return c() # call c
  838. c(): raise Exception( 'foo') # raise
  839. '''
  840. # Set exactly one of <exception> and <tb>.
  841. #
  842. if isinstance( exception_or_traceback, (types.TracebackType, inspect.FrameInfo)):
  843. # Simple backtrace, no Exception information.
  844. exception = None
  845. tb = exception_or_traceback
  846. elif isinstance( exception_or_traceback, BaseException):
  847. exception = exception_or_traceback
  848. tb = None
  849. elif exception_or_traceback is None:
  850. # Show exception if available, else backtrace.
  851. _, exception, tb = sys.exc_info()
  852. tb = None if exception else inspect.stack()[1:]
  853. else:
  854. assert 0, f'Unrecognised exception_or_traceback type: {type(exception_or_traceback)}'
  855. if file == 'return':
  856. out = io.StringIO()
  857. else:
  858. out = file if file else sys.stderr
  859. def do_chain( exception):
  860. exception_info(
  861. exception,
  862. limit,
  863. out,
  864. chain,
  865. outer=False,
  866. show_exception_type=show_exception_type,
  867. _filelinefn=_filelinefn,
  868. )
  869. if exception and chain and chain != 'because' and chain != 'because-compact':
  870. # Output current exception first.
  871. if exception.__cause__:
  872. do_chain( exception.__cause__)
  873. out.write( '\nThe above exception was the direct cause of the following exception:\n')
  874. elif exception.__context__:
  875. do_chain( exception.__context__)
  876. out.write( '\nDuring handling of the above exception, another exception occurred:\n')
  877. cwd = os.getcwd() + os.sep
  878. def output_frames( frames, reverse, limit):
  879. if limit == 0:
  880. return
  881. if reverse:
  882. assert isinstance( frames, list)
  883. frames = reversed( frames)
  884. if limit is not None:
  885. frames = list( frames)
  886. frames = frames[ -limit:]
  887. for frame in frames:
  888. f, filename, line, fnname, text, index = frame
  889. text = text[0].strip() if text else ''
  890. if filename.startswith( cwd):
  891. filename = filename[ len(cwd):]
  892. if filename.startswith( f'.{os.sep}'):
  893. filename = filename[ 2:]
  894. if _filelinefn:
  895. out.write( f' {filename}:{line}:{fnname}(): {text}\n')
  896. else:
  897. out.write( f' {fnname}(): {text}\n')
  898. if limit != 0:
  899. out.write( 'Traceback (most recent call last):\n')
  900. if exception:
  901. tb = exception.__traceback__
  902. assert tb
  903. if outer:
  904. output_frames( inspect.getouterframes( tb.tb_frame), reverse=True, limit=limit)
  905. out.write( ' ^except raise:\n')
  906. limit2 = 0 if limit == 0 else None
  907. output_frames( inspect.getinnerframes( tb), reverse=False, limit=limit2)
  908. else:
  909. if not isinstance( tb, list):
  910. inner = inspect.getinnerframes(tb)
  911. outer = inspect.getouterframes(tb.tb_frame)
  912. tb = outer + inner
  913. tb.reverse()
  914. output_frames( tb, reverse=True, limit=limit)
  915. if exception:
  916. if callable(show_exception_type):
  917. show_exception_type2 = show_exception_type( exception)
  918. else:
  919. show_exception_type2 = show_exception_type
  920. if show_exception_type2:
  921. lines = traceback.format_exception_only( type(exception), exception)
  922. for line in lines:
  923. out.write( line)
  924. else:
  925. out.write( str( exception) + '\n')
  926. if exception and (chain == 'because' or chain == 'because-compact'):
  927. # Output current exception afterwards.
  928. pre, post = ('\n', '\n') if chain == 'because' else ('', ' ')
  929. if exception.__cause__:
  930. out.write( f'{pre}Because:{post}')
  931. do_chain( exception.__cause__)
  932. elif exception.__context__:
  933. out.write( f'{pre}Because: error occurred handling this exception:{post}')
  934. do_chain( exception.__context__)
  935. if file == 'return':
  936. return out.getvalue()
  937. def number_sep( s):
  938. '''
  939. Simple number formatter, adds commas in-between thousands. `s` can be a
  940. number or a string. Returns a string.
  941. >>> number_sep(1)
  942. '1'
  943. >>> number_sep(12)
  944. '12'
  945. >>> number_sep(123)
  946. '123'
  947. >>> number_sep(1234)
  948. '1,234'
  949. >>> number_sep(12345)
  950. '12,345'
  951. >>> number_sep(123456)
  952. '123,456'
  953. >>> number_sep(1234567)
  954. '1,234,567'
  955. '''
  956. if not isinstance( s, str):
  957. s = str( s)
  958. c = s.find( '.')
  959. if c==-1: c = len(s)
  960. end = s.find('e')
  961. if end == -1: end = s.find('E')
  962. if end == -1: end = len(s)
  963. ret = ''
  964. for i in range( end):
  965. ret += s[i]
  966. if i<c-1 and (c-i-1)%3==0:
  967. ret += ','
  968. elif i>c and i<end-1 and (i-c)%3==0:
  969. ret += ','
  970. ret += s[end:]
  971. return ret
  972. class Stream:
  973. '''
  974. Base layering abstraction for streams - abstraction for things like
  975. `sys.stdout` to allow prefixing of all output, e.g. with a timestamp.
  976. '''
  977. def __init__( self, stream):
  978. self.stream = stream
  979. def write( self, text):
  980. self.stream.write( text)
  981. class StreamPrefix:
  982. '''
  983. Prefixes output with a prefix, which can be a string, or a callable that
  984. takes no parameters and return a string, or an integer number of spaces.
  985. '''
  986. def __init__( self, stream, prefix):
  987. if callable(stream):
  988. self.stream_write = stream
  989. self.stream_flush = lambda: None
  990. else:
  991. self.stream_write = stream.write
  992. self.stream_flush = stream.flush
  993. self.at_start = True
  994. if callable(prefix):
  995. self.prefix = prefix
  996. elif isinstance( prefix, int):
  997. self.prefix = lambda: ' ' * prefix
  998. else:
  999. self.prefix = lambda : prefix
  1000. def write( self, text):
  1001. if self.at_start:
  1002. text = self.prefix() + text
  1003. self.at_start = False
  1004. append_newline = False
  1005. if text.endswith( '\n'):
  1006. text = text[:-1]
  1007. self.at_start = True
  1008. append_newline = True
  1009. text = text.replace( '\n', '\n%s' % self.prefix())
  1010. if append_newline:
  1011. text += '\n'
  1012. self.stream_write( text)
  1013. def flush( self):
  1014. self.stream_flush()
  1015. def time_duration( seconds, verbose=False, s_format='%i'):
  1016. '''
  1017. Returns string expressing an interval.
  1018. seconds:
  1019. The duration in seconds
  1020. verbose:
  1021. If true, return like '4 days 1 hour 2 mins 23 secs', otherwise as
  1022. '4d3h2m23s'.
  1023. s_format:
  1024. If specified, use as printf-style format string for seconds.
  1025. >>> time_duration( 303333)
  1026. '3d12h15m33s'
  1027. We pad single-digit numbers with '0' to keep things aligned:
  1028. >>> time_duration( 302703.33, s_format='%.1f')
  1029. '3d12h05m03.3s'
  1030. When verbose, we pad single-digit numbers with ' ' to keep things aligned:
  1031. >>> time_duration( 302703, verbose=True)
  1032. '3 days 12 hours 5 mins 3 secs'
  1033. >>> time_duration( 302703.33, verbose=True, s_format='%.1f')
  1034. '3 days 12 hours 5 mins 3.3 secs'
  1035. >>> time_duration( 0)
  1036. '0s'
  1037. >>> time_duration( 0, verbose=True)
  1038. '0 sec'
  1039. '''
  1040. x = abs(seconds)
  1041. ret = ''
  1042. i = 0
  1043. for div, text in [
  1044. ( 60, 'sec'),
  1045. ( 60, 'min'),
  1046. ( 24, 'hour'),
  1047. ( None, 'day'),
  1048. ]:
  1049. force = ( x == 0 and i == 0)
  1050. if div:
  1051. remainder = x % div
  1052. x = int( x/div)
  1053. else:
  1054. remainder = x
  1055. x = 0
  1056. if not verbose:
  1057. text = text[0]
  1058. if remainder or force:
  1059. if verbose and remainder > 1:
  1060. # plural.
  1061. text += 's'
  1062. if verbose:
  1063. text = ' %s ' % text
  1064. if i == 0:
  1065. remainder_string = s_format % remainder
  1066. else:
  1067. remainder_string = str( remainder)
  1068. if x and (remainder < 10):
  1069. # Pad with space or '0' to keep alignment.
  1070. pad = ' ' if verbose else '0'
  1071. remainder_string = pad + str(remainder_string)
  1072. ret = '%s%s%s' % ( remainder_string, text, ret)
  1073. i += 1
  1074. ret = ret.strip()
  1075. if ret == '':
  1076. ret = '0s'
  1077. if seconds < 0:
  1078. ret = '-%s' % ret
  1079. return ret
  1080. def date_time( t=None):
  1081. if t is None:
  1082. t = time.time()
  1083. return time.strftime( "%F-%T", time.gmtime( t))
  1084. def time_read_date1( text):
  1085. '''
  1086. <text> is:
  1087. <year>-<month>-<day>-<hour>-<min>-<sec>
  1088. Trailing values can be omitted, e.g. `2004-3' is treated as
  1089. 2004-03-0-0-0-0, i.e. 1st of March 2004. I think GMT is used,
  1090. not the local time though.
  1091. >>> assert time_read_date1( '2010') == calendar.timegm( ( 2010, 1, 1, 0, 0, 0, 0, 0, 0))
  1092. >>> assert time_read_date1( '2010-1') == calendar.timegm( ( 2010, 1, 1, 0, 0, 0, 0, 0, 0))
  1093. >>> assert time_read_date1( '2015-4-25-14-39-39') == calendar.timegm( time.strptime( 'Sat Apr 25 14:39:39 2015'))
  1094. '''
  1095. pieces = text.split( '-')
  1096. if len( pieces) == 1:
  1097. pieces.append( '1') # mon
  1098. if len( pieces) == 2:
  1099. pieces.append( '1') # mday
  1100. if len( pieces) == 3:
  1101. pieces.append( '0') # hour
  1102. if len( pieces) == 4:
  1103. pieces.append( '0') # minute
  1104. if len( pieces) == 5:
  1105. pieces.append( '0') # second
  1106. pieces = pieces[:6] + [ 0, 0, 0]
  1107. time_tup = tuple( map( int, pieces))
  1108. t = calendar.timegm( time_tup)
  1109. return t
  1110. def time_read_date2( text):
  1111. '''
  1112. Parses strings like '2y4d8h34m5s', returning seconds.
  1113. Supported time periods are:
  1114. s: seconds
  1115. m: minutes
  1116. h: hours
  1117. d: days
  1118. w: weeks
  1119. y: years
  1120. '''
  1121. #print 'text=%r' % text
  1122. text0 = ''
  1123. t = 0
  1124. i0 = 0
  1125. for i in range( len( text)):
  1126. if text[i] in 'ywdhms':
  1127. dt = int( text[i0:i])
  1128. i0=i+1
  1129. if text[i]=='s': dt *= 1
  1130. elif text[i]=='m': dt *= 60
  1131. elif text[i]=='h': dt *= 60*60
  1132. elif text[i]=='d': dt *= 60*60*24
  1133. elif text[i]=='w': dt *= 60*60*24*7
  1134. elif text[i]=='y': dt *= 60*60*24*365
  1135. t += dt
  1136. return t
  1137. def time_read_date3( t, origin=None):
  1138. '''
  1139. Reads a date/time specification and returns absolute time in seconds.
  1140. If <text> starts with '+' or '-', reads relative time with read_date2() and
  1141. adds/subtracts from <origin> (or time.time() if None).
  1142. Otherwise parses date/time with read_date1().
  1143. '''
  1144. if t[0] in '+-':
  1145. if origin is None:
  1146. origin = time.time()
  1147. dt = time_read_date2( t[1:])
  1148. if t[0] == '+':
  1149. return origin + dt
  1150. else:
  1151. return origin - dt
  1152. return time_read_date1( t)
  1153. def stream_prefix_time( stream):
  1154. '''
  1155. Returns `StreamPrefix` that prefixes lines with time and elapsed time.
  1156. '''
  1157. t_start = time.time()
  1158. def prefix_time():
  1159. return '%s (+%s): ' % (
  1160. time.strftime( '%T'),
  1161. time_duration( time.time() - t_start, s_format='0.1f'),
  1162. )
  1163. return StreamPrefix( stream, prefix_time)
  1164. def stdout_prefix_time():
  1165. '''
  1166. Changes `sys.stdout` to prefix time and elapsed time; returns original
  1167. `sys.stdout`.
  1168. '''
  1169. ret = sys.stdout
  1170. sys.stdout = stream_prefix_time( sys.stdout)
  1171. return ret
  1172. def make_out_callable( out):
  1173. '''
  1174. Returns a stream-like object with a `.write()` method that writes to `out`.
  1175. out:
  1176. * Where output is sent.
  1177. * If `None`, output is lost.
  1178. * Otherwise if an integer, we do: `os.write( out, text)`
  1179. * Otherwise if callable, we do: `out( text)`
  1180. * Otherwise we assume `out` is python stream or similar, and do: `out.write(text)`
  1181. '''
  1182. class Ret:
  1183. def write( self, text):
  1184. pass
  1185. def flush( self):
  1186. pass
  1187. ret = Ret()
  1188. if out == log:
  1189. # A hack to avoid expanding '{...}' in text, if caller
  1190. # does: jlib.system(..., out=jlib.log, ...).
  1191. out = lambda text: log(text, nv=False)
  1192. if out is None:
  1193. ret.write = lambda text: None
  1194. elif isinstance( out, int):
  1195. ret.write = lambda text: os.write( out, text)
  1196. elif callable( out):
  1197. ret.write = out
  1198. else:
  1199. ret.write = lambda text: out.write( text)
  1200. return ret
  1201. def _env_extra_text( env_extra):
  1202. ret = ''
  1203. if env_extra:
  1204. for n, v in env_extra.items():
  1205. assert isinstance( n, str), f'env_extra has non-string name {n!r}: {env_extra!r}'
  1206. assert isinstance( v, str), f'env_extra name={n!r} has non-string value {v!r}: {env_extra!r}'
  1207. ret += f'{n}={shlex.quote(v)} '
  1208. return ret
  1209. def command_env_text( command, env_extra):
  1210. '''
  1211. Returns shell command that would run `command` with environmental settings
  1212. in `env_extra`.
  1213. Useful for diagnostics - the returned text can be pasted into terminal to
  1214. re-run a command manually.
  1215. `command` is expected to be already shell escaped, we do not escape it with
  1216. `shlex.quote()`.
  1217. '''
  1218. prefix = _env_extra_text( env_extra)
  1219. return f'{prefix}{command}'
  1220. def system(
  1221. command,
  1222. verbose=True,
  1223. raise_errors=True,
  1224. out=sys.stdout,
  1225. prefix=None,
  1226. shell=True,
  1227. encoding='utf8',
  1228. errors='replace',
  1229. executable=None,
  1230. caller=1,
  1231. bufsize=-1,
  1232. env_extra=None,
  1233. multiline=True,
  1234. ):
  1235. '''
  1236. Runs a command like `os.system()` or `subprocess.*`, but with more
  1237. flexibility.
  1238. We give control over where the command's output is sent, whether to return
  1239. the output and/or exit code, and whether to raise an exception if the
  1240. command fails.
  1241. Args:
  1242. command:
  1243. The command to run.
  1244. verbose:
  1245. If true, we write information about the command that was run, and
  1246. its result, to `jlib.log()`.
  1247. raise_errors:
  1248. If true, we raise an exception if the command fails, otherwise we
  1249. return the failing error code or zero.
  1250. out:
  1251. Where to send output from child process.
  1252. `out` is `o` or `(o, prefix)` or list of such items. Each `o` is
  1253. matched as follows:
  1254. `None`: child process inherits this process's stdout and
  1255. stderr. (Must be the only item, and `prefix` is not supported.)
  1256. `subprocess.DEVNULL`: child process's output is lost. (Must be
  1257. the only item, and `prefix` is not supported.)
  1258. 'return': we store the output and include it in our return
  1259. value or exception. Can only be specified once.
  1260. 'log': we write to `jlib.log()` using our caller's stack
  1261. frame. Can only be specified once.
  1262. An integer: we do: `os.write(o, text)`
  1263. Is callable: we do: `o(text)`
  1264. Otherwise we assume `o` is python stream or similar, and do:
  1265. `o.write(text)`
  1266. If `prefix` is specified, it is applied to each line in the output
  1267. before being sent to `o`.
  1268. prefix:
  1269. Default prefix for all items in `out`. Can be a string, a callable
  1270. taking no args that returns a string, or an integer designating the
  1271. number of spaces.
  1272. shell:
  1273. Passed to underlying `subprocess.Popen()` call.
  1274. encoding:
  1275. Specify the encoding used to translate the command's output to
  1276. characters. If `None` we send bytes to items in `out`.
  1277. errors:
  1278. How to handle encoding errors; see docs for `codecs` module
  1279. for details. Defaults to 'replace' so we never raise a
  1280. `UnicodeDecodeError`.
  1281. executable=None:
  1282. .
  1283. caller:
  1284. The number of frames to look up stack when call `jlib.log()` (used
  1285. for `out='log'` and `verbose`).
  1286. bufsize:
  1287. As `subprocess.Popen()`'s `bufsize` arg, sets buffer size
  1288. when creating stdout, stderr and stdin pipes. Use 0 for
  1289. unbuffered, e.g. to see login/password prompts that don't end
  1290. with a newline. Default -1 means `io.DEFAULT_BUFFER_SIZE`. +1
  1291. (line-buffered) does not work because we read raw bytes and decode
  1292. ourselves into string.
  1293. env_extra:
  1294. If not `None`, a `dict` with extra items that are added to the
  1295. environment passed to the child process.
  1296. multiline:
  1297. If true (the default) we convert a multiline command into a single
  1298. command, but preserve the multiline representation in verbose
  1299. diagnostics.
  1300. Returns:
  1301. * If raise_errors is true:
  1302. If the command failed, we raise an exception; if `out` contains
  1303. 'return' the exception text includes the output.
  1304. Else if `out` contains 'return' we return the text output from the
  1305. command.
  1306. Else we return `None`.
  1307. * If raise_errors is false:
  1308. If `out` contains 'return', we return `(e, text)` where `e` is the
  1309. command's exit code and `text` is the output from the command.
  1310. Else we return `e`, the command's return code.
  1311. In the above, `e` is the `subprocess`-style returncode - the exit
  1312. code, or `-N` if killed by signal `N`.
  1313. >>> print(system('echo hello a', prefix='foo:', out='return'))
  1314. foo:hello a
  1315. foo:
  1316. >>> system('echo hello b', prefix='foo:', out='return', raise_errors=False)
  1317. (0, 'foo:hello b\\nfoo:')
  1318. >>> system('echo hello c && false', prefix='foo:', out='return', env_extra=dict(FOO='bar qwerty'))
  1319. Traceback (most recent call last):
  1320. Exception: Command failed: FOO='bar qwerty' echo hello c && false
  1321. Output was:
  1322. foo:hello c
  1323. foo:
  1324. <BLANKLINE>
  1325. '''
  1326. out_pipe = 0
  1327. out_none = 0
  1328. out_devnull = 0
  1329. out_return = None
  1330. out_log = 0
  1331. outs = out if isinstance(out, list) else [out]
  1332. decoders = dict()
  1333. def decoders_ensure(encoding):
  1334. d = decoders.get(encoding)
  1335. if d is None:
  1336. class D:
  1337. pass
  1338. d = D()
  1339. # subprocess's universal_newlines and codec.streamreader seem to
  1340. # always use buffering even with bufsize=0, so they don't reliably
  1341. # display prompts or other text that doesn't end with a newline.
  1342. #
  1343. # So we create our own incremental decode, which seems to work
  1344. # better.
  1345. #
  1346. d.decoder = codecs.getincrementaldecoder(encoding)(errors)
  1347. d.out = ''
  1348. decoders[ encoding] = d
  1349. return d
  1350. for i, o in enumerate(outs):
  1351. if o is None:
  1352. out_none += 1
  1353. elif o == subprocess.DEVNULL:
  1354. out_devnull += 1
  1355. else:
  1356. out_pipe += 1
  1357. o_prefix = prefix
  1358. if isinstance(o, tuple) and len(o) == 2:
  1359. o, o_prefix = o
  1360. assert o not in (None, subprocess.DEVNULL), f'out[]={o} does not make sense with a prefix ({o_prefix})'
  1361. assert not isinstance(o, (tuple, list))
  1362. o_decoder = None
  1363. if o == 'return':
  1364. assert not out_return, f'"return" specified twice does not make sense'
  1365. out_return = io.StringIO()
  1366. o_fn = out_return.write
  1367. elif o == 'log':
  1368. assert not out_log, f'"log" specified twice does not make sense'
  1369. out_log += 1
  1370. out_frame_record = inspect.stack()[caller]
  1371. o_fn = lambda text: log( text, caller=out_frame_record, nv=False, raw=True)
  1372. elif isinstance(o, int):
  1373. def fn(text, o=o):
  1374. os.write(o, text.encode())
  1375. o_fn = fn
  1376. elif callable(o):
  1377. o_fn = o
  1378. else:
  1379. assert hasattr(o, 'write') and callable(o.write), (
  1380. f'Do not understand o={o}, must be one of:'
  1381. ' None, subprocess.DEVNULL, "return", "log", <int>,'
  1382. ' or support o() or o.write().'
  1383. )
  1384. o_decoder = decoders_ensure(o.encoding)
  1385. def o_fn(text, o=o):
  1386. if errors == 'strict':
  1387. o.write(text)
  1388. else:
  1389. # This is probably only necessary on Windows, where
  1390. # sys.stdout can be cp1252 and will sometimes raise
  1391. # UnicodeEncodeError. We hard-ignore these errors.
  1392. try:
  1393. o.write(text)
  1394. except Exception as e:
  1395. o.write(f'\n[Ignoring Exception: {e}]\n')
  1396. o.flush() # Seems to be necessary on Windows.
  1397. if o_prefix:
  1398. o_fn = StreamPrefix( o_fn, o_prefix).write
  1399. if not o_decoder:
  1400. o_decoder = decoders_ensure(encoding)
  1401. outs[i] = o_fn, o_decoder
  1402. if out_pipe:
  1403. stdout = subprocess.PIPE
  1404. stderr = subprocess.STDOUT
  1405. elif out_none == len(outs):
  1406. stdout = None
  1407. stderr = None
  1408. elif out_devnull == len(outs):
  1409. stdout = subprocess.DEVNULL
  1410. stderr = subprocess.DEVNULL
  1411. else:
  1412. assert 0, f'Inconsistent out: {out}'
  1413. if multiline and '\n' in command:
  1414. command = textwrap.dedent(command)
  1415. lines = list()
  1416. for line in command.split( '\n'):
  1417. h = 0 if line.startswith( '#') else line.find(' #')
  1418. if h >= 0:
  1419. line = line[:h]
  1420. if line.strip():
  1421. line = line.rstrip()
  1422. lines.append(line)
  1423. sep = ' ' if platform.system() == 'Windows' else ' \\\n'
  1424. command = sep.join(lines)
  1425. if verbose:
  1426. log(f'running: {command_env_text( command, env_extra)}', nv=0, caller=caller+1)
  1427. env = None
  1428. if env_extra:
  1429. env = os.environ.copy()
  1430. env.update(env_extra)
  1431. child = subprocess.Popen(
  1432. command,
  1433. shell=shell,
  1434. stdin=None,
  1435. stdout=stdout,
  1436. stderr=stderr,
  1437. close_fds=True,
  1438. executable=executable,
  1439. bufsize=bufsize,
  1440. env=env
  1441. )
  1442. if out_pipe:
  1443. while 1:
  1444. # os.read() seems to be better for us than child.stdout.read()
  1445. # because it returns a short read if data is not available. Where
  1446. # as child.stdout.read() appears to be more willing to wait for
  1447. # data until the requested number of bytes have been received.
  1448. #
  1449. # Also, os.read() does the right thing if the sender has made
  1450. # multiple calls to write() - it returns all available data, not
  1451. # just from the first unread write() call.
  1452. #
  1453. output0 = os.read( child.stdout.fileno(), 10000)
  1454. final = not output0
  1455. for _, decoder in decoders.items():
  1456. decoder.out = decoder.decoder.decode(output0, final)
  1457. for o_fn, o_decoder in outs:
  1458. o_fn( o_decoder.out)
  1459. if not output0:
  1460. break
  1461. e = child.wait()
  1462. if out_log:
  1463. global _log_text_line_start
  1464. if not _log_text_line_start:
  1465. # Terminate last incomplete line of log outputs.
  1466. sys.stdout.write('\n')
  1467. _log_text_line_start = True
  1468. if verbose:
  1469. log(f'[returned e={e}]', nv=0, caller=caller+1)
  1470. if out_return:
  1471. out_return = out_return.getvalue()
  1472. if raise_errors:
  1473. if e:
  1474. message = f'Command failed: {command_env_text( command, env_extra)}'
  1475. if out_return is not None:
  1476. if not out_return.endswith('\n'):
  1477. out_return += '\n'
  1478. raise Exception(
  1479. message + '\n'
  1480. + 'Output was:\n'
  1481. + out_return
  1482. )
  1483. else:
  1484. raise Exception( message)
  1485. elif out_return is not None:
  1486. return out_return
  1487. else:
  1488. return
  1489. if out_return is not None:
  1490. return e, out_return
  1491. else:
  1492. return e
  1493. def system_rusage(
  1494. command,
  1495. verbose=None,
  1496. raise_errors=True,
  1497. out=sys.stdout,
  1498. prefix=None,
  1499. rusage=False,
  1500. shell=True,
  1501. encoding='utf8',
  1502. errors='replace',
  1503. executable=None,
  1504. caller=1,
  1505. bufsize=-1,
  1506. env_extra=None,
  1507. ):
  1508. '''
  1509. Old code that gets timing info; probably doesn't work.
  1510. '''
  1511. command2 = ''
  1512. command2 += '/usr/bin/time -o ubt-out -f "D=%D E=%D F=%F I=%I K=%K M=%M O=%O P=%P R=%r S=%S U=%U W=%W X=%X Z=%Z c=%c e=%e k=%k p=%p r=%r s=%s t=%t w=%w x=%x C=%C"'
  1513. command2 += ' '
  1514. command2 += command
  1515. e = system(
  1516. command2,
  1517. out,
  1518. shell,
  1519. encoding,
  1520. errors,
  1521. executable=executable,
  1522. )
  1523. if e:
  1524. raise Exception('/usr/bin/time failed')
  1525. with open('ubt-out') as f:
  1526. rusage_text = f.read()
  1527. #print 'have read rusage output: %r' % rusage_text
  1528. if rusage_text.startswith( 'Command '):
  1529. # Annoyingly, /usr/bin/time appears to write 'Command
  1530. # exited with ...' or 'Command terminated by ...' to the
  1531. # output file before the rusage info if command doesn't
  1532. # exit 0.
  1533. nl = rusage_text.find('\n')
  1534. rusage_text = rusage_text[ nl+1:]
  1535. return rusage_text
  1536. def git_get_files( directory, submodules=False, relative=True):
  1537. '''
  1538. Returns list of all files known to git in `directory`; `directory` must be
  1539. somewhere within a git checkout.
  1540. Returned names are all relative to `directory`.
  1541. If `<directory>.git` exists we use git-ls-files and write list of files to
  1542. `<directory>/jtest-git-files`.
  1543. Otherwise we require that `<directory>/jtest-git-files` already exists.
  1544. '''
  1545. def is_within_git_checkout( d):
  1546. while 1:
  1547. #log( '{d=}')
  1548. if not d or d=='/':
  1549. break
  1550. if os.path.isdir( f'{d}/.git'):
  1551. return True
  1552. d = os.path.dirname( d)
  1553. ret = []
  1554. if is_within_git_checkout( directory):
  1555. command = 'cd ' + directory + ' && git ls-files'
  1556. if submodules:
  1557. command += ' --recurse-submodules'
  1558. command += ' > jtest-git-files'
  1559. system( command, verbose=False)
  1560. with open( '%s/jtest-git-files' % directory, 'r') as f:
  1561. text = f.read()
  1562. for p in text.strip().split( '\n'):
  1563. if not relative:
  1564. p = os.path.join( directory, p)
  1565. ret.append( p)
  1566. return ret
  1567. def git_get_id_raw( directory):
  1568. if not os.path.isdir( '%s/.git' % directory):
  1569. return
  1570. text = system(
  1571. f'cd {directory} && (PAGER= git show --pretty=oneline|head -n 1 && git diff)',
  1572. out='return',
  1573. )
  1574. return text
  1575. def git_get_id( directory, allow_none=False):
  1576. '''
  1577. Returns text where first line is '<git-sha> <commit summary>' and remaining
  1578. lines contain output from 'git diff' in <directory>.
  1579. directory:
  1580. Root of git checkout.
  1581. allow_none:
  1582. If true, we return None if `directory` is not a git checkout and
  1583. jtest-git-id file does not exist.
  1584. '''
  1585. filename = f'{directory}/jtest-git-id'
  1586. text = git_get_id_raw( directory)
  1587. if text:
  1588. with open( filename, 'w') as f:
  1589. f.write( text)
  1590. elif os.path.isfile( filename):
  1591. with open( filename) as f:
  1592. text = f.read()
  1593. else:
  1594. if not allow_none:
  1595. raise Exception( f'Not in git checkout, and no file called: {filename}.')
  1596. text = None
  1597. return text
  1598. class Args:
  1599. '''
  1600. Iterates over argv items.
  1601. '''
  1602. def __init__( self, argv):
  1603. self.items = iter( argv)
  1604. def next( self):
  1605. if sys.version_info[0] == 3:
  1606. return next( self.items)
  1607. else:
  1608. return self.items.next()
  1609. def next_or_none( self):
  1610. try:
  1611. return self.next()
  1612. except StopIteration:
  1613. return None
  1614. def fs_read( path, binary=False):
  1615. with open( path, 'rb' if binary else 'r') as f:
  1616. return f.read()
  1617. def fs_write( path, data, binary=False):
  1618. with open( path, 'wb' if binary else 'w') as f:
  1619. return f.write( data)
  1620. def fs_update( text, filename, return_different=False):
  1621. '''
  1622. Writes `text` to `filename`. Does nothing if contents of `filename` are
  1623. already `text`.
  1624. If `return_different` is true, we return existing contents if `filename`
  1625. already exists and differs from `text`.
  1626. Otherwise we return true if file has changed.
  1627. '''
  1628. try:
  1629. with open( filename) as f:
  1630. text0 = f.read()
  1631. except OSError:
  1632. text0 = None
  1633. if text != text0:
  1634. if return_different and text0 is not None:
  1635. return text
  1636. # Write to temp file and rename, to ensure we are atomic.
  1637. filename_temp = f'{filename}-jlib-temp'
  1638. with open( filename_temp, 'w') as f:
  1639. f.write( text)
  1640. fs_rename( filename_temp, filename)
  1641. return True
  1642. def fs_find_in_paths( name, paths=None, verbose=False):
  1643. '''
  1644. Looks for `name` in paths and returns complete path. `paths` is list/tuple
  1645. or `os.pathsep`-separated string; if `None` we use `$PATH`. If `name`
  1646. contains `/`, we return `name` itself if it is a file, regardless of $PATH.
  1647. '''
  1648. if '/' in name:
  1649. return name if os.path.isfile( name) else None
  1650. if paths is None:
  1651. paths = os.environ.get( 'PATH', '')
  1652. if verbose:
  1653. log('From os.environ["PATH"]: {paths=}')
  1654. if isinstance( paths, str):
  1655. paths = paths.split( os.pathsep)
  1656. if verbose:
  1657. log('After split: {paths=}')
  1658. for path in paths:
  1659. p = os.path.join( path, name)
  1660. if verbose:
  1661. log('Checking {p=}')
  1662. if os.path.isfile( p):
  1663. if verbose:
  1664. log('Returning because is file: {p!r}')
  1665. return p
  1666. if verbose:
  1667. log('Returning None because not found: {name!r}')
  1668. def fs_mtime( filename, default=0):
  1669. '''
  1670. Returns mtime of file, or `default` if error - e.g. doesn't exist.
  1671. '''
  1672. try:
  1673. return os.path.getmtime( filename)
  1674. except OSError:
  1675. return default
  1676. def fs_filesize( filename, default=0):
  1677. try:
  1678. return os.path.getsize( filename)
  1679. except OSError:
  1680. return default
  1681. def fs_paths( paths):
  1682. '''
  1683. Yields each file in `paths`, walking any directories.
  1684. If `paths` is a tuple `(paths2, filter_)` and `filter_` is callable, we
  1685. yield all files in `paths2` for which `filter_(path2)` returns true.
  1686. '''
  1687. filter_ = lambda path: True
  1688. if isinstance( paths, tuple) and len( paths) == 2 and callable( paths[1]):
  1689. paths, filter_ = paths
  1690. if isinstance( paths, str):
  1691. paths = (paths,)
  1692. for name in paths:
  1693. if os.path.isdir( name):
  1694. for dirpath, dirnames, filenames in os.walk( name):
  1695. for filename in filenames:
  1696. path = os.path.join( dirpath, filename)
  1697. if filter_( path):
  1698. yield path
  1699. else:
  1700. if filter_( name):
  1701. yield name
  1702. def fs_remove( path, backup=False):
  1703. '''
  1704. Removes file or directory, without raising exception if it doesn't exist.
  1705. path:
  1706. The path to remove.
  1707. backup:
  1708. If true, we rename any existing file/directory called `path` to
  1709. `<path>-<datetime>`.
  1710. We assert-fail if the path still exists when we return, in case of
  1711. permission problems etc.
  1712. '''
  1713. if backup and os.path.exists( path):
  1714. datetime = date_time()
  1715. if platform.system() == 'Windows' or platform.system().startswith( 'CYGWIN'):
  1716. # os.rename() fails if destination contains colons, with:
  1717. # [WinError87] The parameter is incorrect ...
  1718. datetime = datetime.replace( ':', '')
  1719. p = f'{path}-{datetime}'
  1720. log( 'Moving out of way: {path} => {p}')
  1721. os.rename( path, p)
  1722. try:
  1723. os.remove( path)
  1724. except Exception:
  1725. pass
  1726. shutil.rmtree( path, ignore_errors=1)
  1727. assert not os.path.exists( path)
  1728. def fs_remove_dir_contents( path):
  1729. '''
  1730. Removes all items in directory `path`; does not remove `path` itself.
  1731. '''
  1732. for leaf in os.listdir( path):
  1733. path2 = os.path.join( path, leaf)
  1734. fs_remove(path2)
  1735. def fs_ensure_empty_dir( path):
  1736. os.makedirs( path, exist_ok=True)
  1737. fs_remove_dir_contents( path)
  1738. def fs_rename(src, dest):
  1739. '''
  1740. Renames `src` to `dest`. If we get an error, we try to remove `dest`
  1741. explicitly and then retry; this is to make things work on Windows.
  1742. '''
  1743. try:
  1744. os.rename(src, dest)
  1745. except Exception:
  1746. os.remove(dest)
  1747. os.rename(src, dest)
  1748. def fs_copy(src, dest, verbose=False):
  1749. '''
  1750. Wrapper for `shutil.copy()` that also ensures parent of `dest` exists and
  1751. optionally calls `jlib.log()` with diagnostic.
  1752. '''
  1753. if verbose:
  1754. log('Copying {src} to {dest}')
  1755. dirname = os.path.dirname(dest)
  1756. if dirname:
  1757. os.makedirs( dirname, exist_ok=True)
  1758. shutil.copy2( src, dest)
  1759. def untar(path, mode='r:gz', prefix=None):
  1760. '''
  1761. Extracts tar file.
  1762. We fail if items in tar file have different top-level directory names, or
  1763. if tar file's top-level directory name already exists locally.
  1764. path:
  1765. The tar file.
  1766. mode:
  1767. As `tarfile.open()`.
  1768. prefix:
  1769. If not `None`, we fail if tar file's top-level directory name is not
  1770. `prefix`.
  1771. Returns the directory name (which will be `prefix` if not `None`).
  1772. '''
  1773. with tarfile.open( path, mode) as t:
  1774. items = t.getnames()
  1775. assert items
  1776. item = items[0]
  1777. assert not item.startswith('.')
  1778. s = item.find('/')
  1779. if s == -1:
  1780. prefix_actual = item + '/'
  1781. else:
  1782. prefix_actual = item[:s+1]
  1783. if prefix:
  1784. assert prefix == prefix_actual, f'prefix={prefix} prefix_actual={prefix_actual}'
  1785. for item in items[1:]:
  1786. assert item.startswith( prefix_actual), f'prefix_actual={prefix_actual!r} != item={item!r}'
  1787. assert not os.path.exists( prefix_actual)
  1788. t.extractall()
  1789. return prefix_actual
  1790. # Things for figuring out whether files need updating, using mtimes.
  1791. #
  1792. def fs_newest( names):
  1793. '''
  1794. Returns mtime of newest file in `filenames`. Returns 0 if no file exists.
  1795. '''
  1796. assert isinstance( names, (list, tuple))
  1797. assert names
  1798. ret_t = 0
  1799. ret_name = None
  1800. for filename in fs_paths( names):
  1801. if filename.endswith('.pyc'):
  1802. continue
  1803. t = fs_mtime( filename)
  1804. if t > ret_t:
  1805. ret_t = t
  1806. ret_name = filename
  1807. return ret_t, ret_name
  1808. def fs_oldest( names):
  1809. '''
  1810. Returns mtime of oldest file in `filenames` or 0 if no file exists.
  1811. '''
  1812. assert isinstance( names, (list, tuple))
  1813. assert names
  1814. ret_t = None
  1815. ret_name = None
  1816. for filename in fs_paths( names):
  1817. t = fs_mtime( filename)
  1818. if ret_t is None or t < ret_t:
  1819. ret_t = t
  1820. ret_name = filename
  1821. if ret_t is None:
  1822. ret_t = 0
  1823. return ret_t, ret_name
  1824. def fs_any_newer( infiles, outfiles):
  1825. '''
  1826. If any file in `infiles` is newer than any file in `outfiles`, returns
  1827. string description. Otherwise returns `None`.
  1828. '''
  1829. in_tmax, in_tmax_name = fs_newest( infiles)
  1830. out_tmin, out_tmin_name = fs_oldest( outfiles)
  1831. if in_tmax > out_tmin:
  1832. text = f'{in_tmax_name} is newer than {out_tmin_name}'
  1833. return text
  1834. def fs_ensure_parent_dir( path):
  1835. parent = os.path.dirname( path)
  1836. if parent:
  1837. os.makedirs( parent, exist_ok=True)
  1838. def fs_newer( pattern, t):
  1839. '''
  1840. Returns list of files matching glob `pattern` whose mtime is >= `t`.
  1841. '''
  1842. paths = glob.glob(pattern)
  1843. paths_new = []
  1844. for path in paths:
  1845. tt = fs_mtime(path)
  1846. if tt >= t:
  1847. paths_new.append(path)
  1848. return paths_new
  1849. def build(
  1850. infiles,
  1851. outfiles,
  1852. command,
  1853. force_rebuild=False,
  1854. out=None,
  1855. all_reasons=False,
  1856. verbose=True,
  1857. executable=None,
  1858. ):
  1859. '''
  1860. Ensures that `outfiles` are up to date using enhanced makefile-like
  1861. determinism of dependencies.
  1862. Rebuilds `outfiles` by running `command` if we determine that any of them
  1863. are out of date, or if `command` has changed.
  1864. infiles:
  1865. Names of files that are read by `command`. Can be a single filename. If
  1866. an item is a directory, we expand to all filenames in the directory's
  1867. tree. Can be `(files2, filter_)` as supported by `jlib.fs_paths()`.
  1868. outfiles:
  1869. Names of files that are written by `command`. Can also be a single
  1870. filename. Can be `(files2, filter_)` as supported by `jlib.fs_paths()`.
  1871. command:
  1872. Command to run. {IN} and {OUT} are replaced by space-separated
  1873. `infiles` and `outfiles` with '/' changed to '\' on Windows.
  1874. force_rebuild:
  1875. If true, we always re-run the command.
  1876. out:
  1877. A callable, passed to `jlib.system()`. If `None`, we use `jlib.log()`
  1878. with our caller's stack record (by passing `(out='log', caller=2)` to
  1879. `jlib.system()`).
  1880. all_reasons:
  1881. If true we check all ways for a build being needed, even if we already
  1882. know a build is needed; this only affects the diagnostic that we
  1883. output.
  1884. verbose:
  1885. Passed to `jlib.system()`.
  1886. Returns:
  1887. true if we have run the command, otherwise None.
  1888. We compare mtimes of `infiles` and `outfiles`, and we also detect changes
  1889. to the command itself.
  1890. If any of infiles are newer than any of `outfiles`, or `command` is
  1891. different to contents of commandfile `<outfile[0]>.cmd`, then truncates
  1892. commandfile and runs `command`. If `command` succeeds we writes `command`
  1893. to commandfile.
  1894. '''
  1895. if isinstance( infiles, str):
  1896. infiles = (infiles,)
  1897. if isinstance( outfiles, str):
  1898. outfiles = (outfiles,)
  1899. if out is None:
  1900. out = 'log'
  1901. command_filename = f'{outfiles[0]}.cmd'
  1902. reasons = []
  1903. if not reasons or all_reasons:
  1904. if force_rebuild:
  1905. reasons.append( 'force_rebuild was specified')
  1906. os_name = platform.system()
  1907. os_windows = (os_name == 'Windows' or os_name.startswith('CYGWIN'))
  1908. def files_string(files):
  1909. if isinstance(files, tuple) and len(files) == 2 and callable(files[1]):
  1910. files = files[0],
  1911. ret = ' '.join(files)
  1912. if os_windows:
  1913. # This works on Cygwyn; we might only need '\\' if running in a Cmd
  1914. # window.
  1915. ret = ret.replace('/', '\\\\')
  1916. return ret
  1917. command = command.replace('{IN}', files_string(infiles))
  1918. command = command.replace('{OUT}', files_string(outfiles))
  1919. if not reasons or all_reasons:
  1920. try:
  1921. with open( command_filename) as f:
  1922. command0 = f.read()
  1923. except Exception:
  1924. command0 = None
  1925. if command != command0:
  1926. reasons.append( f'command has changed:\n{command0}\n=>\n{command}')
  1927. if not reasons or all_reasons:
  1928. reason = fs_any_newer( infiles, outfiles)
  1929. if reason:
  1930. reasons.append( reason)
  1931. if not reasons:
  1932. log( 'Already up to date: ' + ' '.join(outfiles), caller=2, nv=0)
  1933. return
  1934. log( f'Rebuilding because {", and ".join(reasons)}: {" ".join(outfiles)}',
  1935. caller=2,
  1936. nv=0,
  1937. )
  1938. # Empty <command_filename) while we run the command so that if command
  1939. # fails but still creates target(s), then next time we will know target(s)
  1940. # are not up to date.
  1941. #
  1942. # We rename the command to a temporary file and then rename back again
  1943. # after the command finishes so that its mtime is unchanged if the command
  1944. # has not changed.
  1945. #
  1946. fs_ensure_parent_dir( command_filename)
  1947. command_filename_temp = command_filename + '-'
  1948. fs_remove(command_filename_temp)
  1949. if os.path.exists( command_filename):
  1950. fs_rename(command_filename, command_filename_temp)
  1951. fs_update( command, command_filename_temp)
  1952. assert os.path.isfile( command_filename_temp)
  1953. system( command, out=out, verbose=verbose, executable=executable, caller=2)
  1954. assert os.path.isfile( command_filename_temp), \
  1955. f'Command seems to have deleted {command_filename_temp=}: {command!r}'
  1956. fs_rename( command_filename_temp, command_filename)
  1957. return True
  1958. def link_l_flags( sos, ld_origin=None):
  1959. '''
  1960. Returns link flags suitable for linking with each .so in <sos>.
  1961. We return -L flags for each unique parent directory and -l flags for each
  1962. leafname.
  1963. In addition on non-Windows we append " -Wl,-rpath,'$ORIGIN,-z,origin"
  1964. so that libraries will be searched for next to each other. This can be
  1965. disabled by setting ld_origin to false.
  1966. '''
  1967. darwin = (platform.system() == 'Darwin')
  1968. dirs = set()
  1969. names = []
  1970. if isinstance( sos, str):
  1971. sos = [sos]
  1972. ret = ''
  1973. for so in sos:
  1974. if not so:
  1975. continue
  1976. dir_ = os.path.dirname( so)
  1977. name = os.path.basename( so)
  1978. assert name.startswith( 'lib'), f'name={name}'
  1979. m = re.search( '(.so[.0-9]*)$', name)
  1980. if m:
  1981. l = len(m.group(1))
  1982. dirs.add( dir_)
  1983. names.append( f'-l {name[3:-l]}')
  1984. elif darwin and name.endswith( '.dylib'):
  1985. dirs.add( dir_)
  1986. names.append( f'-l {name[3:-6]}')
  1987. elif name.endswith( '.a'):
  1988. names.append( so)
  1989. else:
  1990. assert 0, f'leaf does not end in .so or .a: {so}'
  1991. ret = ''
  1992. # Important to use sorted() here, otherwise ordering from set() is
  1993. # arbitrary causing occasional spurious rebuilds.
  1994. for dir_ in sorted(dirs):
  1995. ret += f' -L {os.path.relpath(dir_)}'
  1996. for name in names:
  1997. ret += f' {name}'
  1998. if ld_origin is None:
  1999. if platform.system() != 'Windows':
  2000. ld_origin = True
  2001. if ld_origin:
  2002. if darwin:
  2003. # As well as this link flag, it is also necessary to use
  2004. # `install_name_tool -change` to rename internal names to
  2005. # `@rpath/<leafname>`.
  2006. ret += ' -Wl,-rpath,@loader_path/.'
  2007. else:
  2008. ret += " -Wl,-rpath,'$ORIGIN',-z,origin"
  2009. #log('{sos=} {ld_origin=} {ret=}')
  2010. return ret.strip()