archview 0.2.3__tar.gz → 0.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: archview
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Interactive live architecture viewer for Python projects
5
5
  Author-email: Lorenzo Mirante <lorenzomirante@outlook.com>
6
6
  License: MIT
@@ -144,6 +144,8 @@ def _build_module_index(
144
144
  mod = rel.replace("/", ".").removesuffix(ext)
145
145
  if mod.endswith(".__init__"):
146
146
  mod = mod[:-9]
147
+ if not mod:
148
+ continue
147
149
  modules[mod] = full
148
150
  module_rel[mod] = rel
149
151
  return modules, module_rel
@@ -158,7 +160,7 @@ def _parse_modules(modules: dict[str, Path]):
158
160
 
159
161
  for mod, path in modules.items():
160
162
  try:
161
- tree = ast.parse(path.read_text())
163
+ tree = ast.parse(path.read_text(), filename=str(path))
162
164
  parsed[mod] = tree
163
165
  except SyntaxError as e:
164
166
  parse_errors[mod] = f"SyntaxError: {e.msg} (line {e.lineno})"
@@ -515,9 +517,7 @@ def _build_elements(
515
517
  for node in all_nodes:
516
518
  rel = module_rel.get(node, "")
517
519
  raw_name = Path(rel).name if rel else node.split(".")[-1]
518
- filename = (
519
- node.split(".")[-1] + ".py" if raw_name == "__init__.py" else raw_name
520
- )
520
+ filename = node.split(".")[-1] if raw_name == "__init__.py" else raw_name
521
521
  if node in parse_errors:
522
522
  bg, fg = NODE_COLORS["error"]
523
523
  label = "\u26a0 " + filename
@@ -626,7 +626,9 @@ def generate_graph_json(project_dir: Path, ignore_file: Path | None) -> list[dic
626
626
  parts = node.split(".")
627
627
  if len(parts) > 1:
628
628
  candidate = ".".join(parts[:-1])
629
- if candidate not in all_nodes_set:
629
+ # leading-dot hidden files (e.g. .eslintrc.json) produce an empty
630
+ # candidate; skip to avoid emitting an element with id ""
631
+ if candidate and candidate not in all_nodes_set:
630
632
  folder_ids.add(candidate)
631
633
 
632
634
  all_containers = all_nodes_set | folder_ids
@@ -358,6 +358,7 @@
358
358
  <span id="status">Loading…</span>
359
359
  <button id="btn-fit">Fit</button>
360
360
  <button id="btn-layout">Reset layout</button>
361
+ <button id="btn-folders">Collapse all</button>
361
362
  <button id="btn-png">PNG</button>
362
363
  <button id="btn-save" class="primary">Save</button>
363
364
  </div>
@@ -601,7 +602,10 @@ const cy = cytoscape({
601
602
  });
602
603
 
603
604
  cy.on('dragfree', 'node', evt => {
604
- userPositions[evt.target.id()] = { ...evt.target.position() };
605
+ const id = evt.target.id();
606
+ // expanded compounds have positions derived from children — don't persist
607
+ if (compoundNodes.has(id) && expandedFolders.has(id)) return;
608
+ userPositions[id] = { ...evt.target.position() };
605
609
  });
606
610
 
607
611
  // ── Folder collapse / expand ──────────────────────────────────────────────
@@ -731,6 +735,8 @@ function toggleFolder(id) {
731
735
  userPositions[childId].x += dx;
732
736
  userPositions[childId].y += dy;
733
737
  }
738
+ const sp = savedPositions.get(childId);
739
+ if (sp) { sp.x += dx; sp.y += dy; }
734
740
  }
735
741
  }
736
742
  collapsePositions.delete(id);
@@ -741,8 +747,70 @@ function toggleFolder(id) {
741
747
  }
742
748
  updateFolderLabels();
743
749
  applyCollapse();
750
+ if (expandedFolders.has(id)) applyGridInsideFolders(id);
744
751
  cy.nodes(':hidden').forEach(n => selectedNodes.delete(n.id()));
745
752
  applyFocus();
753
+ updateFolderButtonLabel();
754
+ }
755
+
756
+ function updateFolderButtonLabel() {
757
+ const btn = document.getElementById('btn-folders');
758
+ if (!btn) return;
759
+ if (compoundNodes.size === 0) {
760
+ btn.style.display = 'none';
761
+ return;
762
+ }
763
+ btn.style.display = '';
764
+ const anyExpanded = [...compoundNodes].some(id => expandedFolders.has(id));
765
+ btn.textContent = anyExpanded ? 'Collapse all' : 'Expand all';
766
+ }
767
+
768
+ function applyGridInsideFolders(targetId = null) {
769
+ const STEP_X = 170;
770
+ const STEP_Y = 60;
771
+ let ids;
772
+ if (targetId) {
773
+ // include target plus any expanded compound descendants
774
+ ids = [targetId];
775
+ const stack = [targetId];
776
+ while (stack.length) {
777
+ const cur = stack.pop();
778
+ const node = cy.getElementById(cur);
779
+ if (!node.length) continue;
780
+ node.children().forEach(child => {
781
+ const cid = child.id();
782
+ if (compoundNodes.has(cid) && expandedFolders.has(cid)) {
783
+ ids.push(cid);
784
+ stack.push(cid);
785
+ }
786
+ });
787
+ }
788
+ } else {
789
+ ids = [...compoundNodes];
790
+ }
791
+ ids.forEach(folderId => {
792
+ if (!expandedFolders.has(folderId)) return;
793
+ const folder = cy.getElementById(folderId);
794
+ if (!folder.length) return;
795
+ const children = folder.children().filter(n => n.visible());
796
+ const count = children.length;
797
+ if (count <= 1) return;
798
+ if (children.some(c => userPositions[c.id()])) return;
799
+ const cols = Math.ceil(Math.sqrt(count));
800
+ const rows = Math.ceil(count / cols);
801
+ let sumX = 0, sumY = 0;
802
+ children.forEach(c => { sumX += c.position().x; sumY += c.position().y; });
803
+ const cx = sumX / count;
804
+ const cy_ = sumY / count;
805
+ children.forEach((child, i) => {
806
+ const col = i % cols;
807
+ const row = Math.floor(i / cols);
808
+ child.position({
809
+ x: cx + (col - (cols - 1) / 2) * STEP_X,
810
+ y: cy_ + (row - (rows - 1) / 2) * STEP_Y,
811
+ });
812
+ });
813
+ });
746
814
  }
