codeanalyzer-python 1.0.0__py3-none-any.whl → 1.0.2__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.
codeanalyzer/__main__.py CHANGED
@@ -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
codeanalyzer/core.py CHANGED
@@ -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":
@@ -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 │
@@ -1,6 +1,6 @@
1
1
  codeanalyzer/__init__.py,sha256=BZ3Kuwl-F_F-8H8cepLnVJ4Ku4NNUjjqg0Y6ujPQSsI,108
2
- codeanalyzer/__main__.py,sha256=_Bh-JbxaMJYwfRQplmfQDYivMWgwxsycbnO3FoRuDIg,14874
3
- codeanalyzer/core.py,sha256=QGxfKq2ox1YLERx_5wr0xq74GSc-x4OpcVx7bgLyf7Y,35119
2
+ codeanalyzer/__main__.py,sha256=fukx4MhfahAdwN1oHRDkbOaAZZanXrHpHwWV4FG6esg,16275
3
+ codeanalyzer/core.py,sha256=Myq36p6Az821DPzVY-mZa542K6e0zsdAXVn1qoEIOiI,36053
4
4
  codeanalyzer/provenance.py,sha256=DT-DqwdVwO7g-GyK3sC0_4vDNCUjZYEY1fyYCt-7VRM,2306
5
5
  codeanalyzer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  codeanalyzer/config/__init__.py,sha256=9XBxAn1oWGRuhg3bEBUuVGs3hFNXEAKrr-Ce7tq9a2k,61
@@ -26,7 +26,7 @@ codeanalyzer/neo4j/__init__.py,sha256=8Ei6uThTOqmul6GhnKoGOjLXJZbyDwF5Jo-8I8NxnA
26
26
  codeanalyzer/neo4j/bolt.py,sha256=0JHv6bHp2bsKwpFPT7CDzjDaz2GvGyoEaIKVtk_ayuA,10133
27
27
  codeanalyzer/neo4j/cypher.py,sha256=a7YSmxxDbPcYc5gI4TW9sM6_WOE3RgLXTGBR9K8W8kw,5321
28
28
  codeanalyzer/neo4j/emit.py,sha256=QdrZWG3_IQHMcKCX4bFINpXvqZU2Qfsi9beIJovC2p4,3493
29
- codeanalyzer/neo4j/project.py,sha256=8MRLhlW0tW-hkGvTwifPonyfa0rDocCN2dzokUsFTKY,23620
29
+ codeanalyzer/neo4j/project.py,sha256=HbrehiVmVxLtoDCZkUVFpCOEWQuZR1ezr3mrp1EFnyo,24332
30
30
  codeanalyzer/neo4j/rows.py,sha256=med0XFa63PPUVUhJMq1TtVcJxgaVW39sgxBRZJDIJ_w,7811
31
31
  codeanalyzer/neo4j/schema.py,sha256=SyjaME5z9vgBC70Aswksct8Z0Tiau1PNvbHoeT3JLxE,10582
32
32
  codeanalyzer/options/__init__.py,sha256=Ki4qhHFqpyuUWVsntO-NYJMVWrkeFOzPW4nQ7oxiUVI,155
@@ -41,19 +41,19 @@ codeanalyzer/schema/py_schema.py,sha256=KesvWIODJTl-kVMt1fYvw9yVQNWA0tMsLApqmb9Z
41
41
  codeanalyzer/semantic_analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
42
  codeanalyzer/semantic_analysis/call_graph.py,sha256=PuR7dFTVrKanPatomYDJgkBaJq1Fd9920KspQpHQl1U,10957
43
43
  codeanalyzer/semantic_analysis/pycg/__init__.py,sha256=Lsgz25iFM_RGGu_i2psY-LN-KwvYIZQPnKgRPL1JsjU,928
44
- codeanalyzer/semantic_analysis/pycg/pycg_analysis.py,sha256=paUvKHqTNggo7c7-PTyBdDnPqa8g81_SlVDZbcquEzA,45109
44
+ codeanalyzer/semantic_analysis/pycg/pycg_analysis.py,sha256=u22ZbicZ8_uTBNhbO4WepiO4ZcKB4OnarcOFh5xMido,48081
45
45
  codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py,sha256=n4tRderrSYw9cUYgR2wsn64UqozY56nD1YfDNnaVcAk,968
46
46
  codeanalyzer/semantic_analysis/pycg/shard_planner.py,sha256=7wz821fv18vojzOHFYhXB6MLE1KdU4kFaldbd2kZHt0,15837
47
47
  codeanalyzer/syntactic_analysis/__init__.py,sha256=EUQkJEh6wHjWx2qTTKbTbUgwSbfKeNieKHNy7RknVXA,476
48
48
  codeanalyzer/syntactic_analysis/exceptions.py,sha256=whs_n0vIu655Jkk1a7iOoXY6iIca4pZqJnU40V9Ejaw,537
49
49
  codeanalyzer/syntactic_analysis/import_resolver.py,sha256=Q8noZwSdDNt4P4NMqDTB9FaS-M0_d4ASK9GnJop9MDI,2751
50
- codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=-fMkDcbMknkKTZrrYVfpF5CDqwK3TkhgkMxS4qN6WTk,42240
50
+ codeanalyzer/syntactic_analysis/symbol_table_builder.py,sha256=US09mQgyaCbaF72YOtZF5NkJv1UM881c0dT9DSBpqXY,43398
51
51
  codeanalyzer/utils/__init__.py,sha256=hC6VWdR5rerSqBxzu9KQHTASWqwrrYJv-CMDwrTlzkc,137
52
52
  codeanalyzer/utils/logging.py,sha256=Fw0tattPAOMs3o0JMjjXhRVLIF64f-SCcygUXF9jqeg,904
53
53
  codeanalyzer/utils/progress_bar.py,sha256=C9JtzVdd10lIxTv-KA6PebqjKWueC_vMGwVzAtHuHIw,2818
54
- codeanalyzer_python-1.0.0.dist-info/METADATA,sha256=vJ6d7LeO0LrqX7YL89TbS8J-YFB3Jm13-4NZPVYFBB0,46764
55
- codeanalyzer_python-1.0.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
56
- codeanalyzer_python-1.0.0.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
57
- codeanalyzer_python-1.0.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
58
- codeanalyzer_python-1.0.0.dist-info/licenses/NOTICE,sha256=YU0Z9NDWqKY-2jfFcbxeZ6fbnzz0oZeKmnUcO8a-bcQ,901
59
- codeanalyzer_python-1.0.0.dist-info/RECORD,,
54
+ codeanalyzer_python-1.0.2.dist-info/METADATA,sha256=A3SifmcHHDEgR1xXeIoGc_e5RMnRZGqGUNxDiSdaV_A,46764
55
+ codeanalyzer_python-1.0.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
56
+ codeanalyzer_python-1.0.2.dist-info/entry_points.txt,sha256=v4Vux0Nnx7sOntVk_CH7W9RX6SkIkvR1FQYq73oVlCQ,105
57
+ codeanalyzer_python-1.0.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
58
+ codeanalyzer_python-1.0.2.dist-info/licenses/NOTICE,sha256=YU0Z9NDWqKY-2jfFcbxeZ6fbnzz0oZeKmnUcO8a-bcQ,901
59
+ codeanalyzer_python-1.0.2.dist-info/RECORD,,