codedocent 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 (38) hide show
  1. {codedocent-1.0.2/codedocent.egg-info → codedocent-1.0.3}/PKG-INFO +21 -3
  2. codedocent-1.0.2/PKG-INFO → codedocent-1.0.3/README.md +10 -10
  3. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/graph.py +45 -5
  4. codedocent-1.0.2/README.md → codedocent-1.0.3/codedocent.egg-info/PKG-INFO +28 -0
  5. {codedocent-1.0.2 → codedocent-1.0.3}/pyproject.toml +1 -1
  6. {codedocent-1.0.2 → codedocent-1.0.3}/tests/test_graph.py +100 -0
  7. {codedocent-1.0.2 → codedocent-1.0.3}/LICENSE +0 -0
  8. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/__init__.py +0 -0
  9. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/__main__.py +0 -0
  10. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/analyzer.py +0 -0
  11. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/cli.py +0 -0
  12. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/cloud_ai.py +0 -0
  13. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/editor.py +0 -0
  14. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/gui.py +0 -0
  15. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/ollama_utils.py +0 -0
  16. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/parser.py +0 -0
  17. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/quality.py +0 -0
  18. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/renderer.py +0 -0
  19. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/scanner.py +0 -0
  20. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/server.py +0 -0
  21. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/templates/architecture.html +0 -0
  22. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/templates/base.html +0 -0
  23. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent/templates/interactive.html +0 -0
  24. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent.egg-info/SOURCES.txt +0 -0
  25. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent.egg-info/dependency_links.txt +0 -0
  26. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent.egg-info/entry_points.txt +0 -0
  27. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent.egg-info/requires.txt +4 -4
  28. {codedocent-1.0.2 → codedocent-1.0.3}/codedocent.egg-info/top_level.txt +0 -0
  29. {codedocent-1.0.2 → codedocent-1.0.3}/setup.cfg +0 -0
  30. {codedocent-1.0.2 → codedocent-1.0.3}/tests/test_analyzer.py +0 -0
  31. {codedocent-1.0.2 → codedocent-1.0.3}/tests/test_cli.py +0 -0
  32. {codedocent-1.0.2 → codedocent-1.0.3}/tests/test_cloud_ai.py +0 -0
  33. {codedocent-1.0.2 → codedocent-1.0.3}/tests/test_editor.py +0 -0
  34. {codedocent-1.0.2 → codedocent-1.0.3}/tests/test_gui.py +0 -0
  35. {codedocent-1.0.2 → codedocent-1.0.3}/tests/test_parser.py +0 -0
  36. {codedocent-1.0.2 → codedocent-1.0.3}/tests/test_renderer.py +0 -0
  37. {codedocent-1.0.2 → codedocent-1.0.3}/tests/test_scanner.py +0 -0
  38. {codedocent-1.0.2 → codedocent-1.0.3}/tests/test_server.py +0 -0
@@ -1,12 +1,20 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: codedocent
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Code visualization for non-programmers
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
- Provides-Extra: dev
9
8
  License-File: LICENSE
9
+ Requires-Dist: tree-sitter>=0.23
10
+ Requires-Dist: tree-sitter-language-pack>=0.13
11
+ Requires-Dist: radon>=6.0
12
+ Requires-Dist: pathspec>=0.11
13
+ Requires-Dist: jinja2>=3.1
14
+ Requires-Dist: ollama>=0.4
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=7.0; extra == "dev"
17
+ Dynamic: license-file
10
18
 
11
19
  # codedocent
12
20
 
@@ -87,6 +95,16 @@ codedocent /path/to/code --cloud groq # use Groq
87
95
  codedocent /path/to/code --cloud custom --endpoint https://my-llm/v1/chat/completions
88
96
  ```
89
97
 
98
+ ## GUI launcher
99
+
100
+ ![GUI launcher with folder picker, AI backend toggle, model dropdown, and mode selector](https://raw.githubusercontent.com/clanker-lover/codedocent/main/docs/screenshots/gui-launcher.png)
101
+
102
+ If you prefer clicking over typing, `codedocent --gui` opens a graphical launcher. Pick a folder, choose your AI backend (cloud or local Ollama), select a model, and choose a mode — Interactive, Full export, Text tree, or Architecture. Hit Go.
103
+
104
+ ```bash
105
+ codedocent --gui
106
+ ```
107
+
90
108
  ## How it works
91
109
 
92
110
  Parses code structure with tree-sitter, scores quality with static analysis, and sends individual blocks to a cloud AI provider or local Ollama model for plain English summaries and pseudocode. Interactive mode analyzes on click — typically 1-2 seconds per block. Full mode analyzes everything upfront into a self-contained HTML file you can share. Architecture mode builds a dependency graph from import statements and renders it as a zoomable D3 visualization.
@@ -1,13 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: codedocent
3
- Version: 1.0.2
4
- Summary: Code visualization for non-programmers
5
- License: MIT
6
- Requires-Python: >=3.10
7
- Description-Content-Type: text/markdown
8
- Provides-Extra: dev
9
- License-File: LICENSE
10
-
11
1
  # codedocent
12
2
 
13
3
  **Code visualization for non-programmers.**
@@ -87,6 +77,16 @@ codedocent /path/to/code --cloud groq # use Groq
87
77
  codedocent /path/to/code --cloud custom --endpoint https://my-llm/v1/chat/completions
88
78
  ```