747
815
 
748
816
  // ── Node focus ───────────────────────────────────────────────────────────
@@ -897,9 +965,9 @@ function runLayout(fit, newNodeIds = new Set()) {
897
965
  cy.layout({
898
966
  name: 'dagre',
899
967
  rankDir: 'LR',
900
- nodeSep: 20,
901
- rankSep: 80,
902
- edgeSep: 10,
968
+ nodeSep: 10,
969
+ rankSep: 50,
970
+ edgeSep: 8,
903
971
  compound: true,
904
972
  animate: !firstLoad,
905
973
  animationDuration: 400,
@@ -912,6 +980,7 @@ function runLayout(fit, newNodeIds = new Set()) {
912
980
  }
913
981
  }).run();
914
982
  applyCollapse();
983
+ applyGridInsideFolders();
915
984
  applyFocus();
916
985
  }
917
986
 
@@ -979,6 +1048,7 @@ function reconcileGraph({ nodeIds, edgeIds, nodeMap, edgeMap }) {
979
1048
  function trackCompoundNodes(elements) {
980
1049
  compoundNodes.clear();
981
1050
  for (const el of elements) {
1051
+ if (el.data.source) continue; // edges
982
1052
  if (el.data.parent) compoundNodes.add(el.data.parent);
983
1053
  if (el.data.is_folder) compoundNodes.add(el.data.id);
984
1054
  }
@@ -1019,6 +1089,7 @@ async function refresh() {
1019
1089
  runLayout(firstLoad, new Set(addedNodes));
1020
1090
  if (firstLoad) updateFolderLabels();
1021
1091
  firstLoad = false;
1092
+ updateFolderButtonLabel();
1022
1093
  } else {
1023
1094
  applyCollapse();
1024
1095
  applyFocus();
@@ -1056,6 +1127,50 @@ document.getElementById('btn-layout').addEventListener('click', () => {
1056
1127
  runLayout(true, new Set(cy.nodes().map(n => n.id())));
1057
1128
  });
1058
1129
 
1130
+ document.getElementById('btn-folders').addEventListener('click', () => {
1131
+ const anyExpanded = [...compoundNodes].some(id => expandedFolders.has(id));
1132
+ if (anyExpanded) {
1133
+ compoundNodes.forEach(id => {
1134
+ if (expandedFolders.has(id)) {
1135
+ const node = cy.getElementById(id);
1136
+ if (node.length) collapsePositions.set(id, { ...node.position() });
1137
+ expandedFolders.delete(id);
1138
+ }
1139
+ });
1140
+ } else {
1141
+ compoundNodes.forEach(id => {
1142
+ if (!expandedFolders.has(id)) {
1143
+ const savedPos = collapsePositions.get(id);
1144
+ const node = cy.getElementById(id);
1145
+ if (node.length && savedPos) {
1146
+ const cur = node.position();
1147
+ const dx = cur.x - savedPos.x;
1148
+ const dy = cur.y - savedPos.y;
1149
+ if (dx !== 0 || dy !== 0) {
1150
+ for (const childId of getDescendants(id)) {
1151
+ if (userPositions[childId]) {
1152
+ userPositions[childId].x += dx;
1153
+ userPositions[childId].y += dy;
1154
+ }
1155
+ const sp = savedPositions.get(childId);
1156
+ if (sp) { sp.x += dx; sp.y += dy; }
1157
+ }
1158
+ }
1159
+ collapsePositions.delete(id);
1160
+ }
1161
+ delete userPositions[id];
1162
+ expandedFolders.add(id);
1163
+ }
1164
+ });
1165
+ }
1166
+ updateFolderLabels();
1167
+ applyCollapse();
1168
+ applyGridInsideFolders();
1169
+ cy.nodes(':hidden').forEach(n => selectedNodes.delete(n.id()));
1170
+ applyFocus();
1171
+ updateFolderButtonLabel();
1172
+ });
1173
+
1059
1174
  document.getElementById('btn-fit').addEventListener('click', () => cy.fit(48));
1060
1175
 
1061
1176
  document.getElementById('btn-png').addEventListener('click', () => {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: archview
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Interactive live architecture viewer for Python projects
5
5
  Author-email: Lorenzo Mirante <lorenzomirante@outlook.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "archview"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Interactive live architecture viewer for Python projects"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes