Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 5ae9be6

Browse files
authoredDec 25, 2020
bpo-36876: [c-analyzer tool] Additional CLI updates for "capi" command. (pythongh-23929)
https://bugs.python.org/issue36876
1 parent c1ae21c commit 5ae9be6

File tree

3 files changed

+380
-93
lines changed

3 files changed

+380
-93
lines changed
 

‎Tools/c-analyzer/cpython/__main__.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -241,15 +241,15 @@ def process_kinds(args, *, argv=None):
241241
for raw in args.kinds or ():
242242
for kind in raw.replace(',', ' ').strip().split():
243243
if kind in _capi.KINDS:
244-
kind.append(kind)
244+
kinds.append(kind)
245245
else:
246246
parser.error(f'expected KIND to be one of {sorted(_capi.KINDS)}, got {kind!r}')
247247
args.kinds = set(kinds)
248248

249249
parser.add_argument('--group-by', dest='groupby',
250250
choices=['level', 'kind'])
251251

252-
parser.add_argument('--format', default='brief')
252+
parser.add_argument('--format', default='table')
253253
parser.add_argument('--summary', dest='format',
254254
action='store_const', const='summary')
255255
def process_format(args, *, argv=None):
@@ -259,12 +259,27 @@ def process_format(args, *, argv=None):
259259
if args.format not in _capi._FORMATS:
260260
parser.error(f'unsupported format {orig!r}')
261261

262+
parser.add_argument('--show-empty', dest='showempty', action='store_true')
263+
parser.add_argument('--no-show-empty', dest='showempty', action='store_false')
264+
parser.set_defaults(showempty=None)
265+
266+
# XXX Add --sort-by, --sort and --no-sort.
267+
268+
parser.add_argument('--ignore', dest='ignored', action='append')
269+
def process_ignored(args, *, argv=None):
270+
ignored = []
271+
for raw in args.ignored or ():
272+
ignored.extend(raw.replace(',', ' ').strip().split())
273+
args.ignored = ignored or None
274+
262275
parser.add_argument('filenames', nargs='*', metavar='FILENAME')
263276
process_progress = add_progress_cli(parser)
264277

265278
return [
266279
process_levels,
280+
process_kinds,
267281
process_format,
282+
process_ignored,
268283
process_progress,
269284
]
270285

