codeanalyzer-python 0.2.1__py3-none-any.whl → 0.3.1__py3-none-any.whl

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 (32) hide show
  1. codeanalyzer/__main__.py +116 -6
  2. codeanalyzer/core.py +139 -213
  3. codeanalyzer/neo4j/__init__.py +1 -1
  4. codeanalyzer/neo4j/emit.py +2 -2
  5. codeanalyzer/neo4j/project.py +84 -18
  6. codeanalyzer/neo4j/schema.py +261 -15
  7. codeanalyzer/options/__init__.py +2 -2
  8. codeanalyzer/options/options.py +20 -1
  9. codeanalyzer/provenance.py +61 -0
  10. codeanalyzer/schema/py_schema.py +39 -1
  11. codeanalyzer/semantic_analysis/call_graph.py +24 -3
  12. codeanalyzer/semantic_analysis/pycg/__init__.py +20 -0
  13. codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +1054 -0
  14. codeanalyzer/semantic_analysis/{codeql/__init__.py → pycg/pycg_exceptions.py} +5 -8
  15. codeanalyzer/semantic_analysis/pycg/shard_planner.py +401 -0
  16. codeanalyzer/syntactic_analysis/import_resolver.py +67 -0
  17. codeanalyzer/syntactic_analysis/symbol_table_builder.py +74 -10
  18. codeanalyzer/utils/logging.py +5 -2
  19. codeanalyzer/utils/progress_bar.py +15 -7
  20. codeanalyzer_python-0.3.1.dist-info/METADATA +553 -0
  21. codeanalyzer_python-0.3.1.dist-info/RECORD +39 -0
  22. {codeanalyzer_python-0.2.1.dist-info → codeanalyzer_python-0.3.1.dist-info}/WHEEL +1 -1
  23. codeanalyzer/neo4j/catalog.py +0 -245
  24. codeanalyzer/semantic_analysis/codeql/codeql_analysis.py +0 -382
  25. codeanalyzer/semantic_analysis/codeql/codeql_exceptions.py +0 -12
  26. codeanalyzer/semantic_analysis/codeql/codeql_loader.py +0 -91
  27. codeanalyzer/semantic_analysis/codeql/codeql_query_runner.py +0 -185
  28. codeanalyzer_python-0.2.1.dist-info/METADATA +0 -415
  29. codeanalyzer_python-0.2.1.dist-info/RECORD +0 -39
  30. {codeanalyzer_python-0.2.1.dist-info → codeanalyzer_python-0.3.1.dist-info}/entry_points.txt +0 -0
  31. {codeanalyzer_python-0.2.1.dist-info → codeanalyzer_python-0.3.1.dist-info}/licenses/LICENSE +0 -0
  32. {codeanalyzer_python-0.2.1.dist-info → codeanalyzer_python-0.3.1.dist-info}/licenses/NOTICE +0 -0
@@ -1,7 +1,6 @@
1
1
  import logging
2
2
  from typing import Optional
3
3
 