89
79
 
80
+ ## GUI launcher
81
+
82
+ ![GUI launcher with folder picker, AI backend toggle, model dropdown, and mode selector](https://raw.githubusercontent.com/clanker-lover/codedocent/main/docs/screenshots/gui-launcher.png)
83
+
84
+ If you prefer clicking over typing, `codedocent --gui` opens a graphical launcher. Pick a folder, choose your AI backend (cloud or local Ollama), select a model, and choose a mode — Interactive, Full export, Text tree, or Architecture. Hit Go.
85
+
86
+ ```bash
87
+ codedocent --gui
88
+ ```
89
+
90
90
  ## How it works
91
91
 
92
92
  Parses code structure with tree-sitter, scores quality with static analysis, and sends individual blocks to a cloud AI provider or local Ollama model for plain English summaries and pseudocode. Interactive mode analyzes on click — typically 1-2 seconds per block. Full mode analyzes everything upfront into a self-contained HTML file you can share. Architecture mode builds a dependency graph from import statements and renders it as a zoomable D3 visualization.
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import ast
5
6
  import os
6
7
  import sys
7
8
  from dataclasses import dataclass, field
@@ -389,9 +390,11 @@ def get_module_graph(root: CodeNode, project_root: str) -> dict:
389
390
  imp, fnode.filepath, project_files, project_modules_top,
390
391
  )
391
392
  if resolved is None:
392
- # External dependency
393
+ # External dependency (skip stdlib)
393
394
  if not imp.is_relative:
394
- external_deps.add(imp.module.split(".")[0])
395
+ top = imp.module.split(".")[0]
396
+ if top not in _get_stdlib_modules():
397
+ external_deps.add(top)
395
398
  continue
396
399
  if resolved != fnode.filepath:
397
400
  key = (fnode.filepath, resolved)
@@ -582,6 +585,30 @@ def get_file_dependencies(
582
585
  }
583
586
 
584
587
 
588
+ # ---------------------------------------------------------------------------
589
+ # Docstring extraction
590
+ # ---------------------------------------------------------------------------
591
+
592
+
593
+ def _extract_module_docstring(source: str) -> str:
594
+ """Extract the module-level docstring from Python source.
595
+
596
+ Returns the first line of the docstring, truncated to 80 characters.
597
+ Returns an empty string if no docstring is found.
598
+ """
599
+ try:
600
+ tree = ast.parse(source)
601
+ docstring = ast.get_docstring(tree)
602
+ if docstring:
603
+ first_line = docstring.split("\n")[0].strip()
604
+ if len(first_line) > 80:
605
+ return first_line[:77] + "..."
606
+ return first_line
607
+ except SyntaxError:
608
+ pass
609
+ return ""
610
+
611
+
585
612
  # ---------------------------------------------------------------------------
586
613
  # Context export
587
614
  # ---------------------------------------------------------------------------