@@ -273,7 +288,9 @@ def cmd_capi(filenames=None, *,
273288
levels=None,
274289
kinds=None,
275290
groupby='kind',
276-
format='brief',
291+
format='table',
292+
showempty=None,
293+
ignored=None,
277294
track_progress=None,
278295
verbosity=VERBOSITY,
279296
**kwargs
@@ -282,15 +299,24 @@ def cmd_capi(filenames=None, *,
282299

283300
filenames = _files.iter_header_files(filenames, levels=levels)
284301
#filenames = (file for file, _ in main_for_filenames(filenames))
285-
if track_progress is not None:
302+
if track_progress:
286303
filenames = track_progress(filenames)
287304
items = _capi.iter_capi(filenames)
288305
if levels:
289306
items = (item for item in items if item.level in levels)
290307
if kinds:
291308
items = (item for item in items if item.kind in kinds)
292309

293-
lines = render(items, groupby=groupby, verbose=verbosity > VERBOSITY)
310+
filter = _capi.resolve_filter(ignored)
311+
if filter:
312+
items = (item for item in items if filter(item, log=lambda msg: logger.log(1, msg)))
313+
314+
lines = render(
315+
items,
316+
groupby=groupby,
317+
showempty=showempty,
318+
verbose=verbosity > VERBOSITY,
319+
)
294320
print()
295321
for line in lines:
296322
print(line)

‎Tools/c-analyzer/cpython/_capi.py

Lines changed: 274 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections import namedtuple
2+
import logging
23
import os
34
import os.path
45
import re
@@ -10,6 +11,9 @@
1011
from . import REPO_ROOT
1112

1213

14+
logger = logging.getLogger(__name__)
15+
16+
1317
INCLUDE_ROOT = os.path.join(REPO_ROOT, 'Include')
1418
INCLUDE_CPYTHON = os.path.join(INCLUDE_ROOT, 'cpython')
1519
INCLUDE_INTERNAL = os.path.join(INCLUDE_ROOT, 'internal')
@@ -122,31 +126,34 @@ def _parse_line(line, prev=None):
122126
results = zip(KINDS, m.groups())
123127
for kind, name in results:
124128
if name:
125-
clean = last.split('//')[0].strip()
129+
clean = last.split('//')[0].rstrip()
126130
if clean.endswith('*/'):
127131
clean = clean.split('/*')[0].rstrip()
132+
128133
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
131136
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"
140147
# It was a plain #define.
141148
return None
142149

143150

144-
LEVELS = {
151+
LEVELS = [
145152
'stable',
146153
'cpython',
147154
'private',
148155
'internal',
149-
}
156+
]
150157

151158
def _get_level(filename, name, *,
152159
_cpython=INCLUDE_CPYTHON + os.path.sep,
@@ -165,6 +172,12 @@ def _get_level(filename, name, *,
165172
#return '???'
166173

167174

175+
GROUPINGS = {
176+
'kind': KINDS,
177+
'level': LEVELS,
178+
}
179+
180+
168181
class CAPIItem(namedtuple('CAPIItem', 'file lno name kind level')):
169182

170183
@classmethod
@@ -231,34 +244,70 @@ def _parse_groupby(raw):
231244
else:
232245
raise NotImplementedError
233246

234-
if not all(v in ('kind', 'level') for v in groupby):
247+
if not all(v in GROUPINGS for v in groupby):
235248
raise ValueError(f'invalid groupby value {raw!r}')
236249
return groupby
237250

238251

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')
241273

242274
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+
}
255287

256-
for outer in outers:
257-
summary[outer] = _outer = {}
258-
for inner in inners:
259-
_outer[inner] = 0
260288
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]
262311

263312
return summary
264313

@@ -289,48 +338,128 @@ def iter_capi(filenames=None):
289338
yield item
290339

291340

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):
293388
groupby = _parse_groupby(groupby)[0]
294389
maxfilename = maxname = maxkind = maxlevel = 0
390+
295391
collated = {}
392+
groups = GROUPINGS[groupby]
393+
for group in groups:
394+
collated[group] = []
395+
296396
for item in items:
297397
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)
302399
maxfilename = max(len(item.relfile), maxfilename)
303400
maxname = max(len(item.name), maxname)
304401
maxkind = max(len(item.kind), maxkind)
305402
maxlevel = max(len(item.level), maxlevel)
403+
if not includeempty:
404+
for group in groups:
405+
if not collated[group]:
406+
del collated[group]
306407
maxextra = {
307408
'kind': maxkind,
308409
'level': maxlevel,
309410
}
310411
return collated, groupby, maxfilename, maxname, maxextra
311412

312413

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+
313440
##################################
314441
# CLI rendering
315442

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+
},
328457
}
329458

330459

331460
def resolve_format(format):
332461
if not format:
333-
return 'brief'
462+
return 'table'
334463
elif isinstance(format, str) and format in _FORMATS:
335464
return format
336465
else:
@@ -350,19 +479,29 @@ def render(items, **kwargs):
350479
return render
351480

352481

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+
354494
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]
366505
else:
367506
# XXX Support no grouping?
368507
raise NotImplementedError
@@ -373,8 +512,6 @@ def get_extra(item):
373512
for extra in ('kind', 'level')}
374513
else:
375514
if verbose:
376-
maxextra['kind'] = max(len(kind) for kind in KINDS)
377-
maxextra['level'] = max(len(level) for level in LEVELS)
378515
extracols = [f'{extra}:{maxextra[extra]}'
379516
for extra in extras]
380517
def get_extra(item):
@@ -404,43 +541,66 @@ def get_extra(item):
404541
]
405542
header, div, fmt = build_table(columns)
406543

544+
if sort:
545+
sortkey = _get_sortkey(sort, groupby, columns)
546+
407547
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:
410550
continue
411551
yield ''
412552
yield f' === {group} ==='
413553
yield ''
414554
yield header
415555
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+
)
422565
yield div
423-
subtotal = len(collated[group])
566+
subtotal = len(grouped)
424567
yield f' sub-total: {subtotal}'
425568
total += subtotal
426569
yield ''
427570
yield f'total: {total}'
428571

