codeanalyzer-python 1.0.2__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.2 → codeanalyzer_python-1.0.3}/PKG-INFO +2 -1
  2. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/core.py +161 -8
  3. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/pyproject.toml +4 -1
  4. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/.gitignore +0 -0
  5. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/LICENSE +0 -0
  6. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/NOTICE +0 -0
  7. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/README.md +0 -0
  8. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/__init__.py +0 -0
  9. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/__main__.py +0 -0
  10. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/config/__init__.py +0 -0
  11. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/config/config.py +0 -0
  12. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/__init__.py +0 -0
  13. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/access_paths.py +0 -0
  14. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/alias.py +0 -0
  15. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/builder.py +0 -0
  16. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/cfg.py +0 -0
  17. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/defuse.py +0 -0
  18. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/dominance.py +0 -0
  19. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/identity.py +0 -0
  20. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/pdg.py +0 -0
  21. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
  22. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/scc.py +0 -0
  23. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/sdg.py +0 -0
  24. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/slicing.py +0 -0
  25. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/summaries.py +0 -0
  26. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/syntactic.py +0 -0
  27. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/jedi/__init__.py +0 -0
  28. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/jedi/jedi.py +0 -0
  29. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/__init__.py +0 -0
  30. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/bolt.py +0 -0
  31. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/cypher.py +0 -0
  32. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/emit.py +0 -0
  33. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/project.py +0 -0
  34. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/rows.py +0 -0
  35. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/schema.py +0 -0
  36. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/options/__init__.py +0 -0
  37. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/options/options.py +0 -0
  38. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/provenance.py +0 -0
  39. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/py.typed +0 -0
  40. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/__init__.py +0 -0
  41. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/assign_ids.py +0 -0
  42. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/call_graph_ids.py +0 -0
  43. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/ids.py +0 -0
  44. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/l1_body.py +0 -0
  45. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/l2_callees.py +0 -0
  46. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/py_schema.py +0 -0
  47. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/__init__.py +0 -0
  48. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
  49. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/__init__.py +0 -0
  50. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +0 -0
  51. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py +0 -0
  52. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/shard_planner.py +0 -0
  53. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
  54. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
  55. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
  56. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +0 -0
  57. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/utils/__init__.py +0 -0
  58. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/utils/logging.py +0 -0
  59. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/codeanalyzer/utils/progress_bar.py +0 -0
  60. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/docs/handoff/README.md +0 -0
  61. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/docs/handoff/schema.neo4j.json +0 -0
  62. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/schema-uml.drawio +0 -0
  63. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/schema.neo4j.json +0 -0
  64. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/README.md +0 -0
  65. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
  66. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
  67. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
  68. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/LICENSE +0 -0
  69. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/NOTICE +0 -0
  70. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/README.md +0 -0
  71. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
  72. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
  73. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
  74. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/README.md +0 -0
  75. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
  76. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
  77. {codeanalyzer_python-1.0.2 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
  78. {codeanalyzer_python-1.0.2 → 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.2
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,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codeanalyzer-python"
3
- version = "1.0.2"
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'",