codeanalyzer-python 1.0.0__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/PKG-INFO +21 -21
  2. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/README.md +20 -20
  3. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/__main__.py +35 -0
  4. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/core.py +22 -0
  5. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/project.py +29 -14
  6. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +67 -6
  7. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +36 -17
  8. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/docs/handoff/README.md +2 -2
  9. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/pyproject.toml +1 -1
  10. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/.gitignore +0 -0
  11. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/LICENSE +0 -0
  12. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/NOTICE +0 -0
  13. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/__init__.py +0 -0
  14. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/config/__init__.py +0 -0
  15. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/config/config.py +0 -0
  16. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/__init__.py +0 -0
  17. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/access_paths.py +0 -0
  18. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/alias.py +0 -0
  19. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/builder.py +0 -0
  20. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/cfg.py +0 -0
  21. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/defuse.py +0 -0
  22. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/dominance.py +0 -0
  23. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/identity.py +0 -0
  24. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/pdg.py +0 -0
  25. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
  26. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/scc.py +0 -0
  27. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/sdg.py +0 -0
  28. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/slicing.py +0 -0
  29. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/summaries.py +0 -0
  30. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/dataflow/syntactic.py +0 -0
  31. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/jedi/__init__.py +0 -0
  32. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/jedi/jedi.py +0 -0
  33. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/__init__.py +0 -0
  34. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/bolt.py +0 -0
  35. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/cypher.py +0 -0
  36. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/emit.py +0 -0
  37. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/rows.py +0 -0
  38. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/neo4j/schema.py +0 -0
  39. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/options/__init__.py +0 -0
  40. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/options/options.py +0 -0
  41. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/provenance.py +0 -0
  42. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/py.typed +0 -0
  43. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/__init__.py +0 -0
  44. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/assign_ids.py +0 -0
  45. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/call_graph_ids.py +0 -0
  46. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/ids.py +0 -0
  47. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/l1_body.py +0 -0
  48. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/l2_callees.py +0 -0
  49. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/schema/py_schema.py +0 -0
  50. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/__init__.py +0 -0
  51. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
  52. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/pycg/__init__.py +0 -0
  53. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py +0 -0
  54. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/semantic_analysis/pycg/shard_planner.py +0 -0
  55. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
  56. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
  57. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
  58. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/utils/__init__.py +0 -0
  59. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/utils/logging.py +0 -0
  60. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/codeanalyzer/utils/progress_bar.py +0 -0
  61. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/docs/handoff/schema.neo4j.json +0 -0
  62. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/schema-uml.drawio +0 -0
  63. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/schema.neo4j.json +0 -0
  64. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/flask/README.md +0 -0
  65. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
  66. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
  67. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
  68. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/requests/LICENSE +0 -0
  69. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/requests/NOTICE +0 -0
  70. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/requests/README.md +0 -0
  71. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
  72. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
  73. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
  74. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/README.md +0 -0
  75. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
  76. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
  77. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
  78. {codeanalyzer_python-1.0.0 → codeanalyzer_python-1.0.2}/test/fixtures/whole_applications/xarray/xarray/datatree_/docs/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeanalyzer-python
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Static analysis for Python — canonical schema v2 (symbol table, call graph, and native CFG/PDG/SDG dataflow) as analysis.json or a Neo4j property graph.
5
5
  Author-email: Rahul Krishna <i.m.ralk@gmail.com>
6
6
  License-File: LICENSE
@@ -203,19 +203,19 @@ $ canpy --help
203
203
  │ --version Show the canpy │
204
204
  │ version and │
205
205
  │ exit. │
206
- │ --input -i PATH Path to the │
206
+ │ --input -i <path> Path to the │
207
207
  │ project root │
208
208
  │ directory (not │
209
209
  │ required for │
210
210
  │ --emit schema). │
211
- │ --output -o PATH Output directory │
211
+ │ --output -o <path> Output directory │
212
212
  │ for artifacts. │
213
- │ --format -f [json|msgpack] Output format │
213
+ │ --format -f <json|msgpack> Output format │
214
214
  │ for --emit json: │
215
215
  │ json or msgpack. │
216
216
  │ [default: json] │
217
- │ --emit [json|neo4j|sche Output target: │
218
- │ ma] json │
217
+ │ --emit <json|neo4j|sche Output target: │
218
+ │ ma> json │
219
219
  │ (analysis.json, │
220
220
  │ default) | neo4j │
221
221
  │ (graph.cypher or │
@@ -225,13 +225,13 @@ $ canpy --help
225
225
  │ schema.json │
226
226
  │ contract). │
227
227
  │ [default: json] │
228
- │ --app-name TEXT Logical │
228
+ │ --app-name <str> Logical │
229
229
  │ application name │
230
230
  │ for the graph │
231
231
  │ :PyApplication │
232
232
  │ anchor (default: │
233
233
  │ input dir name). │
234
- │ --neo4j-uri TEXT Push the graph │
234
+ │ --neo4j-uri <str> Push the graph │
235
235
  │ to a live Neo4j │
