The text output of `uv tree` into a nested data structure.
(text: str)
| 12 | |
| 13 | |
| 14 | def parse_uv_tree(text: str) -> DependencyTreeNode: |
| 15 | """The text output of `uv tree` into a nested data structure.""" |
| 16 | lines = text.strip().split("\n") |
| 17 | |
| 18 | # Create a virtual root to hold all top-level dependencies |
| 19 | tree = DependencyTreeNode( |
| 20 | name="<root>", version=None, tags=[], dependencies=[] |
| 21 | ) |
| 22 | stack = [(tree, -1)] # (node, level) |
| 23 | |
| 24 | for line in lines: |
| 25 | line = line.rstrip() |
| 26 | if ( |
| 27 | not line |
| 28 | or "Package tree already displayed" in line |
| 29 | or "Package tree is a cycle" in line |
| 30 | ): |
| 31 | continue |
| 32 | |
| 33 | # Calculate indentation level by counting characters before tree symbols |
| 34 | if not any(symbol in line for symbol in ["├──", "└──"]): |
| 35 | level = 0 # Top-level package |
| 36 | else: |
| 37 | # Find the tree symbol position and divide by 4 (standard tree indentation) |
| 38 | for symbol in ["├──", "└──"]: |
| 39 | pos = line.find(symbol) |
| 40 | if pos != -1: |
| 41 | level = (pos // 4) + 1 |
| 42 | break |
| 43 | |
| 44 | # content after tree symbols |
| 45 | content = line.lstrip("│ ├└─").strip() |
| 46 | |
| 47 | # Check for cycle indicator |
| 48 | is_cycle = content.endswith("(*)") |
| 49 | if is_cycle: |
| 50 | content = content[:-3].strip() |
| 51 | |
| 52 | # tags (extras/groups) |
| 53 | tags: list[DependencyTag] = [] |
| 54 | while "(extra:" in content or "(group:" in content: |
| 55 | start = ( |
| 56 | content.rfind("(extra:") |
| 57 | if "(extra:" in content |
| 58 | else content.rfind("(group:") |
| 59 | ) |
| 60 | if start == -1: |
| 61 | break |
| 62 | end = content.find(")", start) |
| 63 | if end == -1: |
| 64 | break |
| 65 | tag_text = content[start + 1 : end] |
| 66 | kind, value = tag_text.split(":", 1) |
| 67 | assert kind == "extra" or kind == "group" |
| 68 | tags.append(DependencyTag(kind=kind, value=value.strip())) |
| 69 | content = content[:start].strip() |
| 70 | |
| 71 | name, version = parse_name_version(content) |
nothing calls this directly
no test coverage detected
searching dependent graphs…