429572

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+
431587
if groupby:
432-
collated, groupby, _, _, _ = _collate(items, groupby)
588+
collated, groupby, _, _, _ = _collate(items, groupby, showempty)
433589
for group, grouped in collated.items():
434590
yield '#' * 25
435591
yield f'# {group} ({len(grouped)})'
436592
yield '#' * 25
437593
yield ''
438594
if not grouped:
439595
continue
596+
if sort:
597+
grouped = sorted(grouped, key=sortkey)
440598
for item in grouped:
441599
yield from _render_item_full(item, groupby, verbose)
442600
yield ''
443601
else:
602+
if sort:
603+
items = sorted(items, key=sortkey)
444604
for item in items:
445605
yield from _render_item_full(item, None, verbose)
446606
yield ''
@@ -459,21 +619,47 @@ def _render_item_full(item, groupby, verbose):
459619
print(' ---------------------------------------')
460620

461621

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}'
473659

474660

475661
_FORMATS = {
476-
'brief': render_table,
662+
'table': render_table,
477663
'full': render_full,
478664
'summary': render_summary,
479665
}

‎Tools/c-analyzer/must-resolve.sh

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
3+
# Any PyObject exposed via the public API is problematic since it must
4+
# be made per-interpreter. This involves the following:
5+
#
6+
# singletons:
7+
# - None
8+
# - True
9+
# - False
10+
# - NotImplemented
11+
# - Ellipsis
12+
# PyTypeObject:
13+
# - PyExc* [97]
14+
# - static types [81]
15+
#
16+
# In the non-stable API we could use #defines to do the conversion
17+
# transparently (though Py_None is perhaps problematic for performance
18+
# reasons). However, we can't take that approach with the stable API.
19+
# That means we must find all functions (& macros) in the stable API
20+
# (and probably the full public API, for sanity sake) and adjust them.
21+
# This will involve internally converting from the public object to the
22+
# corresponding per-interpreter object.
23+
#
24+
# Note that the only place this solution fails is with direct pointer
25+
# equality checks with the public objects.
26+
27+
# XXX What about saying that the stable API is not sub-interpreter
28+
# compatible?
29+
30+
31+
function run_capi() {
32+
./python Tools/c-analyzer/c-analyzer.py capi \
33+
--no-progress \
34+
--group-by kind \
35+
--func --inline --macro \
36+
--no-show-empty \
37+
--ignore '<must-resolve.ignored>' \
38+
$@
39+
}
40+
41+
echo ''
42+
echo '#################################################'
43+
echo '# All API'
44+
echo '#################################################'
45+
run_capi --format summary Include/*.h Include/cpython/*.h
46+
run_capi --format table Include/*.h Include/cpython/*.h
47+
echo ''
48+
echo ''
49+
echo '#################################################'
50+
echo '# stable API'
51+
echo '#################################################'
52+
echo ''
53+
echo '# public:'
54+
run_capi --format summary --public --no-show-empty Include/*.h
55+
echo ''
56+
echo '# private:'
57+
run_capi --format summary --private --no-show-empty Include/*.h
58+
echo ''
59+
run_capi --format full -v Include/*.h
60+
#run_capi --format full -v --public Include/*.h
61+
#run_capi --format full -v --private Include/*.h
62+
echo ''
63+
echo '#################################################'
64+
echo '# cpython API'
65+
echo '#################################################'
66+
echo ''
67+
echo '# public:'
68+
run_capi --format summary --public --no-show-empty Include/cpython/*.h
69+
echo ''
70+
echo '# private:'
71+
run_capi --format summary --private --no-show-empty Include/cpython/*.h
72+
echo ''
73+
run_capi --format full -v Include/cpython/*.h
74+
#run_capi --format full -v --public Include/cpython/*.h
75+
#run_capi --format full -v --private Include/cpython/*.h

0 commit comments

Comments
 (0)
Please sign in to comment.