codeanalyzer-python 1.0.1__tar.gz → 1.0.3__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.1 → codeanalyzer_python-1.0.3}/PKG-INFO +2 -1
  2. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/core.py +161 -8
  3. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/project.py +29 -14
  4. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/pyproject.toml +4 -1
  5. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/.gitignore +0 -0
  6. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/LICENSE +0 -0
  7. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/NOTICE +0 -0
  8. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/README.md +0 -0
  9. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/__init__.py +0 -0
  10. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/__main__.py +0 -0
  11. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/config/__init__.py +0 -0
  12. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/config/config.py +0 -0
  13. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/__init__.py +0 -0
  14. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/access_paths.py +0 -0
  15. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/alias.py +0 -0
  16. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/builder.py +0 -0
  17. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/cfg.py +0 -0
  18. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/defuse.py +0 -0
  19. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/dominance.py +0 -0
  20. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/identity.py +0 -0
  21. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/pdg.py +0 -0
  22. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
  23. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/scc.py +0 -0
  24. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/sdg.py +0 -0
  25. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/slicing.py +0 -0
  26. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/summaries.py +0 -0
  27. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/syntactic.py +0 -0
  28. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/jedi/__init__.py +0 -0
  29. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/jedi/jedi.py +0 -0
  30. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/__init__.py +0 -0
  31. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/bolt.py +0 -0
  32. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/cypher.py +0 -0
  33. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/emit.py +0 -0
  34. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/rows.py +0 -0
  35. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/schema.py +0 -0
  36. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/options/__init__.py +0 -0
  37. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/options/options.py +0 -0
  38. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/provenance.py +0 -0
  39. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/py.typed +0 -0
  40. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/__init__.py +0 -0
  41. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/assign_ids.py +0 -0
  42. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/call_graph_ids.py +0 -0
  43. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/ids.py +0 -0
  44. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/l1_body.py +0 -0
  45. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/l2_callees.py +0 -0
  46. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/py_schema.py +0 -0
  47. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/__init__.py +0 -0
  48. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
  49. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/__init__.py +0 -0
  50. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +0 -0
  51. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py +0 -0
  52. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/shard_planner.py +0 -0
  53. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
  54. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
  55. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
  56. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +0 -0
  57. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/utils/__init__.py +0 -0
  58. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/utils/logging.py +0 -0
  59. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/utils/progress_bar.py +0 -0
  60. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/docs/handoff/README.md +0 -0
  61. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/docs/handoff/schema.neo4j.json +0 -0
  62. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/schema-uml.drawio +0 -0
  63. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/schema.neo4j.json +0 -0
  64. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/README.md +0 -0
  65. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
  66. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
  67. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
  68. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/LICENSE +0 -0
  69. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/NOTICE +0 -0
  70. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/README.md +0 -0
  71. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
  72. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
  73. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
  74. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/README.md +0 -0
  75. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
  76. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
  77. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
  78. {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/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.1
3
+ Version: 1.0.3
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
@@ -18,6 +18,7 @@ Requires-Dist: numpy<2.0.0,>=1.26.0; python_version >= '3.12'
18
18
  Requires-Dist: packaging>=25.0
19
19
  Requires-Dist: pandas<2.0.0,>=1.3.0; python_version < '3.11'
20
20
  Requires-Dist: pandas<3.0.0,>=2.0.0; python_version >= '3.11'
21
+ Requires-Dist: parso>=0.8.5
21
22
  Requires-Dist: pycg>=0.0.6
22
23
  Requires-Dist: pydantic<2.0.0,>=1.8.0; python_version < '3.11'
23
24
  Requires-Dist: pydantic<3.0.0,>=2.0.0; python_version >= '3.11'
@@ -163,8 +163,159 @@ class Codeanalyzer:
163
163
  stderr=None,
164
164
  )
165
165
 