236
236
  │ over Bolt │
237
237
  │ (incremental); │
@@ -239,11 +239,11 @@ $ canpy --help
239
239
  │ graph.cypher. │
240
240
  │ [env var: │
241
241
  │ NEO4J_URI] │
242
- │ --neo4j-user TEXT Neo4j username. │
242
+ │ --neo4j-user <str> Neo4j username. │
243
243
  │ [env var: │
244
244
  │ NEO4J_USERNAME] │
245
245
  │ [default: neo4j] │
246
- │ --neo4j-password TEXT Neo4j password. │
246
+ │ --neo4j-password <str> Neo4j password. │
247
247
  │ Prefer the env │
248
248
  │ var over the │
249
249
  │ flag (the flag │
@@ -253,12 +253,12 @@ $ canpy --help
253
253
  │ [env var: │
254
254
  │ NEO4J_PASSWORD] │
255
255
  │ [default: neo4j] │
256
- │ --neo4j-database TEXT Neo4j database │
256
+ │ --neo4j-database <str> Neo4j database │
257
257
  │ name (default: │
258
258
  │ server default). │
259
259
  │ [env var: │
260
260
  │ NEO4J_DATABASE] │
261
- │ --analysis-level -a INTEGER RANGE Analysis depth: │
261
+ │ --analysis-level -a <int range> Analysis depth: │
262
262
  │ [1<=x<=4] 1=symbol │
263
263
  │ table+Jedi call │
264
264
  │ graph, 2=+PyCG │
@@ -274,7 +274,7 @@ $ canpy --help
274
274
  │ alias-aware │
275
275
  │ DDG). │
276
276
  │ [default: 1] │
277
- │ --graphs TEXT Level 3+ only: │
277
+ │ --graphs <str> Level 3+ only: │
278
278
  │ comma-separated │
279
279
  │ program-graph │
280
280
  │ sections to emit │
@@ -287,7 +287,7 @@ $ canpy --help
287
287
  │ requires -a 4. │
288
288
  │ [default: │
289
289
  │ cfg,dfg,pdg] │
290
- │ --graph-field-de… INTEGER RANGE Level 3 only: │
290
+ │ --graph-field-de… <int range> Level 3 only: │
291
291
  │ [x>=1] k-limit on │
292
292
  │ access-path │