4
- from rich.console import Console
5
4
  from rich.progress import (
6
5
  BarColumn,
7
6
  Progress,
@@ -15,14 +14,17 @@ from rich.progress import (
15
14
 
16
15
  class ProgressBar:
17
16
  def __init__(
18
- self, total_files: int, description: str = "Processing files..."
17
+ self,
18
+ total_files: int,
19
+ description: str = "Processing files...",
20
+ item_label: str = "files",
19
21
  ) -> None:
20
22
  self.total_files = total_files
21
23
  self.description = description
22
- self.console = Console(stderr=True)
24
+ self.item_label = item_label
23
25
 
24
- logger = logging.getLogger("codeanalyzer")
25
- current_level = logger.getEffectiveLevel()
26
+ _logger = logging.getLogger("codeanalyzer")
27
+ current_level = _logger.getEffectiveLevel()
26
28
 
27
29
  # Disable progress if logger level is higher than INFO (e.g., WARNING or ERROR)
28
30
  self.disabled = current_level >= logging.ERROR
@@ -32,16 +34,22 @@ class ProgressBar:
32
34
 
33
35
  def __enter__(self):
34
36
  if not self.disabled:
37
+ # Import the shared console from the logging module so that Rich can
38
+ # coordinate log messages and live progress rendering on the same
39
+ # console — prevents the bar from appearing twice when a warning is
40
+ # printed while the progress is active.
41
+ from codeanalyzer.utils.logging import console as _shared_console
42
+
35
43
  self._progress = Progress(
36
44
  SpinnerColumn(spinner_name="dots"),
37
45
  TextColumn("[progress.description]{task.description}"),
38
46
  BarColumn(),
39
47
  TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
40
- TextColumn("[blue]{task.completed}/{task.total} files"),
48
+ TextColumn(f"[blue]{{task.completed}}/{{task.total}} {self.item_label}"),
41
49
  TimeElapsedColumn(),
42
50
  TimeRemainingColumn(),
43
51
  transient=False,
44
- console=self.console, # <-- Use stderr-safe console
52
+ console=_shared_console,
45
53
  )
46
54
  self._progress.start()
47
55
  self._task_id = self._progress.add_task(
@@ -0,0 +1,553 @@
1
+ Metadata-Version: 2.4
2
+ Name: codeanalyzer-python
3
+ Version: 0.3.1
4
+ Summary: Static Analysis on Python source code using Jedi, CodeQL and Treesitter — emits analysis.json or a Neo4j property graph.
5
+ Author-email: Rahul Krishna <i.m.ralk@gmail.com>
6
+ License-File: LICENSE
7
+ License-File: NOTICE
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: jedi<0.20.0,>=0.18.0; python_version < '3.11'
10
+ Requires-Dist: jedi<=0.19.2; python_version >= '3.11'
11
+ Requires-Dist: msgpack<1.0.7,>=1.0.0; python_version < '3.11'
12
+ Requires-Dist: msgpack<2.0.0,>=1.0.7; python_version >= '3.11'
13
+ Requires-Dist: networkx<3.2.0,>=2.6.0; python_version < '3.11'
14
+ Requires-Dist: networkx<4.0.0,>=3.0.0; python_version >= '3.11'
15
+ Requires-Dist: numpy<1.24.0,>=1.21.0; python_version < '3.11'
16
+ Requires-Dist: numpy<2.0.0,>=1.24.0; python_version >= '3.11' and python_version < '3.12'
17
+ Requires-Dist: numpy<2.0.0,>=1.26.0; python_version >= '3.12'
18
+ Requires-Dist: packaging>=25.0
19
+ Requires-Dist: pandas<2.0.0,>=1.3.0; python_version < '3.11'
20
+ Requires-Dist: pandas<3.0.0,>=2.0.0; python_version >= '3.11'
21
+ Requires-Dist: pycg>=0.0.6
22
+ Requires-Dist: pydantic<2.0.0,>=1.8.0; python_version < '3.11'
23
+ Requires-Dist: pydantic<3.0.0,>=2.0.0; python_version >= '3.11'
24
+ Requires-Dist: ray<3.0.0,>=2.10.0; python_version >= '3.11'
25
+ Requires-Dist: ray==2.0.0; python_version < '3.11'
26
+ Requires-Dist: requests<3.0.0,>=2.20.0; python_version >= '3.11'
27
+ Requires-Dist: rich<14.0.0,>=12.6.0; python_version < '3.11'
28
+ Requires-Dist: rich<15.0.0,>=14.0.0; python_version >= '3.11'
29
+ Requires-Dist: typer<1.0.0,>=0.9.0; python_version < '3.11'
30
+ Requires-Dist: typer<2.0.0,>=0.9.0; python_version >= '3.11'
31
+ Requires-Dist: typing-extensions<5.0.0,>=4.0.0; python_version < '3.11'
32
+ Requires-Dist: typing-extensions<6.0.0,>=4.5.0; python_version >= '3.11'
33
+ Requires-Dist: uv>=0.5.0
34
+ Provides-Extra: neo4j
35
+ Requires-Dist: neo4j<6.0.0,>=5.0.0; extra == 'neo4j'
36
+ Description-Content-Type: text/markdown
37
+
38
+ <div align="center">
39
+
40
+ <img src="https://github.com/codellm-devkit/codeanalyzer-python/blob/main/docs/assets/logo.png?raw=true" alt="CodeLLM-DevKit" />
41
+
42
+ # codeanalyzer-python (`canpy`)
43
+
44
+ **A Python static-analysis toolkit — the CLDK backend that emits a canonical symbol table and call graph, as `analysis.json` or a Neo4j property graph.**
45
+
46
+ [![PyPI](https://img.shields.io/pypi/v/codeanalyzer-python?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/codeanalyzer-python/)
47
+ [![GitHub release](https://img.shields.io/github/v/release/codellm-devkit/codeanalyzer-python?style=for-the-badge&logo=github&label=GitHub&color=2dba4e)](https://github.com/codellm-devkit/codeanalyzer-python/releases/latest)
48
+ [![Release](https://img.shields.io/github/actions/workflow/status/codellm-devkit/codeanalyzer-python/release.yml?style=for-the-badge&label=release&logo=githubactions&logoColor=white)](https://github.com/codellm-devkit/codeanalyzer-python/actions/workflows/release.yml)
49
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue?style=for-the-badge)](./LICENSE)
50
+
51
+ </div>
52
+
53
+ ---
54
+
55
+ `canpy` is a static analyzer for Python built on [Jedi](https://jedi.readthedocs.io/), with optional
56
+ [CodeQL](https://codeql.github.com/)-resolved call edges and
57
+ [Tree-sitter](https://tree-sitter.github.io/) parsing. It produces the canonical CodeLLM-DevKit
58
+ (CLDK) `analysis.json` — a symbol table plus a call graph — and can project that same analysis into a
59
+ **Neo4j property graph**. It is the Python backend behind
60
+ [CLDK](https://github.com/codellm-devkit/python-sdk), mirroring its
61
+ [TypeScript](https://github.com/codellm-devkit/codeanalyzer-typescript) (`cants`) and
62
+ [Java](https://github.com/codellm-devkit/codeanalyzer-java) siblings.
63
+
64
+ Every run produces a symbol table **and** a call graph. Edges come from Jedi's lexical resolution by
65
+ default; `--codeql` resolves additional edges (RPC / third-party / dynamically-dispatched targets)
66
+ and merges them with the Jedi-derived edges, also backfilling callees Jedi could not resolve.
67
+
68
+ ## Table of Contents
69
+
70
+ - [Features](#features)
71
+ - [Installation](#installation)
72
+ - [Prerequisites](#prerequisites)
73
+ - [Install via pip (PyPI)](#install-via-pip-pypi)
74
+ - [Install via shell script](#install-via-shell-script)
75
+ - [Install via Homebrew](#install-via-homebrew)
76
+ - [Build from source](#build-from-source)
77
+ - [Usage](#usage)
78
+ - [Options](#options)
79
+ - [Examples](#examples)
80
+ - [Output targets](#output-targets)
81
+ - [`analysis.json` (default)](#analysisjson-default)
82
+ - [Neo4j graph](#neo4j-graph)
83
+ - [Schema contract](#schema-contract)
84
+ - [Development](#development)
85
+ - [License](#license)
86
+
87
+ ## Features
88
+
89
+ - **Symbol table** — modules, classes, functions, methods, variables, decorators, imports, and
90
+ docstrings, with precise source spans.
91
+ - **Call graph** — Jedi's lexical resolver by default, with optional **CodeQL**-resolved edges
92
+ (`--codeql`) for RPC / third-party / dynamically-dispatched targets, merged with the Jedi edges;
93
+ CodeQL also backfills callees Jedi could not resolve.
94
+ - **Neo4j output** — project the analysis into a labeled property graph: a self-contained
95
+ `graph.cypher` snapshot, or an **incremental** push to a live database over Bolt.
96
+ - **Versioned schema** — a machine-readable, version-stamped Neo4j schema contract (`--emit schema`),
97
+ checked in as `schema.neo4j.json` and shipped with every release.
98
+ - **Incremental cache** — per-file results are cached under `.codeanalyzer`; `--lazy` (default)
99
+ reuses them, `--eager` forces a clean rebuild. `--ray` distributes the work across cores.
100
+ - **Compact output** — canonical `analysis.json`, or binary `analysis.msgpack` for smaller artifacts.
101
+
102
+ ## Installation
103
+
104
+ ### Prerequisites
105
+
106
+ - **Python 3.10 or newer.**
107
+ - A C toolchain and the `venv` / development headers — the analyzer builds an isolated virtual
108
+ environment per project (via Python's `venv`) so Jedi can resolve types and imports:
109
+
110
+ ```sh
111
+ # Ubuntu / Debian
112
+ sudo apt install python3-venv python3-dev build-essential
113
+
114
+ # Fedora / RHEL / CentOS
115
+ sudo dnf group install "Development Tools" && sudo dnf install python3-venv python3-devel
116
+
117
+ # macOS
118
+ xcode-select --install
119
+ ```
120
+
121
+ ### Install via pip (PyPI)
122
+
123
+ ```sh
124
+ pip install codeanalyzer-python
125
+ canpy --help
126
+ ```
127
+
128
+ For the optional **live Neo4j push** (`--emit neo4j --neo4j-uri …`), install the `neo4j` extra:
129
+
130
+ ```sh
131
+ pip install 'codeanalyzer-python[neo4j]'
132
+ ```
133
+
134
+ ### Install via shell script
135
+
136
+ Install the CLI as an isolated tool with the one-line installer (provisions via uv / pipx / pip):
137
+
138
+ ```sh
139
+ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/codellm-devkit/codeanalyzer-python/releases/latest/download/canpy-installer.sh | sh
140
+ ```
141
+
142
+ ### Install via Homebrew
143
+
144
+ ```sh
145
+ brew install codellm-devkit/tap/codeanalyzer-python
146
+ ```
147
+
148
+ The formula depends on [uv](https://docs.astral.sh/uv/) and installs `canpy` as an isolated,
149
+ version-pinned uv tool (the package and its dependencies are resolved and cached on first run).
150
+
151
+ ### Build from source
152
+
153
+ This project uses [uv](https://docs.astral.sh/uv/) for dependency management.
154
+
155
+ ```sh
156
+ git clone https://github.com/codellm-devkit/codeanalyzer-python
157
+ cd codeanalyzer-python
158
+ uv sync --all-groups
159
+ uv run canpy --help
160
+ ```
161
+
162
+ ## Usage
163
+
164
+ ```sh
165
+ canpy --input /path/to/python/project
166
+ ```
167
+
168
+ With no `--output`, the analysis is printed to stdout as compact JSON; with `--output <dir>` it is
169
+ written to `analysis.json` (or `graph.cypher` for `--emit neo4j`, or `analysis.msgpack` with
170
+ `--format msgpack`) in that directory.
171
+
172
+ ### Options
173
+
174
+ <!-- BEGIN canpy-help -->
175
+
176
+ ```text
177
+ $ canpy --help
178
+
179
+ Usage: canpy [OPTIONS] COMMAND [ARGS]...
180
+
181
+ Static Analysis on Python source code using Jedi, PyCG and Tree sitter.
182
+
183
+ ╭─ Options ────────────────────────────────────────────────────────────────────╮
184
+ │ --version Show the canpy │
185
+ │ version and │
186
+ │ exit. │
187
+ │ --input -i PATH Path to the │
188
+ │ project root │
189
+ │ directory (not │
190
+ │ required for │
191
+ │ --emit schema). │
192
+ │ --output -o PATH Output directory │
193
+ │ for artifacts. │
194
+ │ --format -f [json|msgpack] Output format │
195
+ │ for --emit json: │
196
+ │ json or msgpack. │
197
+ │ [default: json] │
198
+ │ --emit [json|neo4j|sche Output target: │
199
+ │ ma] json │
200
+ │ (analysis.json, │
201
+ │ default) | neo4j │
202
+ │ (graph.cypher or │
203
+ │ live Bolt push) │
204
+ │ | schema (the │
205
+ │ Neo4j │
206
+ │ schema.json │
207
+ │ contract). │
208
+ │ [default: json] │
209
+ │ --app-name TEXT Logical │
210
+ │ application name │
211
+ │ for the graph │
212
+ │ :PyApplication │
213
+ │ anchor (default: │
214
+ │ input dir name). │
215
+ │ --neo4j-uri TEXT Push the graph │
216
+ │ to a live Neo4j │
217
+ │ over Bolt │
218
+ │ (incremental); │
219
+ │ omit to write │
220
+ │ graph.cypher. │
221
+ │ [env var: │
222
+ │ NEO4J_URI] │
223
+ │ --neo4j-user TEXT Neo4j username. │
224
+ │ [env var: │
225
+ │ NEO4J_USERNAME] │
226
+ │ [default: neo4j] │
227
+ │ --neo4j-password TEXT Neo4j password. │
228
+ │ Prefer the env │
229
+ │ var over the │
230
+ │ flag (the flag │
231
+ │ is visible in │
232
+ │ shell history / │
233
+ │ process list). │
234
+ │ [env var: │
235
+ │ NEO4J_PASSWORD] │
236
+ │ [default: neo4j] │
237
+ │ --neo4j-database TEXT Neo4j database │
238
+ │ name (default: │
239
+ │ server default). │
240
+ │ [env var: │
241
+ │ NEO4J_DATABASE] │
242
+ │ --analysis-level -a INTEGER RANGE Analysis depth: │
243
+ │ [1<=x<=2] 1=symbol │
244
+ │ table+Jedi call │
245
+ │ graph, 2=+PyCG │
246
+ │ call graph. │
247
+ │ [default: 1] │
248
+ │ --ray --no-ray Enable Ray for │
249
+ │ distributed │
250
+ │ analysis. │
251
+ │ [default: │
252
+ │ no-ray] │
253
+ │ --eager --lazy Enable eager or │
254
+ │ lazy analysis. │
255
+ │ Defaults to │
256
+ │ lazy. │
257
+ │ [default: lazy] │
258
+ │ --skip-tests --include-tests Skip test files │
259
+ │ in analysis. │
260
+ │ [default: │
261
+ │ skip-tests] │
262
+ │ --no-venv --venv Skip virtualenv │
263
+ │ creation and │
264
+ │ dependency │
265
+ │ installation; │
266
+ │ resolve imports │
267
+ │ against the │
268
+ │ ambient Python │
269
+ │ environment │
270
+ │ instead. │
271
+ │ [default: venv] │
272
+ │ --file-name PATH Analyze only the │
273
+ │ specified file │
274
+ │ (relative to │
275
+ │ input │
276
+ │ directory). │
277
+ │ --cache-dir -c PATH Directory to │
278
+ │ store analysis │
279
+ │ cache. Defaults │
280
+ │ to │
281
+ │ '.codeanalyzer' │
282
+ │ in the input │
283
+ │ directory. │
284
+ │ --clear-cache --keep-cache Clear cache │
285
+ │ after analysis. │
286
+ │ By default, │
287
+ │ cache is │
288
+ │ retained. │
289
+ │ [default: │
290
+ │ keep-cache] │
291
+ │ -v INTEGER Increase │
292
+ │ verbosity: -v, │
293
+ │ -vv, -vvv │
294
+ │ [default: 0] │
295
+ │ --pycg-shard --no-pycg-shard Shard PyCG │
296
+ │ call-graph │
297
+ │ analysis by │
298
+ │ Python package │
299
+ │ (level 2 only). │
300
+ │ When the project │
301
+ │ exceeds the │
302
+ │ 500-file │
303
+ │ ceiling, PyCG is │
304
+ │ run │
305
+ │ independently │
306
+ │ per top-level │
307
+ │ package with │
308
+ │ cross-package │
309
+ │ imports treated │
310
+ │ as ghost nodes. │
311
+ │ Without this │
312
+ │ flag, projects │
313
+ │ over the ceiling │
314
+ │ fall back to │
315
+ │ Jedi-only edges. │
316
+ │ [default: │
317
+ │ no-pycg-shard] │
318
+ │ --pycg-shard-cei… INTEGER RANGE Maximum files │
319
+ │ [x>=1] per shard when │
320
+ │ --pycg-shard is │
321
+ │ active (default │
322
+ │ 100). Shards │
323
+ │ exceeding this │
324
+ │ limit are │
325
+ │ skipped; their │
326
+ │ call edges are │
327
+ │ omitted from the │
328
+ │ call graph (Jedi │
329
+ │ edges for those │
330
+ │ packages are │
331
+ │ still included). │
332
+ │ Lower values are │
333
+ │ safer for │
334
+ │ packages with │
335
+ │ deep class │
336
+ │ hierarchies or │
337
+ │ heavy import │
338
+ │ graphs. │
339
+ │ [default: 100] │
340
+ │ --pycg-shard-tim… INTEGER RANGE Per-shard │
341
+ │ [x>=0] wall-clock │
342
+ │ timeout in │
343
+ │ seconds when │
344
+ │ --pycg-shard is │
345
+ │ active (default │
346
+ │ 120). A shard │
347
+ │ that exceeds │
348
+ │ this limit is │
349
+ │ skipped │
350
+ │ gracefully. │
351
+ │ PyCG's fixpoint │
352
+ │ is bimodal: it │
353
+ │ either converges │
354
+ │ quickly or │
355
+ │ diverges │
356
+ │ indefinitely, so │
357
+ │ the timeout acts │
358
+ │ as a final │
359
+ │ safety net after │
360
+ │ the file-count │
361
+ │ ceiling. Set to │
362
+ │ 0 to disable. │
363
+ │ POSIX only │
364
+ │ (macOS / Linux); │
365
+ │ ignored on │
366
+ │ Windows. │
367
+ │ [default: 120] │
368
+ │ --pycg-shard-str… [jedi|package] How --pycg-shard │
369
+ │ groups files │
370
+ │ (level 2 only). │
371
+ │ 'jedi' (default) │
372
+ │ partitions the │
373
+ │ Jedi │
374
+ │ module-dependen… │
375
+ │ graph (SCC + │
376
+ │ Louvain) so │
377
+ │ tightly-coupled │
378
+ │ modules │
379
+ │ co-compute and │
380
+ │ few call edges │
381
+ │ are severed │
382
+ │ between shards; │
383
+ │ import cycles │
384
+ │ are never split. │
385
+ │ 'package' uses │
386
+ │ the legacy │
387
+ │ one-shard-per-p… │
388
+ │ grouping. │
389
+ │ [default: jedi] │
390
+ │ --pycg-max-iter INTEGER RANGE Cap on PyCG's │
391
+ │ [x>=-1] fixpoint passes │
392
+ │ per │
393
+ │ shard/project │
394
+ │ (level 2; │
395
+ │ default 50). │
396
+ │ PyCG iterates │
397
+ │ until its │
398
+ │ points-to state │
399
+ │ stops changing, │
400
+ │ but its │
401
+ │ access-path │
402
+ │ domain has no │
403
+ │ convergence │
404
+ │ bound, so heavy │
405
+ │ metaclass/mixin │
406
+ │ code (e.g. an │
407
+ │ ORM) can loop │
408
+ │ with each pass │
409
+ │ costing seconds. │
410
+ │ The cap returns │
411
+ │ a │
412
+ │ sound-but-incom… │
413
+ │ call graph │
414
+ │ instead of │
415
+ │ looping until │
416
+ │ the timeout │
417
+ │ kills it. Set to │
418
+ │ -1 for PyCG's │
419
+ │ unbounded │
420
+ │ run-to-converge… │
421
+ │ behaviour. │
422
+ │ [default: 50] │
423
+ │ --help Show this │
424
+ │ message and │
425
+ │ exit. │
426
+ ╰──────────────────────────────────────────────────────────────────────────────╯
427
+ ```
428
+
429
+ <!-- END canpy-help -->
430
+
431
+ ### Examples
432
+
433
+ 1. **Basic analysis to stdout, or to a file:**
434
+ ```sh
435
+ canpy --input ./my-python-project # compact JSON on stdout
436
+ canpy --input ./my-python-project --output ./out # → ./out/analysis.json
437
+ ```
438
+
439
+ 2. **Binary output (msgpack):**
440
+ ```sh
441
+ canpy --input ./my-python-project --output ./out --format msgpack # → ./out/analysis.msgpack
442
+ ```
443
+
444
+ 3. **Resolve extra call edges with CodeQL:**
445
+ ```sh
446
+ canpy --input ./my-python-project --codeql
447
+ ```
448
+ By default, edges come from Jedi's lexical analysis. Adding `--codeql` resolves additional edges
449
+ (including RPC / third-party / dynamically-dispatched targets) and merges them with the
450
+ Jedi-derived edges; CodeQL also backfills resolved callees Jedi could not resolve. CodeQL
451
+ integration is experimental; the CLI is downloaded into `<cache_dir>/codeql/` on first use.
452
+
453
+ 4. **Emit a Neo4j snapshot, or push to a live database:**
454
+ ```sh
455
+ canpy --input ./my-python-project --emit neo4j --output ./out # → ./out/graph.cypher
456
+ canpy --input ./my-python-project --emit neo4j \
457
+ --neo4j-uri bolt://localhost:7687 --neo4j-user neo4j --neo4j-password secret
458
+ ```
459
+
460
+ 5. **Emit the Neo4j schema contract:**
461
+ ```sh
462
+ canpy --emit schema # print schema.json to stdout (no project needed)
463
+ canpy --emit schema --output ./out # → ./out/schema.json
464
+ ```
465
+
466
+ 6. **Force a clean rebuild with a custom cache directory:**
467
+ ```sh
468
+ canpy --input ./my-python-project --eager --cache-dir /path/to/custom-cache
469
+ ```
470
+
471
+ ## Output targets
472
+
473
+ `canpy` builds one analysis in memory and can emit it three ways (`--emit`):
474
+
475
+ ### `analysis.json` (default)
476
+
477
+ A `PyApplication` document — the canonical CLDK contract:
478
+
479
+ ```jsonc
480
+ {
481
+ "symbol_table": { /* file path → module (classes, functions, variables, imports, …) */ },
482
+ "call_graph": [ /* CALL_DEP edges: { source, target, weight, provenance } keyed by callable signature */ ]
483
+ }
484
+ ```
485
+
486
+ By default this is printed to stdout in JSON; with `--output` it is written to `analysis.json` (or
487
+ `analysis.msgpack` with `--format msgpack`, a more compact binary format).
488
+
489
+ ### Neo4j graph
490
+
491
+ `--emit neo4j` projects the same analysis into a labeled property graph. Every node label is
492
+ `Py`-prefixed and every relationship type is `PY_`-prefixed (e.g. `:PyClass`, `PY_CALLS`) so multiple
493
+ language analyzers can share one database without label or relationship-type collisions. Declarations
494
+ are keyed by their signature under a shared `:PySymbol` label; calls, imports, inheritance,
495
+ decorators, and call sites are relationships:
496
+
497
+ - **Without `--neo4j-uri`** — writes a self-contained `graph.cypher` (constraints + indexes, a scoped
498
+ wipe, then batched `MERGE`s). Load it with `cypher-shell < graph.cypher`. Needs no extra
499
+ dependencies.
500
+ - **With `--neo4j-uri`** — pushes to a live Neo4j over Bolt **incrementally**: only modules whose
501
+ content hash changed are rewritten, and on a full run modules whose source file vanished are
502
+ pruned. Requires the `neo4j` extra. Every graph carries a `schema_version` on its `:PyApplication`
503
+ node.
504
+
505
+ Call-graph endpoints that aren't present in the symbol table (third-party / framework / RPC targets)
506
+ are materialized as `:PyExternal` ghost nodes, mirroring the analyzer's own ghost-node behaviour.
507
+
508
+ The connection options also read from the standard Neo4j environment variables — `NEO4J_URI`,
509
+ `NEO4J_USERNAME`, `NEO4J_PASSWORD`, `NEO4J_DATABASE` — when the corresponding flag is omitted (an
510
+ explicit flag wins). Prefer the env var for the password so it doesn't land in shell history or the
511
+ process list:
512
+
513
+ ```sh
514
+ export NEO4J_URI=bolt://localhost:7687
515
+ export NEO4J_PASSWORD=secret
516
+ canpy -i ./my-project --emit neo4j # credentials picked up from the environment
517
+ ```
518
+
519
+ ### Schema contract
520
+
521
+ `--emit schema` writes the machine-readable, version-stamped Neo4j schema (`schema.json`: node labels,
522
+ relationships, properties, constraints, and indexes). It needs no project and is checked into the repo
523
+ as `schema.neo4j.json` and bundled in every release as a GitHub Release asset, so a consumer can
524
+ validate producer/consumer compatibility without invoking the tool. The shape of the contract matches
525
+ the [`codeanalyzer-typescript`](https://github.com/codellm-devkit/codeanalyzer-typescript) backend.
526
+
527
+ A UML of the `analysis.json` schema (the `PyApplication` containment tree) is checked in as
528
+ [`schema-uml.drawio`](./schema-uml.drawio), and the property-graph schema as
529
+ [`neo4j-schema.drawio`](./neo4j-schema.drawio).
530
+
531
+ ## Development
532
+
533
+ This project uses [uv](https://docs.astral.sh/uv/).
534
+
535
+ ```sh
536
+ uv sync --all-groups
537
+ uv run canpy --input /path/to/project # run from source
538
+ uv run canpy --emit schema > schema.neo4j.json # regenerate the checked-in schema contract
539
+ uv run python scripts/update_readme.py # regenerate the canpy --help block above
540
+ uv run pytest # run the test suite
541
+ ```
542
+
543
+ The Neo4j schema-conformance test always runs. The Neo4j **bolt** integration test spins up a real
544
+ Neo4j via [Testcontainers](https://testcontainers.com/) and is **opt-in** — it needs a container
545
+ runtime (Docker or Podman) and is enabled with an environment variable:
546
+
547
+ ```sh
548
+ RUN_CONTAINER_TESTS=1 uv run pytest test/test_neo4j_bolt.py -s
549
+ ```
550
+
551
+ ## License
552
+
553
+ Apache 2.0 — see [LICENSE](./LICENSE).
@@ -0,0 +1,39 @@
1
+ codeanalyzer/__init__.py,sha256=BZ3Kuwl-F_F-8H8cepLnVJ4Ku4NNUjjqg0Y6ujPQSsI,108
2
+ codeanalyzer/__main__.py,sha256=GOGc6j-euSeRZbUQgfZykQClT32WNxcStKoO96s-VSY,12790
3
+ codeanalyzer/core.py,sha256=BCH5OynbXrnItPvYhcpAChcBtkXdQIMbH_fyFGCxITU,31185
4
+ codeanalyzer/provenance.py,sha256=DT-DqwdVwO7g-GyK3sC0_4vDNCUjZYEY1fyYCt-7VRM,2306
5
+ codeanalyzer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ codeanalyzer/config/__init__.py,sha256=9XBxAn1oWGRuhg3bEBUuVGs3hFNXEAKrr-Ce7tq9a2k,61
7
+ codeanalyzer/config/config.py,sha256=ZiKzc5uEUCIvih58-6BDtLLI1hPij41wGQjBcj9KNQM,188
8
+ codeanalyzer/jedi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ codeanalyzer/jedi/jedi.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ codeanalyzer/neo4j/__init__.py,sha256=8Ei6uThTOqmul6GhnKoGOjLXJZbyDwF5Jo-8I8NxnAk,1574
11
+ codeanalyzer/neo4j/bolt.py,sha256=-IFDb_d67IkGwxvmmbLNI6AvSQHY-XsutI3szibWidw,9635
12
+ codeanalyzer/neo4j/cypher.py,sha256=2zIWXA1AADrwCMhSTeqKjEXRgBjbob6o3bme_cwLu0s,5024
13
+ codeanalyzer/neo4j/emit.py,sha256=NZL5BVY1Fb32igH22986_cUFUIgHNWJHbii2bfX2E3M,3099
14
+ codeanalyzer/neo4j/project.py,sha256=YMVtF1GjLZYwnhFl-7fy9Qj7fpGgd5aEGOp0JMF3VAY,14513
15
+ codeanalyzer/neo4j/rows.py,sha256=5xI3X-l-vwPe_gmKYUg7VuUQARcOlVAZnGmiQr9QyRk,7326
16
+ codeanalyzer/neo4j/schema.py,sha256=tZjnpIFdTV3-GB1x2zTbDMxQOH16GqELYCf2cR2XhgU,8765
17
+ codeanalyzer/options/__init__.py,sha256=Ki4qhHFqpyuUWVsntO-NYJMVWrkeFOzPW4nQ7oxiUVI,155
18
+ codeanalyzer/options/options.py,sha256=5w3DZYlAv-0LVPavF1P5EEJp8XBRrSXidbDusdbXQAo,1976
19
+ codeanalyzer/schema/__init__.py,sha256=cLPjvowrnz8xzi7tZAsKQeIOjdOKRGHy4I7wbG0jHk8,2024
20
+ codeanalyzer/schema/py_schema.py,sha256=yYKkg3ufNDteNvtiPiRJQz-pzK5NnnnNldhXcW7p2RI,13274
21
+ codeanalyzer/semantic_analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ codeanalyzer/semantic_analysis/call_graph.py,sha256=H3IkfGp1VCkDxI75JPyLSSA-wOaJp_I-NYPsjCY04Bg,11185
23
+ codeanalyzer/semantic_analysis/pycg/__init__.py,sha256=Lsgz25iFM_RGGu_i2psY-LN-KwvYIZQPnKgRPL1JsjU,928
24
+ codeanalyzer/semantic_analysis/pycg/pycg_analysis.py,sha256=22kiZ2Kjpds024iFLu2bPEcCtAKAivdp1Xlr9rfP9lM,45205
25
+ codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py,sha256=n4tRderrSYw9cUYgR2wsn64UqozY56nD1YfDNnaVcAk,968
26
+ codeanalyzer/semantic_analysis/pycg/shard_planner.py,sha256=PTXMG9YcrZX9hgnlnIlpQx6xmWkizP3aQrf0elcrnmw,15865
27
+ codeanalyzer/syntactic_analysis/__init__.py,sha256=EUQkJEh6wHjWx2qTTKbTbUgwSbfKeNieKHNy7RknVXA,476
28
+ codeanalyzer/syntactic_analysis/exceptions.py,sha256=whs_n0vIu655Jkk1a7iOoXY6iIca4pZqJnU40V9Ejaw,537
29
+ codeanalyzer/syntactic_analysis/import_resolver.py,sha256=Q8noZwSdDNt4P4NMqDTB9FaS-M0_d4ASK9GnJop9MDI,2751
30
+ codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=hQOEk2qRYM9Y_UoejNbZFCWSkDjBa-vRcU3kPURrMNc,41148
31
+ codeanalyzer/utils/__init__.py,sha256=hC6VWdR5rerSqBxzu9KQHTASWqwrrYJv-CMDwrTlzkc,137
32
+ codeanalyzer/utils/logging.py,sha256=Fw0tattPAOMs3o0JMjjXhRVLIF64f-SCcygUXF9jqeg,904
33
+ codeanalyzer/utils/progress_bar.py,sha256=C9JtzVdd10lIxTv-KA6PebqjKWueC_vMGwVzAtHuHIw,2818
34
+ codeanalyzer_python-0.3.1.dist-info/METADATA,sha256=F-I41TfuL_typkjLjnjSxR8fPvUu1VgbAX8K1_vnm80,34032
35
+ codeanalyzer_python-0.3.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
36
+ codeanalyzer_python-0.3.1.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
37
+ codeanalyzer_python-0.3.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
38
+ codeanalyzer_python-0.3.1.dist-info/licenses/NOTICE,sha256=YU0Z9NDWqKY-2jfFcbxeZ6fbnzz0oZeKmnUcO8a-bcQ,901
39
+ codeanalyzer_python-0.3.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any