graphlens-python 0.1.1__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.
@@ -0,0 +1,140 @@
1
+ """Python project detection: marker files and project name extraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import configparser
6
+ from typing import TYPE_CHECKING
7
+
8
+ import tomllib
9
+
10
+ if TYPE_CHECKING:
11
+ from pathlib import Path
12
+
13
+ PYTHON_MARKERS: tuple[str, ...] = (
14
+ "pyproject.toml",
15
+ "setup.py",
16
+ "setup.cfg",
17
+ "Pipfile",
18
+ "requirements.txt",
19
+ )
20
+
21
+ _EXCLUDED_DIRS: frozenset[str] = frozenset({
22
+ ".venv", "venv", "__pycache__", ".git",
23
+ "dist", "build", ".eggs", "node_modules",
24
+ })
25
+
26
+
27
+ def is_python_project(project_root: Path) -> bool:
28
+ """
29
+ Return True if the directory contains Python source files.
30
+
31
+ Detection order:
32
+ 1. Python-specific marker files (pyproject.toml, setup.py, etc.)
33
+ 2. Fallback: any .py file exists anywhere under project_root
34
+
35
+ The fallback handles multi-language projects (e.g. a monorepo root
36
+ that has no Python markers but contains Python sub-packages alongside
37
+ JS/Rust code). For pyproject.toml, also verifies the file contains a
38
+ [project] section to avoid false positives from Rust projects that
39
+ use pyproject.toml for tools.
40
+ """
41
+ if _has_python_markers(project_root):
42
+ return True
43
+ # Fallback: presence of any .py file is enough
44
+ return any(project_root.rglob("*.py"))
45
+
46
+
47
+ def find_python_roots(search_root: Path) -> list[Path]:
48
+ """
49
+ Find the actual Python project roots within search_root.
50
+
51
+ If search_root itself has Python markers, returns ``[search_root]``.
52
+ Otherwise walks subdirectories for marker files and returns their parent
53
+ directories — one per distinct Python sub-project. This ensures that
54
+ ``detect_project_name`` and source-root resolution use the *correct* root
55
+ rather than the monorepo root, giving accurate import mappings.
56
+
57
+ Falls back to ``[search_root]`` when no markers are found anywhere (the
58
+ directory contains only bare .py scripts with no packaging metadata).
59
+ """
60
+ if _has_python_markers(search_root):
61
+ return [search_root]
62
+
63
+ roots: list[Path] = []
64
+ for marker in PYTHON_MARKERS:
65
+ for marker_file in sorted(search_root.rglob(marker)):
66
+ rel_parts = marker_file.relative_to(search_root).parts
67
+ if _EXCLUDED_DIRS & set(rel_parts):
68
+ continue
69
+ if marker == "pyproject.toml" and not (
70
+ _pyproject_has_project_section(marker_file)
71
+ ):
72
+ continue
73
+ candidate = marker_file.parent
74
+ # Skip if already covered by a previously found (ancestor) root
75
+ if any(
76
+ candidate == r or candidate.is_relative_to(r)
77
+ for r in roots
78
+ ):
79
+ continue
80
+ roots.append(candidate)
81
+
82
+ return sorted(roots) if roots else [search_root]
83
+
84
+
85
+ def detect_project_name(project_root: Path) -> str:
86
+ """
87
+ Extract the project name.
88
+
89
+ Resolution order:
90
+ 1. pyproject.toml [project].name
91
+ 2. setup.cfg [metadata] name
92
+ 3. project_root directory name
93
+ """
94
+ pyproject = project_root / "pyproject.toml"
95
+ if pyproject.exists():
96
+ try:
97
+ with pyproject.open("rb") as f:
98
+ data = tomllib.load(f)
99
+ name = data.get("project", {}).get("name")
100
+ if name:
101
+ return str(name)
102
+ except (tomllib.TOMLDecodeError, OSError):
103
+ pass
104
+
105
+ setup_cfg = project_root / "setup.cfg"
106
+ if setup_cfg.exists():
107
+ try:
108
+ cfg = configparser.ConfigParser()
109
+ cfg.read(setup_cfg)
110
+ name = cfg.get("metadata", "name", fallback=None)
111
+ if name:
112
+ return name.strip()
113
+ except (configparser.Error, OSError):
114
+ pass
115
+
116
+ return project_root.name
117
+
118
+
119
+ def _has_python_markers(directory: Path) -> bool:
120
+ """Return True if directory contains Python project marker files."""
121
+ for marker in PYTHON_MARKERS:
122
+ path = directory / marker
123
+ if not path.exists():
124
+ continue
125
+ if marker == "pyproject.toml":
126
+ if _pyproject_has_project_section(path):
127
+ return True
128
+ else:
129
+ return True
130
+ return False
131
+
132
+
133
+ def _pyproject_has_project_section(path: Path) -> bool:
134
+ try:
135
+ with path.open("rb") as f:
136
+ data = tomllib.load(f)
137
+ return "project" in data
138
+ except (tomllib.TOMLDecodeError, OSError):
139
+ # If we can't parse it, check if .py files exist nearby as a fallback
140
+ return any(path.parent.rglob("*.py"))