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.
- graphlens_python/__init__.py +5 -0
- graphlens_python/_adapter.py +291 -0
- graphlens_python/_deps.py +191 -0
- graphlens_python/_module_resolver.py +87 -0
- graphlens_python/_project_detector.py +140 -0
- graphlens_python/_visitor.py +734 -0
- graphlens_python-0.1.1.dist-info/METADATA +8 -0
- graphlens_python-0.1.1.dist-info/RECORD +10 -0
- graphlens_python-0.1.1.dist-info/WHEEL +4 -0
- graphlens_python-0.1.1.dist-info/entry_points.txt +3 -0
|
@@ -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"))
|