1
1
from collections import namedtuple
2
+ import logging
2
3
import os
3
4
import os .path
4
5
import re
10
11
from . import REPO_ROOT
11
12
12
13
14
+ logger = logging .getLogger (__name__ )
15
+
16
+
13
17
INCLUDE_ROOT = os .path .join (REPO_ROOT , 'Include' )
14
18
INCLUDE_CPYTHON = os .path .join (INCLUDE_ROOT , 'cpython' )
15
19
INCLUDE_INTERNAL = os .path .join (INCLUDE_ROOT , 'internal' )
@@ -122,31 +126,34 @@ def _parse_line(line, prev=None):
122
126
results = zip (KINDS , m .groups ())
123
127
for kind , name in results :
124
128
if name :
125
- clean = last .split ('//' )[0 ].strip ()
129
+ clean = last .split ('//' )[0 ].rstrip ()
126
130
if clean .endswith ('*/' ):
127
131
clean = clean .split ('/*' )[0 ].rstrip ()
132
+
128
133
if kind == 'macro' or kind == 'constant' :
129
- if clean .endswith ('\\ ' ):
130
- return line # the new "prev"
134
+ if not clean .endswith ('\\ ' ):
135
+ return name , kind
131
136
elif kind == 'inline' :
132
- if not prev :
133
- if not clean .endswith ('}' ):
134
- return line # the new "prev"
135
- elif clean != '}' :
136
- return line # the new "prev"
137
- elif not clean .endswith (';' ):
138
- return line # the new "prev"
139
- return name , kind
137
+ if clean .endswith ('}' ):
138
+ if not prev or clean == '}' :
139
+ return name , kind
140
+ elif kind == 'func' or kind == 'data' :
141
+ if clean .endswith (';' ):
142
+ return name , kind
143
+ else :
144
+ # This should not be reached.
145
+ raise NotImplementedError
146
+ return line # the new "prev"
140
147
# It was a plain #define.
141
148
return None
142
149
143
150
144
- LEVELS = {
151
+ LEVELS = [
145
152
'stable' ,
146
153
'cpython' ,
147
154
'private' ,
148
155
'internal' ,
149
- }
156
+ ]
150
157
151
158
def _get_level (filename , name , * ,
152
159
_cpython = INCLUDE_CPYTHON + os .path .sep ,
@@ -165,6 +172,12 @@ def _get_level(filename, name, *,
165
172
#return '???'
166
173
167
174
175
+ GROUPINGS = {
176
+ 'kind' : KINDS ,
177
+ 'level' : LEVELS ,
178
+ }
179
+
180
+
168
181
class CAPIItem (namedtuple ('CAPIItem' , 'file lno name kind level' )):
169
182
170
183
@classmethod
@@ -231,34 +244,70 @@ def _parse_groupby(raw):
231
244
else :
232
245
raise NotImplementedError
233
246
234
- if not all (v in ( 'kind' , 'level' ) for v in groupby ):
247
+ if not all (v in GROUPINGS for v in groupby ):
235
248
raise ValueError (f'invalid groupby value { raw !r} ' )
236
249
return groupby
237
250
238
251
239
- def summarize (items , * , groupby = 'kind' ):
240
- summary = {}
252
+ def _resolve_full_groupby (groupby ):
253
+ if isinstance (groupby , str ):
254
+ groupby = [groupby ]
255
+ groupings = []
256
+ for grouping in groupby + list (GROUPINGS ):
257
+ if grouping not in groupings :
258
+ groupings .append (grouping )
259
+ return groupings
260
+
261
+
262
+ def summarize (items , * , groupby = 'kind' , includeempty = True , minimize = None ):
263
+ if minimize is None :
264
+ if includeempty is None :
265
+ minimize = True
266
+ includeempty = False
267
+ else :
268
+ minimize = includeempty
269
+ elif includeempty is None :
270
+ includeempty = minimize
271
+ elif minimize and includeempty :
272
+ raise ValueError (f'cannot minimize and includeempty at the same time' )
241
273
242
274
groupby = _parse_groupby (groupby )[0 ]
243
- if groupby == 'kind' :
244
- outers = KINDS
245
- inners = LEVELS
246
- def increment ( item ):
247
- summary [ item . kind ][ item . level ] += 1
248
- elif groupby == 'level' :
249
- outers = LEVELS
250
- inners = KINDS
251
- def increment ( item ):
252
- summary [ item . level ][ item . kind ] += 1
253
- else :
254
- raise NotImplementedError
275
+ _outer , _inner = _resolve_full_groupby ( groupby )
276
+ outers = GROUPINGS [ _outer ]
277
+ inners = GROUPINGS [ _inner ]
278
+
279
+ summary = {
280
+ 'totals' : {
281
+ 'all' : 0 ,
282
+ 'subs' : { o : 0 for o in outers },
283
+ 'bygroup' : { o : { i : 0 for i in inners }
284
+ for o in outers },
285
+ },
286
+ }
255
287
256
- for outer in outers :
257
- summary [outer ] = _outer = {}
258
- for inner in inners :
259
- _outer [inner ] = 0
260
288
for item in items :
261
- increment (item )
289
+ outer = getattr (item , _outer )
290
+ inner = getattr (item , _inner )
291
+ # Update totals.
292
+ summary ['totals' ]['all' ] += 1
293
+ summary ['totals' ]['subs' ][outer ] += 1
294
+ summary ['totals' ]['bygroup' ][outer ][inner ] += 1
295
+
296
+ if not includeempty :
297
+ subtotals = summary ['totals' ]['subs' ]
298
+ bygroup = summary ['totals' ]['bygroup' ]
299
+ for outer in outers :
300
+ if subtotals [outer ] == 0 :
301
+ del subtotals [outer ]
302
+ del bygroup [outer ]
303
+ continue
304
+
305
+ for inner in inners :
306
+ if bygroup [outer ][inner ] == 0 :
307
+ del bygroup [outer ][inner ]
308
+ if minimize :
309
+ if len (bygroup [outer ]) == 1 :
310
+ del bygroup [outer ]
262
311
263
312
return summary
264
313
@@ -289,48 +338,128 @@ def iter_capi(filenames=None):
289
338
yield item
290
339
291
340
292
- def _collate (items , groupby ):
341
+ def resolve_filter (ignored ):
342
+ if not ignored :
343
+ return None
344
+ ignored = set (_resolve_ignored (ignored ))
345
+ def filter (item , * , log = None ):
346
+ if item .name not in ignored :
347
+ return True
348
+ if log is not None :
349
+ log (f'ignored { item .name !r} ' )
350
+ return False
351
+ return filter
352
+
353
+
354
+ def _resolve_ignored (ignored ):
355
+ if isinstance (ignored , str ):
356
+ ignored = [ignored ]
357
+ for raw in ignored :
358
+ if isinstance (raw , str ):
359
+ if raw .startswith ('|' ):
360
+ yield raw [1 :]
361
+ elif raw .startswith ('<' ) and raw .endswith ('>' ):
362
+ filename = raw [1 :- 1 ]
363
+ try :
364
+ infile = open (filename )
365
+ except Exception as exc :
366
+ logger .error (f'ignore file failed: { exc } ' )
367
+ continue
368
+ logger .log (1 , f'reading ignored names from { filename !r} ' )
369
+ with infile :
370
+ for line in infile :
371
+ if not line :
372
+ continue
373
+ if line [0 ].isspace ():
374
+ continue
375
+ line = line .partition ('#' )[0 ].rstrip ()
376
+ if line :
377
+ # XXX Recurse?
378
+ yield line
379
+ else :
380
+ raw = raw .strip ()
381
+ if raw :
382
+ yield raw
383
+ else :
384
+ raise NotImplementedError
385
+
386
+
387
+ def _collate (items , groupby , includeempty ):
293
388
groupby = _parse_groupby (groupby )[0 ]
294
389
maxfilename = maxname = maxkind = maxlevel = 0
390
+
295
391
collated = {}
392
+ groups = GROUPINGS [groupby ]
393
+ for group in groups :
394
+ collated [group ] = []
395
+
296
396
for item in items :
297
397
key = getattr (item , groupby )
298
- if key in collated :
299
- collated [key ].append (item )
300
- else :
301
- collated [key ] = [item ]
398
+ collated [key ].append (item )
302
399
maxfilename = max (len (item .relfile ), maxfilename )
303
400
maxname = max (len (item .name ), maxname )
304
401
maxkind = max (len (item .kind ), maxkind )
305
402
maxlevel = max (len (item .level ), maxlevel )
403
+ if not includeempty :
404
+ for group in groups :
405
+ if not collated [group ]:
406
+ del collated [group ]
306
407
maxextra = {
307
408
'kind' : maxkind ,
308
409
'level' : maxlevel ,
309
410
}
310
411
return collated , groupby , maxfilename , maxname , maxextra
311
412
312
413
414
+ def _get_sortkey (sort , _groupby , _columns ):
415
+ if sort is True or sort is None :
416
+ # For now:
417
+ def sortkey (item ):
418
+ return (
419
+ item .level == 'private' ,
420
+ LEVELS .index (item .level ),
421
+ KINDS .index (item .kind ),
422
+ os .path .dirname (item .file ),
423
+ os .path .basename (item .file ),
424
+ item .name ,
425
+ )
426
+ return sortkey
427
+
428
+ sortfields = 'not-private level kind dirname basename name' .split ()
429
+ elif isinstance (sort , str ):
430
+ sortfields = sort .replace (',' , ' ' ).strip ().split ()
431
+ elif callable (sort ):
432
+ return sort
433
+ else :
434
+ raise NotImplementedError
435
+
436
+ # XXX Build a sortkey func from sortfields.
437
+ raise NotImplementedError
438
+
439
+
313
440
##################################
314
441
# CLI rendering
315
442
316
- _LEVEL_MARKERS = {
317
- 'S' : 'stable' ,
318
- 'C' : 'cpython' ,
319
- 'P' : 'private' ,
320
- 'I' : 'internal' ,
321
- }
322
- _KIND_MARKERS = {
323
- 'F' : 'func' ,
324
- 'D' : 'data' ,
325
- 'I' : 'inline' ,
326
- 'M' : 'macro' ,
327
- 'C' : 'constant' ,
443
+ _MARKERS = {
444
+ 'level' : {
445
+ 'S' : 'stable' ,
446
+ 'C' : 'cpython' ,
447
+ 'P' : 'private' ,
448
+ 'I' : 'internal' ,
449
+ },
450
+ 'kind' : {
451
+ 'F' : 'func' ,
452
+ 'D' : 'data' ,
453
+ 'I' : 'inline' ,
454
+ 'M' : 'macro' ,
455
+ 'C' : 'constant' ,
456
+ },
328
457
}
329
458
330
459
331
460
def resolve_format (format ):
332
461
if not format :
333
- return 'brief '
462
+ return 'table '
334
463
elif isinstance (format , str ) and format in _FORMATS :
335
464
return format
336
465
else :
@@ -350,19 +479,29 @@ def render(items, **kwargs):
350
479
return render
351
480
352
481
353
- def render_table (items , * , columns = None , groupby = 'kind' , verbose = False ):
482
+ def render_table (items , * ,
483
+ columns = None ,
484
+ groupby = 'kind' ,
485
+ sort = True ,
486
+ showempty = False ,
487
+ verbose = False ,
488
+ ):
489
+ if groupby is None :
490
+ groupby = 'kind'
491
+ if showempty is None :
492
+ showempty = False
493
+
354
494
if groupby :
355
- collated , groupby , maxfilename , maxname , maxextra = _collate (items , groupby )
356
- if groupby == 'kind' :
357
- groups = KINDS
358
- extras = ['level' ]
359
- markers = {'level' : _LEVEL_MARKERS }
360
- elif groupby == 'level' :
361
- groups = LEVELS
362
- extras = ['kind' ]
363
- markers = {'kind' : _KIND_MARKERS }
364
- else :
365
- raise NotImplementedError
495
+ (collated , groupby , maxfilename , maxname , maxextra ,
496
+ ) = _collate (items , groupby , showempty )
497
+ for grouping in GROUPINGS :
498
+ maxextra [grouping ] = max (len (g ) for g in GROUPINGS [grouping ])
499
+
500
+ _ , extra = _resolve_full_groupby (groupby )
501
+ extras = [extra ]
502
+ markers = {extra : _MARKERS [extra ]}
503
+
504
+ groups = GROUPINGS [groupby ]
366
505
else :
367
506
# XXX Support no grouping?
368
507
raise NotImplementedError
@@ -373,8 +512,6 @@ def get_extra(item):
373
512
for extra in ('kind' , 'level' )}
374
513
else :
375
514
if verbose :
376
- maxextra ['kind' ] = max (len (kind ) for kind in KINDS )
377
- maxextra ['level' ] = max (len (level ) for level in LEVELS )
378
515
extracols = [f'{ extra } :{ maxextra [extra ]} '
379
516
for extra in extras ]
380
517
def get_extra (item ):
@@ -404,43 +541,66 @@ def get_extra(item):
404
541
]
405
542
header , div , fmt = build_table (columns )
406
543
544
+ if sort :
545
+ sortkey = _get_sortkey (sort , groupby , columns )
546
+
407
547
total = 0
408
- for group in groups :
409
- if group not in collated :
548
+ for group , grouped in collated . items () :
549
+ if not showempty and group not in collated :
410
550
continue
411
551
yield ''
412
552
yield f' === { group } ==='
413
553
yield ''
414
554
yield header
415
555
yield div
416
- for item in collated [group ]:
417
- yield fmt .format (
418
- filename = item .relfile ,
419
- name = item .name ,
420
- ** get_extra (item ),
421
- )
556
+ if grouped :
557
+ if sort :
558
+ grouped = sorted (grouped , key = sortkey )
559
+ for item in grouped :
560
+ yield fmt .format (
561
+ filename = item .relfile ,
562
+ name = item .name ,
563
+ ** get_extra (item ),
564
+ )
422
565
yield div
423
- subtotal = len (collated [ group ] )
566
+ subtotal = len (grouped )
424
567
yield f' sub-total: { subtotal } '
425
568
total += subtotal
426
569
yield ''
427
570
yield f'total: { total } '
428
571
429
572
430
- def render_full (items , * , groupby = None , verbose = False ):
573
+ def render_full (items , * ,
574
+ groupby = 'kind' ,
575
+ sort = None ,
576
+ showempty = None ,
577
+ verbose = False ,
578
+ ):
579
+ if groupby is None :
580
+ groupby = 'kind'
581
+ if showempty is None :
582
+ showempty = False
583
+
584
+ if sort :
585
+ sortkey = _get_sortkey (sort , groupby , None )
586
+
431
587
if groupby :
432
- collated , groupby , _ , _ , _ = _collate (items , groupby )
588
+ collated , groupby , _ , _ , _ = _collate (items , groupby , showempty )
433
589
for group , grouped in collated .items ():
434
590
yield '#' * 25
435
591
yield f'# { group } ({ len (grouped )} )'
436
592
yield '#' * 25
437
593
yield ''
438
594
if not grouped :
439
595
continue
596
+ if sort :
597
+ grouped = sorted (grouped , key = sortkey )
440
598
for item in grouped :
441
599
yield from _render_item_full (item , groupby , verbose )
442
600
yield ''
443
601
else :
602
+ if sort :
603
+ items = sorted (items , key = sortkey )
444
604
for item in items :
445
605
yield from _render_item_full (item , None , verbose )
446
606
yield ''
@@ -459,21 +619,47 @@ def _render_item_full(item, groupby, verbose):
459
619
print (' ---------------------------------------' )
460
620
461
621
462
- def render_summary (items , * , groupby = 'kind' , verbose = False ):
463
- total = 0
464
- summary = summarize (items , groupby = groupby )
465
- # XXX Stablize the sorting to match KINDS/LEVELS.
466
- for outer , counts in summary .items ():
467
- subtotal = sum (c for _ , c in counts .items ())
468
- yield f'{ outer + ":" :20} ({ subtotal } )'
469
- for inner , count in counts .items ():
470
- yield f' { inner + ":" :9} { count } '
471
- total += subtotal
472
- yield f'{ "total:" :20} ({ total } )'
622
+ def render_summary (items , * ,
623
+ groupby = 'kind' ,
624
+ sort = None ,
625
+ showempty = None ,
626
+ verbose = False ,
627
+ ):
628
+ if groupby is None :
629
+ groupby = 'kind'
630
+ summary = summarize (
631
+ items ,
632
+ groupby = groupby ,
633
+ includeempty = showempty ,
634
+ minimize = None if showempty else not verbose ,
635
+ )
636
+
637
+ subtotals = summary ['totals' ]['subs' ]
638
+ bygroup = summary ['totals' ]['bygroup' ]
639
+ lastempty = False
640
+ for outer , subtotal in subtotals .items ():
641
+ if bygroup :
642
+ subtotal = f'({ subtotal } )'
643
+ yield f'{ outer + ":" :20} { subtotal :>8} '
644
+ else :
645
+ yield f'{ outer + ":" :10} { subtotal :>8} '
646
+ if outer in bygroup :
647
+ for inner , count in bygroup [outer ].items ():
648
+ yield f' { inner + ":" :9} { count } '
649
+ lastempty = False
650
+ else :
651
+ lastempty = True
652
+
653
+ total = f'*{ summary ["totals" ]["all" ]} *'
654
+ label = '*total*:'
655
+ if bygroup :
656
+ yield f'{ label :20} { total :>8} '
657
+ else :
658
+ yield f'{ label :10} { total :>9} '
473
659
474
660
475
661
_FORMATS = {
476
- 'brief ' : render_table ,
662
+ 'table ' : render_table ,
477
663
'full' : render_full ,
478
664
'summary' : render_summary ,
479
665
}
0 commit comments