@@ -637,20 +664,33 @@ def export_module_md(
637
664
  if graph is None:
638
665
  return None
639
666
 
667
+ # Build filepath -> docstring lookup for Purpose column
668
+ docstrings: dict[str, str] = {}
669
+ for node in graph["nodes"]:
670
+ if node["node_type"] == "external":
671
+ continue
672
+ fnode = _find_file_node(root, node["id"])
673
+ if fnode and fnode.source and fnode.language == "python":
674
+ docstrings[node["id"]] = _extract_module_docstring(fnode.source)
675
+
640
676
  mod_name = os.path.basename(module_path)
641
677
  lines = [f"# Module: {mod_name}\n"]
642
678
 
643
679
  # Files table
644
680
  lines.append("## Files\n")
645
- lines.append("| File | Lines | Quality |")
646
- lines.append("|------|-------|---------|")
681
+ lines.append("| File | Lines | Quality | Purpose |")
682
+ lines.append("|------|-------|---------|---------|")
647
683
  for node in graph["nodes"]:
648
684
  if node["node_type"] == "external":
649
685
  continue
650
686
  quality_label = {"clean": "OK", "complex": "Complex", "warning": "Warning"}.get(
651
687
  node["quality"], "OK"
652
688
  )
653
- lines.append(f"| {node['name']} | {node['line_count']} | {quality_label} |")
689
+ purpose = docstrings.get(node["id"], "") or "\u2014"
690
+ lines.append(
691
+ f"| {node['name']} | {node['line_count']} "
692
+ f"| {quality_label} | {purpose} |"
693
+ )
654
694
 
655
695
  # Internal dependencies
656
696
  internal_edges = [
@@ -1,3 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: codedocent
3
+ Version: 1.0.3
4
+ Summary: Code visualization for non-programmers
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: tree-sitter>=0.23
10
+ Requires-Dist: tree-sitter-language-pack>=0.13
11
+ Requires-Dist: radon>=6.0
12
+ Requires-Dist: pathspec>=0.11
13
+ Requires-Dist: jinja2>=3.1
14
+ Requires-Dist: ollama>=0.4
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=7.0; extra == "dev"
17
+ Dynamic: license-file
18
+
1
19
  # codedocent
2
20
 
3
21
  **Code visualization for non-programmers.**
@@ -77,6 +95,16 @@ codedocent /path/to/code --cloud groq # use Groq
77
95
  codedocent /path/to/code --cloud custom --endpoint https://my-llm/v1/chat/completions
78
96
  ```
79
97
 
98
+ ## GUI launcher
99
+
100
+ ![GUI launcher with folder picker, AI backend toggle, model dropdown, and mode selector](https://raw.githubusercontent.com/clanker-lover/codedocent/main/docs/screenshots/gui-launcher.png)
101
+
102
+ If you prefer clicking over typing, `codedocent --gui` opens a graphical launcher. Pick a folder, choose your AI backend (cloud or local Ollama), select a model, and choose a mode — Interactive, Full export, Text tree, or Architecture. Hit Go.
103
+
104
+ ```bash
105
+ codedocent --gui
106
+ ```
107
+
80
108
  ## How it works
81
109
 
82
110
  Parses code structure with tree-sitter, scores quality with static analysis, and sends individual blocks to a cloud AI provider or local Ollama model for plain English summaries and pseudocode. Interactive mode analyzes on click — typically 1-2 seconds per block. Full mode analyzes everything upfront into a self-contained HTML file you can share. Architecture mode builds a dependency graph from import statements and renders it as a zoomable D3 visualization.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codedocent"
7
- version = "1.0.2"
7
+ version = "1.0.3"
8
8
  description = "Code visualization for non-programmers"
9
9
  license = {text = "MIT"}
10
10
  readme = "README.md"
@@ -9,6 +9,7 @@ import pytest
9
9
  from codedocent.graph import (
10
10
  ImportInfo,
11
11
  _extract_full_imports,
12
+ _extract_module_docstring,
12
13
  _resolve_import,
13
14
  _find_modules,
14
15
  _aggregate_quality,
@@ -408,6 +409,14 @@ class TestGetModuleGraph:
408
409
  graph = get_module_graph(root, "/tmp/project")
409
410
  assert "pytest" in graph["external"]
410
411
 
412
+ def test_stdlib_excluded_from_external_deps(self):
413
+ """Stdlib modules should not appear in external dependencies."""
414
+ root = _make_multi_module_tree()
415
+ graph = get_module_graph(root, "/tmp/project")
416
+ stdlib_names = {"os", "sys", "json", "pathlib", "collections", "re"}
417
+ for dep in graph["external"]:
418
+ assert dep not in stdlib_names, f"stdlib module '{dep}' in external deps"
419
+
411
420
  def test_module_stats(self):
412
421
  root = _make_multi_module_tree()
413
422
  graph = get_module_graph(root, "/tmp/project")
@@ -485,6 +494,45 @@ class TestGetFileGraph:
485
494
  assert gn["node_id"] in lookup
486
495
 
487
496
 
497
+ # ---------------------------------------------------------------------------
498
+ # Docstring extraction tests
499
+ # ---------------------------------------------------------------------------
500
+
501
+
502
+ class TestExtractModuleDocstring:
503
+ """Tests for _extract_module_docstring."""
504
+
505
+ def test_extracts_triple_double_quote_docstring(self):
506
+ source = '"""Parse source files into a tree."""\nimport os\n'
507
+ assert _extract_module_docstring(source) == "Parse source files into a tree."
508
+
509
+ def test_extracts_triple_single_quote_docstring(self):
510
+ source = "'''Helper utilities for scanning.'''\nimport sys\n"
511
+ assert _extract_module_docstring(source) == "Helper utilities for scanning."
512
+
513
+ def test_returns_empty_for_no_docstring(self):
514
+ source = "import os\nx = 1\n"
515
+ assert _extract_module_docstring(source) == ""
516
+
517
+ def test_returns_first_line_of_multiline(self):
518
+ source = '"""First line.\n\nMore detail here.\n"""\nimport os\n'
519
+ assert _extract_module_docstring(source) == "First line."
520
+
521
+ def test_truncates_long_docstring(self):
522
+ long_text = "A" * 100
523
+ source = f'"""{long_text}"""\n'
524
+ result = _extract_module_docstring(source)
525
+ assert len(result) <= 80
526
+ assert result.endswith("...")
527
+
528
+ def test_returns_empty_for_syntax_error(self):
529
+ source = "def foo(:\n"
530
+ assert _extract_module_docstring(source) == ""
531
+
532
+ def test_returns_empty_for_empty_source(self):
533
+ assert _extract_module_docstring("") == ""
534
+
535
+
488
536
  # ---------------------------------------------------------------------------
489
537
  # Context export tests
490
538
  # ---------------------------------------------------------------------------
@@ -511,6 +559,16 @@ class TestExportArchitectureMd:
511
559
  md = export_architecture_md(root, "/tmp/project")
512
560
  assert "pytest" in md
513
561
 
562
+ def test_excludes_stdlib_from_external_deps(self):
563
+ """Stdlib modules like os, sys should not appear in External Dependencies."""
564
+ root = _make_multi_module_tree()
565
+ md = export_architecture_md(root, "/tmp/project")
566
+ # The test tree includes files that import os/sys but those are stdlib
567
+ for line in md.splitlines():
568
+ if line.startswith("- "):
569
+ dep = line[2:].strip()
570
+ assert dep not in ("os", "sys", "json", "pathlib")
571
+
514
572
 
515
573
  class TestExportModuleMd:
516
574
  """Tests for export_module_md."""
@@ -522,6 +580,12 @@ class TestExportModuleMd:
522
580
  assert "# Module: codedocent" in md
523
581
  assert "## Files" in md
524
582
 
583
+ def test_contains_purpose_column(self):
584
+ root = _make_multi_module_tree()
585
+ md = export_module_md(root, "/tmp/project", "codedocent")
586
+ assert md is not None
587
+ assert "| File | Lines | Quality | Purpose |" in md
588
+
525
589
  def test_returns_none_for_invalid_module(self):
526
590
  root = _make_multi_module_tree()
527
591
  result = export_module_md(root, "/tmp/project", "nonexistent")
@@ -534,6 +598,42 @@ class TestExportModuleMd:
534
598
  assert "parser.py" in md
535
599
  assert "server.py" in md
536
600
 
601
+ def test_shows_docstring_in_purpose(self):
602
+ """Files with module docstrings should show them in the Purpose column."""
603
+ init_node = _make_file_node(
604
+ name="__init__.py",
605
+ filepath="mypkg/__init__.py",
606
+ source='"""My package init."""\n',
607
+ node_id="init",
608
+ )
609
+ core_node = _make_file_node(
610
+ name="core.py",
611
+ filepath="mypkg/core.py",
612
+ source='"""Core business logic for the app."""\nimport os\nx = 1\n',
613
+ node_id="core",
614
+ )
615
+ nodc_node = _make_file_node(
616
+ name="utils.py",
617
+ filepath="mypkg/utils.py",
618
+ source="import os\nx = 1\n",
619
+ node_id="utils",
620
+ )
621
+ dir_node = _make_dir_node(
622
+ name="mypkg",
623
+ children=[init_node, core_node, nodc_node],
624
+ filepath="/tmp/project/mypkg",
625
+ )
626
+ root = _make_dir_node(
627
+ name="project",
628
+ children=[dir_node],
629
+ filepath="/tmp/project",
630
+ )
631
+ md = export_module_md(root, "/tmp/project", "mypkg")
632
+ assert md is not None
633
+ assert "Core business logic for the app" in md
634
+ # utils.py has no docstring -> em dash
635
+ assert "\u2014" in md
636
+
537
637
 
538
638
  # ---------------------------------------------------------------------------
539
639
  # File dependency lookup tests
File without changes
File without changes
File without changes
@@ -1,9 +1,9 @@
1
+ tree-sitter>=0.23
2
+ tree-sitter-language-pack>=0.13
3
+ radon>=6.0
4
+ pathspec>=0.11
1
5
  jinja2>=3.1
2
6
  ollama>=0.4
3
- pathspec>=0.11
4
- radon>=6.0
5
- tree-sitter-language-pack>=0.13
6
- tree-sitter>=0.23
7
7
 
8
8
  [dev]
9
9
  pytest>=7.0
File without changes
File without changes
File without changes