293
293
  │ depth (x.f.g.h │
@@ -324,12 +324,12 @@ $ canpy --help
324
324
  │ environment │
325
325
  │ instead. │
326
326
  │ [default: venv] │
327
- │ --file-name PATH Analyze only the │
327
+ │ --file-name <path> Analyze only the │
328
328
  │ specified file │
329
329
  │ (relative to │
330
330
  │ input │
331
331
  │ directory). │
332
- │ --cache-dir -c PATH Directory to │
332
+ │ --cache-dir -c <path> Directory to │
333
333
  │ store analysis │
334
334
  │ cache. Defaults │
335
335
  │ to │
@@ -343,7 +343,7 @@ $ canpy --help
343
343
  │ retained. │
344
344
  │ [default: │
345
345
  │ keep-cache] │
346
- │ -v INTEGER Increase │
346
+ │ -v <int> Increase │
347
347
  │ verbosity: -v, │
348
348
  │ -vv, -vvv │
349
349
  │ [default: 0] │
@@ -370,7 +370,7 @@ $ canpy --help
370
370
  │ Jedi-only edges. │
371
371
  │ [default: │
372
372
  │ no-pycg-shard] │
373
- │ --pycg-shard-cei… INTEGER RANGE Maximum files │
373
+ │ --pycg-shard-cei… <int range> Maximum files │
374
374
  │ [x>=1] per shard when │
375
375
  │ --pycg-shard is │
376
376
  │ active (default │
@@ -392,7 +392,7 @@ $ canpy --help
392
392
  │ heavy import │
393
393
  │ graphs. │
394
394
  │ [default: 100] │
395
- │ --pycg-shard-tim… INTEGER RANGE Per-shard │
395
+ │ --pycg-shard-tim… <int range> Per-shard │
396
396
  │ [x>=0] wall-clock │
397
397
  │ timeout in │
398
398
  │ seconds when │
@@ -420,7 +420,7 @@ $ canpy --help
420
420
  │ ignored on │
421
421
  │ Windows. │
422
422
  │ [default: 120] │
423
- │ --pycg-shard-str… [jedi|package] How --pycg-shard │
423
+ │ --pycg-shard-str… <jedi|package> How --pycg-shard │
424
424
  │ groups files │
425
425
  │ (level 2 only). │
426
426
  │ 'jedi' (default) │
@@ -442,7 +442,7 @@ $ canpy --help
442
442
  │ one-shard-per-p… │
443
443
  │ grouping. │
444
444
  │ [default: jedi] │
445
- │ --pycg-max-iter INTEGER RANGE Cap on PyCG's │
445
+ │ --pycg-max-iter <int range> Cap on PyCG's │
446
446
  │ [x>=-1] fixpoint passes │
447
447
  │ per │
448
448
  │ shard/project │
@@ -164,19 +164,19 @@ $ canpy --help
164
164
  │ --version Show the canpy │
165
165
  │ version and │
166
166
  │ exit. │
167
- │ --input -i PATH Path to the │
167
+ │ --input -i <path> Path to the │
168
168
  │ project root │
169
169
  │ directory (not │
170
170
  │ required for │
171
171
  │ --emit schema). │
172
- │ --output -o PATH Output directory │
172
+ │ --output -o <path> Output directory │
173
173
  │ for artifacts. │
174
- │ --format -f [json|msgpack] Output format │
174
+ │ --format -f <json|msgpack> Output format │
175
175
  │ for --emit json: │
176
176
  │ json or msgpack. │
177
177
  │ [default: json] │
178
- │ --emit [json|neo4j|sche Output target: │
179
- │ ma] json │
178
+ │ --emit <json|neo4j|sche Output target: │
179
+ │ ma> json │
180
180
  │ (analysis.json, │
181
181
  │ default) | neo4j │
182
182
  │ (graph.cypher or │
@@ -186,13 +186,13 @@ $ canpy --help
186
186
  │ schema.json │
187
187
  │ contract). │
188
188
  │ [default: json] │
189
- │ --app-name TEXT Logical │
189
+ │ --app-name <str> Logical │
190
190
  │ application name │
191
191
  │ for the graph │
192
192
  │ :PyApplication │
193
193
  │ anchor (default: │
194
194
  │ input dir name). │
195
- │ --neo4j-uri TEXT Push the graph │
195
+ │ --neo4j-uri <str> Push the graph │
196
196
  │ to a live Neo4j │
197
197
  │ over Bolt │
198
198
  │ (incremental); │
@@ -200,11 +200,11 @@ $ canpy --help
200
200
  │ graph.cypher. │
201
201
  │ [env var: │
202
202
  │ NEO4J_URI] │
203
- │ --neo4j-user TEXT Neo4j username. │
203
+ │ --neo4j-user <str> Neo4j username. │
204
204
  │ [env var: │
205
205
  │ NEO4J_USERNAME] │
206
206
  │ [default: neo4j] │
207
- │ --neo4j-password TEXT Neo4j password. │
207
+ │ --neo4j-password <str> Neo4j password. │
208
208
  │ Prefer the env │
209
209
  │ var over the │
210
210
  │ flag (the flag │
@@ -214,12 +214,12 @@ $ canpy --help
214
214
  │ [env var: │
215
215
  │ NEO4J_PASSWORD] │
216
216
  │ [default: neo4j] │
217
- │ --neo4j-database TEXT Neo4j database │
217
+ │ --neo4j-database <str> Neo4j database │
218
218
  │ name (default: │
219
219
  │ server default). │
220
220
  │ [env var: │
221
221
  │ NEO4J_DATABASE] │
222
- │ --analysis-level -a INTEGER RANGE Analysis depth: │
222
+ │ --analysis-level -a <int range> Analysis depth: │
223
223
  │ [1<=x<=4] 1=symbol │
224
224
  │ table+Jedi call │
225
225
  │ graph, 2=+PyCG │
@@ -235,7 +235,7 @@ $ canpy --help
235
235
  │ alias-aware │
236
236
  │ DDG). │
237
237
  │ [default: 1] │
238
- │ --graphs TEXT Level 3+ only: │
238
+ │ --graphs <str> Level 3+ only: │
239
239
  │ comma-separated │
240
240
  │ program-graph │
241
241
  │ sections to emit │
@@ -248,7 +248,7 @@ $ canpy --help
248
248
  │ requires -a 4. │
249
249
  │ [default: │
250
250
  │ cfg,dfg,pdg] │
251
- │ --graph-field-de… INTEGER RANGE Level 3 only: │
251
+ │ --graph-field-de… <int range> Level 3 only: │
252
252
  │ [x>=1] k-limit on │
253
253
  │ access-path │
254
254
  │ depth (x.f.g.h │
@@ -285,12 +285,12 @@ $ canpy --help
285
285
  │ environment │
286
286
  │ instead. │
287
287
  │ [default: venv] │
288
- │ --file-name PATH Analyze only the │
288
+ │ --file-name <path> Analyze only the │
289
289
  │ specified file │
290
290
  │ (relative to │
291
291
  │ input │
292
292
  │ directory). │
293
- │ --cache-dir -c PATH Directory to │
293
+ │ --cache-dir -c <path> Directory to │
294
294
  │ store analysis │
295
295
  │ cache. Defaults │
296
296
  │ to │
@@ -304,7 +304,7 @@ $ canpy --help
304
304
  │ retained. │
305
305
  │ [default: │
306
306
  │ keep-cache] │
307
- │ -v INTEGER Increase │
307
+ │ -v <int> Increase │
308
308
  │ verbosity: -v, │
309
309
  │ -vv, -vvv │
310
310
  │ [default: 0] │
@@ -331,7 +331,7 @@ $ canpy --help
331
331
  │ Jedi-only edges. │
332
332
  │ [default: │
333
333
  │ no-pycg-shard] │
334
- │ --pycg-shard-cei… INTEGER RANGE Maximum files │
334
+ │ --pycg-shard-cei… <int range> Maximum files │
335
335
  │ [x>=1] per shard when │
336
336
  │ --pycg-shard is │
337
337
  │ active (default │
@@ -353,7 +353,7 @@ $ canpy --help
353
353
  │ heavy import │
354
354
  │ graphs. │
355
355
  │ [default: 100] │
356
- │ --pycg-shard-tim… INTEGER RANGE Per-shard │
356
+ │ --pycg-shard-tim… <int range> Per-shard │
357
357
  │ [x>=0] wall-clock │
358
358
  │ timeout in │
359
359
  │ seconds when │
@@ -381,7 +381,7 @@ $ canpy --help
381
381
  │ ignored on │
382
382
  │ Windows. │
383
383
  │ [default: 120] │
384
- │ --pycg-shard-str… [jedi|package] How --pycg-shard │
384
+ │ --pycg-shard-str… <jedi|package> How --pycg-shard │
385
385
  │ groups files │
386
386
  │ (level 2 only). │
387
387
  │ 'jedi' (default) │
@@ -403,7 +403,7 @@ $ canpy --help
403
403
  │ one-shard-per-p… │
404
404
  │ grouping. │
405
405
  │ [default: jedi] │
406
- │ --pycg-max-iter INTEGER RANGE Cap on PyCG's │
406
+ │ --pycg-max-iter <int range> Cap on PyCG's │
407
407
  │ [x>=-1] fixpoint passes │
408
408
  │ per │
409
409
  │ shard/project │
@@ -1,9 +1,40 @@
1
+ import os
2
+ import sys
1
3
  from importlib.metadata import version as _pkg_version, PackageNotFoundError
2
4
  from pathlib import Path
3
5
  from typing import Optional, Annotated
4
6
 
5
7
  import typer
6
8
 
9
+
10
+ def _pin_hash_seed() -> None:
11
+ """Re-exec once with ``PYTHONHASHSEED=0`` unless the caller pinned one.
12
+
13
+ PyCG's capped fixpoint (``--pycg-max-iter``) iterates hash-ordered sets
14
+ keyed on module/access-path strings, so an unpinned per-interpreter hash
15
+ seed makes the emitted L2+ call graph vary run to run (issue #99). The
16
+ seed cannot be set after interpreter start, hence the exec. Export
17
+ PYTHONHASHSEED (any value) to opt out or pin a different seed.
18
+
19
+ Only fires when this process really is the CLI (canpy / python -m
20
+ codeanalyzer): in-process invocations — e.g. Typer's CliRunner in the
21
+ test suite, or a host app calling the callback — must never have their
22
+ own process exec'd out from under them."""
23
+ if os.environ.get("PYTHONHASHSEED") is not None:
24
+ return
25
+ argv0 = os.path.basename(sys.argv[0]) if sys.argv else ""
26
+ is_cli = argv0 in ("canpy", "codeanalyzer") or sys.argv[0].endswith(
27
+ os.path.join("codeanalyzer", "__main__.py")
28
+ )
29
+ if not is_cli:
30
+ return
31
+ env = dict(os.environ, PYTHONHASHSEED="0")
32
+ os.execvpe(
33
+ sys.executable,
34
+ [sys.executable, "-m", "codeanalyzer", *sys.argv[1:]],
35
+ env,
36
+ )
37
+
7
38
  from codeanalyzer.core import Codeanalyzer
8
39
  from codeanalyzer.utils import _set_log_level, logger
9
40
  from codeanalyzer.config import OutputFormat
@@ -264,6 +295,10 @@ def main(
264
295
  ),
265
296
  ] = 50,
266
297
  ):
298
+ # Determinism: pin the interpreter hash seed before any analysis (no-op
299
+ # when PYTHONHASHSEED is already set; --version exits before this).
300
+ _pin_hash_seed()
301
+
267
302
  # Flag validation (strict: unrecognized values error out, never fall back).
268
303
  selected_graphs = [g.strip() for g in graphs.split(",") if g.strip()]
269
304
  from codeanalyzer.dataflow.builder import VALID_GRAPHS
@@ -37,6 +37,22 @@ from codeanalyzer.utils import ProgressBar
37
37
  from codeanalyzer.options import AnalysisOptions
38
38
  from codeanalyzer.provenance import analyzer_info, repository_info
39
39
 
40
+ def _ensure_ray() -> None:
41
+ """Initialize Ray with the driver's pinned hash seed in the workers.
42
+
43
+ An implicit auto-init would not carry PYTHONHASHSEED into worker
44
+ interpreters, so PyCG shards (and Jedi inference) run there with random
45
+ set-iteration order and the emitted edges vary run to run (issue #99)."""
46
+ if not ray.is_initialized():
47
+ ray.init(
48
+ runtime_env={
49
+ "env_vars": {
50
+ "PYTHONHASHSEED": os.environ.get("PYTHONHASHSEED", "0")
51
+ }
52
+ },
53
+ )
54
+
55
+
40
56
  @ray.remote
41
57
  def _process_file_with_ray(py_file: Union[Path, str], project_dir: Union[Path, str], virtualenv: Union[Path, str, None]) -> Dict[str, PyModule]:
42
58
  """Processes files in the project directory using Ray for distributed processing.
@@ -438,6 +454,11 @@ class Codeanalyzer:
438
454
  call_graph = merge_edges(call_graph, pycg_edges)
439
455
 
440
456
  call_graph = filter_external_edges(call_graph, symbol_table)
457
+ # Canonical edge order: backend iteration order (PyCG dicts, Counter
458
+ # insertion) is not a contract — sort so identical edge SETS always
459
+ # serialize identically (issue #99 determinism gate), and so the
460
+ # external-symbol homing below assigns ids in a stable order.
461
+ call_graph.sort(key=lambda e: (e.src, e.dst))
441
462
 
442
463
  # Recreate pyapplication
443
464
  app = (
@@ -716,6 +737,7 @@ class Codeanalyzer:
716
737
 
717
738
  # Process only new/changed files with Ray
718
739
  if files_to_process:
740
+ _ensure_ray()
719
741
  futures = [_process_file_with_ray.remote(py_file, self.project_dir, str(self.virtualenv) if self.virtualenv else None) for py_file in files_to_process]
720
742
 
721
743
  with ProgressBar(len(futures), "Building symbol table (parallel)") as progress:
@@ -290,9 +290,11 @@ def _project_module_body(
290
290
  externals: dict, sig_to_id: dict, module_id_by_key: dict,
291
291
  ) -> None:
292
292
  for fn in (mod.functions or {}).values():
293
- _project_callable(b, file_key, mod_ref, "PY_DECLARES", fn, externals, sig_to_id)
293
+ _project_callable(b, file_key, mod_ref, "PY_DECLARES", fn, externals, sig_to_id,
294
+ mod.source)
294
295
  for cl in (mod.types or {}).values():
295
- _project_class(b, file_key, mod_ref, "PY_DECLARES", cl, externals, sig_to_id)
296
+ _project_class(b, file_key, mod_ref, "PY_DECLARES", cl, externals, sig_to_id,
297
+ mod.source)
296
298
  for v in mod.variables or []:
297
299
  _project_variable(b, file_key, mod_ref, file_key, v)
298
300
  _project_imports(b, mod_ref, mod, module_id_by_key)
@@ -360,10 +362,10 @@ def _project_imports(b: RowBuilder, mod_ref: NodeRef, mod: PyModule,
360
362
 
361
363
  def _project_class(
362
364
  b: RowBuilder, file_key: str, parent: NodeRef, parent_rel: str, cl: PyClass,
363
- externals: dict, sig_to_id: dict,
365
+ externals: dict, sig_to_id: dict, source: str,
364
366
  ) -> None:
365
367
  ref = b.node(
366
- ["PySymbol", "PyClass"], "id", cl.id, _class_props(cl, file_key)
368
+ ["PySymbol", "PyClass"], "id", cl.id, _class_props(cl, file_key, source)
367
369
  )
368
370
  b.edge(parent_rel, parent, ref)
369
371
 
@@ -372,22 +374,23 @@ def _project_class(
372
374
  b.edge_to_symbol("PY_EXTENDS", ref, _symbol_ref(base, externals, sig_to_id))
373
375
 
374
376
  for m in (cl.callables or {}).values():
375
- _project_callable(b, file_key, ref, "PY_HAS_METHOD", m, externals, sig_to_id)
377
+ _project_callable(b, file_key, ref, "PY_HAS_METHOD", m, externals, sig_to_id,
378
+ source)
376
379
  for a in (cl.attributes or {}).values():
377
380
  _project_attribute(b, file_key, ref, cl.signature, a)
378
381
  for ic in (cl.types or {}).values():
379
- _project_class(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id)
382
+ _project_class(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id, source)
380
383
 
381
384
 
382
385
  def _project_callable(
383
386
  b: RowBuilder, file_key: str, owner: NodeRef, owner_rel: str, c: PyCallable,
384
- externals: dict, sig_to_id: dict,
387
+ externals: dict, sig_to_id: dict, source: str,
385
388
  ) -> None:
386
389
  ref = b.node(
387
390
  ["PySymbol", "PyCallable"],
388
391
  "id",
389
392
  c.id,
390
- _callable_props(c, file_key),
393
+ _callable_props(c, file_key, source),
391
394
  )
392
395
  b.edge(owner_rel, owner, ref)
393
396
 
@@ -410,9 +413,10 @@ def _project_callable(
410
413
  for v in c.local_variables or []:
411
414
  _project_variable(b, file_key, ref, c.signature, v)
412
415
  for ic in (c.callables or {}).values():
413
- _project_callable(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id)
416
+ _project_callable(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id,
417
+ source)
414
418
  for cl in (c.types or {}).values():
415
- _project_class(b, file_key, ref, "PY_DECLARES", cl, externals, sig_to_id)
419
+ _project_class(b, file_key, ref, "PY_DECLARES", cl, externals, sig_to_id, source)
416
420
 
417
421
 
418
422
  def _project_attribute(
@@ -459,13 +463,24 @@ def _module_props(mod: PyModule, file_key: str) -> Props:
459
463
  )
460
464
 
461
465
 
462
- def _class_props(cl: PyClass, file_key: str) -> Props:
466
+ def _span_code(source: str, span) -> str | None:
467
+ """A declaration's text: the owning module's ``source`` sliced by the node's
468
+ utf-8 byte span. Schema v2 stores source once per module, so the graph's
469
+ ``code`` property (declared on :PyClass/:PyCallable and indexed by
470
+ ``py_code_fts``) is derived here at projection time (#104)."""
471
+ if span is None or not source:
472
+ return None
473
+ lo, hi = span.bytes
474
+ return source.encode("utf-8")[lo:hi].decode("utf-8")
475
+
476
+
477
+ def _class_props(cl: PyClass, file_key: str, source: str) -> Props:
463
478
  return prune(
464
479
  {
465
480
  "id": cl.id,
466
481
  "signature": cl.signature,
467
482
  "name": cl.name,
468
- "code": getattr(cl, "code", None),
483
+ "code": _span_code(source, cl.span),
469
484
  "base_classes": list(cl.base_classes or []),
470
485
  "docstring": _docstring_of(cl.comments),
471
486
  "start_line": cl.start_line,
@@ -475,7 +490,7 @@ def _class_props(cl: PyClass, file_key: str) -> Props:
475
490
  )
476
491
 
477
492
 
478
- def _callable_props(c: PyCallable, file_key: str) -> Props:
493
+ def _callable_props(c: PyCallable, file_key: str, source: str) -> Props:
479
494
  return prune(
480
495
  {
481
496
  "id": c.id,
@@ -484,7 +499,7 @@ def _callable_props(c: PyCallable, file_key: str) -> Props:
484
499
  "path": c.path,
485
500
  "return_type": c.return_type,
486
501
  "cyclomatic_complexity": c.cyclomatic_complexity,
487
- "code": getattr(c, "code", None),
502
+ "code": _span_code(source, c.span),
488
503
  "code_start_line": c.code_start_line,
489
504
  "start_line": c.start_line,
490
505
  "end_line": c.end_line,
@@ -39,9 +39,12 @@ project-relative dotted paths so they align with the symbol table.
39
39
  # re-enters PyCG's hook before its import graph is ready. Pre-importing
40
40
  # these modules at import time ensures they're already in sys.modules when
41
41
  # PyCG's hook is active, preventing the re-entrant ImportManagerError.
42
+ import fcntl
43
+ import hashlib
42
44
  import importlib.metadata # noqa: F401
43
45
  import importlib.util # noqa: F401
44
46
  import contextlib
47
+ import os
45
48
  import json # noqa: F401
46
49
  import shutil
47
50
  import signal
@@ -85,6 +88,15 @@ from codeanalyzer.semantic_analysis.pycg.shard_planner import plan_shards
85
88
  from codeanalyzer.utils import ProgressBar, logger
86
89
 
87
90
 
91
+ def _shard_root_path(files: List[str], project_dir: Path) -> Path:
92
+ """Content-derived mini-project root for a shard: same project + same file
93
+ set → same path on every run (determinism, issue #99)."""
94
+ digest = hashlib.sha1(
95
+ "\0".join([str(project_dir), *sorted(files)]).encode("utf-8")
96
+ ).hexdigest()[:16]
97
+ return Path(tempfile.gettempdir()) / f"canpy_pycg_shard_{digest}"
98
+
99
+
88
100
  def _materialize_shard_root(
89
101
  files: List[str],
90
102
  project_dir: Path,
@@ -103,10 +115,21 @@ def _materialize_shard_root(
103
115
 
104
116
  The caller owns the returned *root* and must ``shutil.rmtree`` it.
105
117
  """
106
- root = Path(tempfile.mkdtemp(prefix="canpy_pycg_shard_"))
118
+ # Deterministic root: PyCG's capped fixpoint (--pycg-max-iter) is
119
+ # order-sensitive, and its internal state keys on absolute module paths —
120
+ # a random mkdtemp suffix changes those strings every run and shifts the
121
+ # iteration frontier, making the emitted edge set vary run-to-run
122
+ # (issue #99). Deriving the directory name from the shard's content keeps
123
+ # the path (and thus the analysis input) identical across runs. Callers
124
+ # that may run concurrently on the same shard serialize on the sidecar
125
+ # lock (see _shard_symlink_root).
126
+ root = _shard_root_path(files, project_dir)
127
+ if root.exists():
128
+ shutil.rmtree(root, ignore_errors=True)
129
+ root.mkdir(parents=True, exist_ok=True)
107
130
  entry_points: List[str] = []
108
131
  linked_inits: Set[Path] = set()
109
- for f in files:
132
+ for f in sorted(files):
110
133
  src = Path(f).resolve()
111
134
  try:
112
135
  rel = src.relative_to(project_dir)
@@ -142,12 +165,27 @@ def _shard_symlink_root(
142
165
  """Context-manager wrapper around :func:`_materialize_shard_root`.
143
166
 
144
167
  Yields ``(root, entry_points)`` and removes the temp tree on exit.
168
+
169
+ The root path is content-derived (determinism, issue #99), so two
170
+ concurrent analyses of the same shard — e.g. a test suite and a manual
171
+ run on one project — would collide on it (one rmtree's the tree the
172
+ other is mid-analysis on). An exclusive flock on a sidecar lockfile
173
+ serializes them; distinct projects/shards hash to distinct roots and
174
+ never contend.
145
175
  """
146
- root, entry_points = _materialize_shard_root(files, project_dir)
176
+ digest_root = _shard_root_path(files, project_dir)
177
+ lock_path = digest_root.with_name(digest_root.name + ".lock")
178
+ lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR)
147
179
  try:
148
- yield root, entry_points
180
+ fcntl.flock(lock_fd, fcntl.LOCK_EX)
181
+ root, entry_points = _materialize_shard_root(files, project_dir)
182
+ try:
183
+ yield root, entry_points
184
+ finally:
185
+ shutil.rmtree(root, ignore_errors=True)
149
186
  finally:
150
- shutil.rmtree(root, ignore_errors=True)
187
+ fcntl.flock(lock_fd, fcntl.LOCK_UN)
188
+ os.close(lock_fd)
151
189
 
152
190
 
153
191
  def _pycg_shard_worker(
@@ -469,7 +507,9 @@ class PyCG:
469
507
  ):
470
508
  continue
471
509
  paths.append(str(p))
472
- return paths
510
+ # Sorted for run-to-run stability: rglob yields filesystem order, and
511
+ # PyCG's capped fixpoint is sensitive to entry-point order (issue #99).
512
+ return sorted(paths)
473
513
 
474
514
  # ------------------------------------------------------------------
475
515
  # Package-root helpers for sharding
@@ -712,11 +752,14 @@ class PyCG:
712
752
  """
713
753
  import os
714
754
  import ray
755
+ from codeanalyzer.core import _ensure_ray
756
+ _ensure_ray()
715
757
 
716
758
  os.environ.setdefault("RAY_IGNORE_UNHANDLED_ERRORS", "1")
717
759
  remote_fn = ray.remote(_pycg_shard_worker)
718
760
 
719
761
  roots: List[Path] = []
762
+ lock_fds: List[int] = []
720
763
  futures: List[Any] = []
721
764
  meta: Dict[Any, List[str]] = {} # ObjectRef -> shard file list
722
765
  edges_all: List[PyCallEdge] = []
@@ -724,6 +767,16 @@ class PyCG:
724
767
  try:
725
768
  with ProgressBar(len(shards), "Building call graph shards (parallel)", item_label="shards") as progress:
726
769
  for files in shards:
770
+ # Deterministic roots can collide across concurrent
771
+ # analyses of the same project — the driver holds each
772
+ # shard's sidecar lock for the whole Ray fan-out (released
773
+ # in the finally below with the root cleanup).
774
+ lock_fd = os.open(
775
+ str(_shard_root_path(files, self.project_dir).with_suffix(".lock")),
776
+ os.O_CREAT | os.O_RDWR,
777
+ )
778
+ fcntl.flock(lock_fd, fcntl.LOCK_EX)
779
+ lock_fds.append(lock_fd)
727
780
  root, eps = _materialize_shard_root(files, self.project_dir)
728
781
  roots.append(root)
729
782
  fut = remote_fn.remote(eps, str(root), "", self.max_iter)
@@ -765,6 +818,12 @@ class PyCG:
765
818
  finally:
766
819
  for root in roots:
767
820
  shutil.rmtree(root, ignore_errors=True)
821
+ for fd in lock_fds:
822
+ try:
823
+ fcntl.flock(fd, fcntl.LOCK_UN)
824
+ os.close(fd)
825
+ except OSError:
826
+ pass
768
827
  return edges_all, runaways
769
828
 
770
829
  def _build_sharded(
@@ -870,6 +929,8 @@ class PyCG:
870
929
  """
871
930
  import os
872
931
  import ray
932
+ from codeanalyzer.core import _ensure_ray
933
+ _ensure_ray()
873
934
 
874
935
  # force-cancel kills worker processes; suppress Ray's "worker died
875
936
  # unexpectedly" noise since the death is intentional here.
@@ -57,13 +57,26 @@ class SymbolTableBuilder:
57
57
  relative = Path(script_path).relative_to(self.project_dir)
58
58
  return ".".join(relative.with_suffix("").parts) + f".{name}"
59
59
 
60
+ @staticmethod
61
+ def _first_definition(definitions):
62
+ """Deterministic pick from Jedi's inference candidates.
63
+
64
+ On a union-typed receiver Jedi may return several candidates whose
65
+ ORDER varies run to run (issue #99 — e.g. ``o.seek`` on
66
+ ``_IOBase | BufferedRandom | TextIOWrapper``); taking whichever came
67
+ first made the emitted call graph nondeterministic. Sort on the
68
+ stable identity (full_name, then name) and take the smallest."""
69
+ if not definitions:
70
+ return None
71
+ return min(definitions, key=lambda d: (d.full_name or "", d.name or ""))
72
+
60
73
  @staticmethod
61
74
  def _infer_type(script: Script, line: int, column: int) -> str:
62
75
  """Tries to infer the type at a given position using Jedi."""
63
76
  try:
64
- inference = script.infer(line=line, column=column)
65
- if inference:
66
- return inference[0].name # or .full_name
77
+ d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
78
+ if d is not None:
79
+ return d.name # or .full_name
67
80
  except Exception:
68
81
  pass
69
82
  return None
@@ -82,9 +95,9 @@ class SymbolTableBuilder:
82
95
  Optional[str]: The fully qualified name if available, else None.
83
96
  """
84
97
  try:
85
- definitions = script.infer(line=line, column=column)
86
- if definitions:
87
- return definitions[0].full_name
98
+ d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
99
+ if d is not None:
100
+ return d.full_name
88
101
  except Exception:
89
102
  pass
90
103
  return None
@@ -103,10 +116,9 @@ class SymbolTableBuilder:
103
116
  the call graph.
104
117
  """
105
118
  try:
106
- definitions = script.infer(line=line, column=column)
107
- if not definitions:
119
+ d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
120
+ if d is None:
108
121
  return None, False
109
- d = definitions[0]
110
122
  is_class = (d.type == "class")
111
123
  full = d.full_name
112
124
  if is_class and full:
@@ -144,11 +156,17 @@ class SymbolTableBuilder:
144
156
  as the callee's own name.
145
157
  """
146
158
  try:
147
- definitions = script.infer(line=line, column=column)
148
- if definitions:
149
- results = definitions[0].execute()
150
- if results:
151
- return results[0].name
159
+ d = SymbolTableBuilder._first_definition(script.infer(line=line, column=column))
160
+ if d is not None:
161
+ # Drop NoneType results before picking: Jedi flaps between
162
+ # yielding {NoneType} and {} for the None arm of an Optional
163
+ # return (issue #99), and when a real type is also in the
164
+ # union it is the informative choice — a bare NoneType says
165
+ # nothing a missing return_type doesn't.
166
+ results = [r for r in d.execute() if r.name != "NoneType"]
167
+ r = SymbolTableBuilder._first_definition(results)
168
+ if r is not None:
169
+ return r.name
152
170
  except Exception:
153
171
  pass
154
172
  return None
@@ -974,9 +992,10 @@ class SymbolTableBuilder:
974
992
 
975
993
  if script:
976
994
  try:
977
- definitions = script.infer(line=lineno, column=col_offset)
978
- if definitions:
979
- d = definitions[0]
995
+ d = SymbolTableBuilder._first_definition(
996
+ script.infer(line=lineno, column=col_offset)
997
+ )
998
+ if d is not None:
980
999
  inferred_type = d.name
981
1000
  qname = d.full_name
982
1001
  if d.type == "function":
@@ -13,14 +13,14 @@ the contract lives in the spec, the decision log, and `schema.neo4j.json`.
13
13
  Pin the analyzer that produced these samples:
14
14
 
15
15
  ```
16
- pip install "codeanalyzer-python==1.0.0"
16
+ pip install "codeanalyzer-python==1.0.1"
17
17
  ```
18
18
 
19
19
  For L4 alias analysis (the Scalpel points-to oracle), install the optional extra —
20
20
  without it, L4 degrades to the type-based fallback oracle (still sound, coarser):
21
21
 
22
22
  ```
23
- pip install "codeanalyzer-python[scalpel]==1.0.0"
23
+ pip install "codeanalyzer-python[scalpel]==1.0.1"
24
24
  ```
25
25
 
26
26
  ## Schema contract
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codeanalyzer-python"
3
- version = "1.0.0"
3
+ version = "1.0.2"
4
4
  description = "Static analysis for Python — canonical schema v2 (symbol table, call graph, and native CFG/PDG/SDG dataflow) as analysis.json or a Neo4j property graph."
5
5
  readme = "README.md"
6
6
  authors = [