166
+ @classmethod
167
+ def _get_base_interpreter(cls) -> Path:
168
+ """The interpreter used to provision the analysis virtualenv.
169
+
170
+ jedi parses the *analysis environment's* Python version with parso,
171
+ which ships one hardcoded grammar file per minor version — an
172
+ environment newer than the newest shipped grammar makes every file
173
+ fail with "Python version X.Y is currently not supported" while the
174
+ run still exits 0 (#107). So the default choice is gated on the
175
+ installed parso's ceiling: a too-new default is swapped for the
176
+ newest supported interpreter found on the host, falling back to the
177
+ default (loudly) only when none exists. An explicit ``SYSTEM_PYTHON``
178
+ always wins, with a warning when parso cannot parse its version.
179
+ """
180
+ # An explicit SYSTEM_PYTHON override wins (consulted only when running
181
+ # inside a virtualenv, matching the historical behavior).
182
+ if sys.prefix != sys.base_prefix:
183
+ system_python = os.getenv("SYSTEM_PYTHON")
184
+ if system_python:
185
+ system_python_path = Path(system_python)
186
+ if system_python_path.exists() and system_python_path.is_file():
187
+ ceiling = cls._parso_supported_ceiling()
188
+ version = cls._interpreter_version(system_python_path)
189
+ if ceiling is not None and version is not None and version > ceiling:
190
+ logger.warning(
191
+ f"SYSTEM_PYTHON={system_python} is Python "
192
+ f"{version[0]}.{version[1]}, newer than the newest grammar "
193
+ f"the installed parso ships ({ceiling[0]}.{ceiling[1]}). "
194
+ "jedi will likely reject every file in the analysis "
195
+ "environment (#107); honoring the explicit override anyway."
196
+ )
197
+ return system_python_path
198
+
199
+ candidate = cls._default_base_interpreter()
200
+ ceiling = cls._parso_supported_ceiling()
201
+ if ceiling is None:
202
+ return candidate
203
+ version = cls._interpreter_version(candidate)
204
+ if version is None or version <= ceiling:
205
+ return candidate
206
+ logger.warning(
207
+ f"Default interpreter {candidate} is Python {version[0]}.{version[1]}, "
208
+ f"newer than the newest grammar the installed parso ships "
209
+ f"({ceiling[0]}.{ceiling[1]}) — looking for a supported interpreter "
210
+ "for the analysis environment (#107)."
211
+ )
212
+ supported = cls._find_supported_interpreter(ceiling)
213
+ if supported is not None:
214
+ logger.info(f"Provisioning the analysis environment with {supported}.")
215
+ return supported
216
+ logger.warning(
217
+ f"No interpreter <= {ceiling[0]}.{ceiling[1]} found on this host; "
218
+ f"falling back to {candidate}. jedi/parso will likely reject every "
219
+ "file — install a supported Python or upgrade parso."
220
+ )
221
+ return candidate
222
+
223
+ @staticmethod
224
+ def _versions_from_grammar_stems(stems: List[str]) -> List[tuple]:
225
+ """``grammar313`` → ``(3, 13)``, sorted ascending; malformed stems dropped."""
226
+ versions = []
227
+ for stem in stems:
228
+ digits = stem[len("grammar"):]
229
+ if len(digits) >= 2 and digits.isdigit():
230
+ versions.append((int(digits[0]), int(digits[1:])))
231
+ return sorted(versions)
232
+
233
+ @classmethod
234
+ def _parso_supported_ceiling(cls) -> Optional[tuple]:
235
+ """Newest ``(major, minor)`` the installed parso ships a grammar for,
236
+ derived from its ``python/grammar*.txt`` files so the ceiling moves
237
+ automatically when parso adds a version. ``None`` if undeterminable."""
238
+ try:
239
+ import parso
240
+
241
+ stems = [
242
+ p.stem
243
+ for p in (Path(parso.__file__).parent / "python").glob("grammar*.txt")
244
+ ]
245
+ versions = cls._versions_from_grammar_stems(stems)
246
+ return versions[-1] if versions else None
247
+ except Exception:
248
+ return None
249
+
250
+ @staticmethod
251
+ def _interpreter_version(interpreter: Path) -> Optional[tuple]:
252
+ """``(major, minor)`` of an interpreter, or ``None`` if it can't run."""
253
+ try:
254
+ result = subprocess.run(
255
+ [
256
+ str(interpreter),
257
+ "-c",
258
+ "import sys; print('%d.%d' % sys.version_info[:2])",
259
+ ],
260
+ capture_output=True,
261
+ text=True,
262
+ timeout=5,
263
+ )
264
+ if result.returncode == 0:
265
+ major, minor = result.stdout.strip().split(".")
266
+ return (int(major), int(minor))
267
+ except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError, ValueError):
268
+ pass
269
+ return None
270
+
271
+ @staticmethod
272
+ def _pick_supported_interpreter(
273
+ candidates: List[tuple], ceiling: tuple
274
+ ) -> Optional[Path]:
275
+ """Newest candidate whose version is within the ceiling.
276
+
277
+ ``candidates`` is ``[(path, (major, minor) | None), ...]``."""
278
+ supported = [
279
+ (version, path)
280
+ for path, version in candidates
281
+ if version is not None and version <= ceiling
282
+ ]
283
+ return max(supported)[1] if supported else None
284
+
285
+ @classmethod
286
+ def _find_supported_interpreter(cls, ceiling: tuple) -> Optional[Path]:
287
+ """Search the host for the newest interpreter within the parso ceiling:
288
+ versioned names on PATH (``python3.13``, ``python3.12``, ...) first,
289
+ then pyenv installs."""
290
+ paths: List[Path] = []
291
+ for minor in range(ceiling[1], 7, -1):
292
+ which = shutil.which(f"python{ceiling[0]}.{minor}")
293
+ # Skip the current virtualenv's own interpreter (same rule as
294
+ # _default_base_interpreter): the analysis env must come from a
295
+ # base installation.
296
+ if which and not which.startswith(sys.prefix):
297
+ paths.append(Path(which))
298
+ for pyenv_root in (os.getenv("PYENV_ROOT"), str(Path.home() / ".pyenv")):
299
+ if not pyenv_root:
300
+ continue
301
+ versions_dir = Path(pyenv_root) / "versions"
302
+ if versions_dir.is_dir():
303
+ for install in sorted(versions_dir.iterdir(), reverse=True):
304
+ exe = install / "bin" / "python3"
305
+ if exe.exists():
306
+ paths.append(exe)
307
+ seen = set()
308
+ candidates = []
309
+ for path in paths:
310
+ key = str(path)
311
+ if key in seen:
312
+ continue
313
+ seen.add(key)
314
+ candidates.append((path, cls._interpreter_version(path)))
315
+ return cls._pick_supported_interpreter(candidates, ceiling)
316
+
166
317
  @staticmethod
167
- def _get_base_interpreter() -> Path:
318
+ def _default_base_interpreter() -> Path:
168
319
  """Get the base Python interpreter path.
169
320
 
170
321
  This method finds a suitable base Python interpreter that can be used
@@ -183,13 +334,6 @@ class Codeanalyzer:
183
334
 
184
335
  # We're inside a virtual environment; need to find the base interpreter
185
336
 
186
- # First, check if user explicitly set SYSTEM_PYTHON
187
- system_python = os.getenv("SYSTEM_PYTHON")
188
- if system_python:
189
- system_python_path = Path(system_python)
190
- if system_python_path.exists() and system_python_path.is_file():
191
- return system_python_path
192
-
193
337
  # Try to get the base interpreter from sys.base_executable (Python 3.3+)
194
338
  if hasattr(sys, "base_executable") and sys.base_executable:
195
339
  base_exec = Path(sys.base_executable)
@@ -778,6 +922,15 @@ class Codeanalyzer:
778
922
  if files_from_cache > 0:
779
923
  logger.info(f"Reused {files_from_cache} files from cache, processed {files_processed} new/changed files")
780
924
 
925
+ if py_files and not symbol_table:
926
+ logger.error(
927
+ "Every one of the %d discovered Python files failed to process — "
928
+ "the symbol table is empty. This usually means the analysis "
929
+ "environment's interpreter is newer than the installed jedi/parso "
930
+ "stack supports (#107); check the per-file errors above.",
931
+ len(py_files),
932
+ )
933
+
781
934
  logger.info(
782
935
  "✅ Symbol table: %d modules in %.1fs",
783
936
  len(symbol_table), time.perf_counter() - t0_st,
@@ -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,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codeanalyzer-python"
3
- version = "1.0.1"
3
+ version = "1.0.3"
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 = [
@@ -12,6 +12,9 @@ dependencies = [
12
12
  # jedi
13
13
  "jedi>=0.18.0,<0.20.0; python_version < '3.11'",
14
14
  "jedi<=0.19.2; python_version >= '3.11'",
15
+ # parso 0.8.5 is the first release shipping the Python 3.14 grammar; older
16
+ # resolutions make jedi reject every file in a 3.14 analysis env (#107)
17
+ "parso>=0.8.5",
15
18
  # msgpack
16
19
  "msgpack>=1.0.0,<1.0.7; python_version < '3.11'",
17
20
  "msgpack>=1.0.7,<2.0.0; python_version >= '3.11'",