Skip to content

Commit d139212

Browse files
committed
Commit changes and add service modules
1 parent f05c6ca commit d139212

10 files changed

Lines changed: 1108 additions & 4962 deletions

File tree

Core/Services/DependencyService.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2026 Ague Samuel Amen
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from __future__ import annotations
17+
18+
import ast
19+
import functools
20+
import importlib.util
21+
import json
22+
import os
23+
import re
24+
import sys
25+
import sysconfig
26+
from pathlib import Path
27+
from typing import Any, List, Set, Tuple
28+
29+
import yaml
30+
from importlib.metadata import distribution, PackageNotFoundError
31+
32+
class DependencyService:
33+
"""Business logic for dependency analysis."""
34+
35+
@staticmethod
36+
def _default_excluded_stdlib() -> set[str]:
37+
return {
38+
"sys", "os", "re", "subprocess", "json", "math", "time", "pathlib", "typing",
39+
"itertools", "functools", "collections", "asyncio", "importlib", "inspect",
40+
"logging", "argparse", "dataclasses", "unittest", "threading", "multiprocessing",
41+
"http", "urllib", "email", "socket", "ssl", "hashlib", "hmac", "gzip", "bz2",
42+
"lzma", "base64", "shutil", "tempfile", "glob", "fnmatch", "statistics", "pprint",
43+
"getpass", "uuid", "enum", "contextlib", "queue", "traceback", "warnings", "gc",
44+
"platform", "sysconfig", "pkgutil", "site", "venv", "sqlite3", "tkinter",
45+
}
46+
47+
@staticmethod
48+
def _load_excluded_stdlib() -> set[str]:
49+
default = DependencyService._default_excluded_stdlib()
50+
# Mapping file still in the old location for now, but should ideally move to Core/Services/data
51+
mapping_path = os.path.join(os.path.dirname(__file__), "..", "..", "Ui", "Gui", "DepsAnalyser", "stblib.yml")
52+
if not os.path.isfile(mapping_path):
53+
# Try absolute path from project root
54+
mapping_path = os.path.join(os.getcwd(), "Ui", "Gui", "DepsAnalyser", "stblib.yml")
55+
56+
if not os.path.isfile(mapping_path):
57+
return default
58+
try:
59+
with open(mapping_path, encoding="utf-8") as f:
60+
data = yaml.safe_load(f)
61+
modules = None
62+
if isinstance(data, dict):
63+
modules = data.get("excluded_stdlib")
64+
if modules is None:
65+
modules = data.get("modules")
66+
elif isinstance(data, list):
67+
modules = data
68+
if not isinstance(modules, list):
69+
return default
70+
cleaned = [m.strip() for m in modules if isinstance(m, str) and m.strip()]
71+
return set(cleaned) or default
72+
except Exception:
73+
return default
74+
75+
EXCLUDED_STDLIB = _load_excluded_stdlib()
76+
77+
@staticmethod
78+
@functools.lru_cache(maxsize=256)
79+
def is_stdlib_module(module_name: str) -> bool:
80+
try:
81+
if module_name in DependencyService.EXCLUDED_STDLIB:
82+
return True
83+
if module_name in sys.builtin_module_names:
84+
return True
85+
spec = importlib.util.find_spec(module_name)
86+
if spec is None:
87+
return False
88+
if getattr(spec, "origin", None) in ("built-in", "frozen"):
89+
return True
90+
stdlib_path = sysconfig.get_path("stdlib") or ""
91+
stdlib_path = os.path.realpath(stdlib_path)
92+
candidates = []
93+
if getattr(spec, "origin", None):
94+
candidates.append(os.path.realpath(spec.origin))
95+
for loc in spec.submodule_search_locations or []:
96+
candidates.append(os.path.realpath(loc))
97+
for c in candidates:
98+
try:
99+
if os.path.commonpath([c, stdlib_path]) == stdlib_path:
100+
return True
101+
except Exception:
102+
pass
103+
return False
104+
except Exception:
105+
return False
106+
107+
@staticmethod
108+
def normalize_realpath(path: str | None) -> str:
109+
if not path:
110+
return ""
111+
try:
112+
return os.path.normcase(os.path.realpath(path))
113+
except Exception:
114+
return path
115+
116+
@staticmethod
117+
def is_path_under(path: str, root: str) -> bool:
118+
if not path or not root:
119+
return False
120+
try:
121+
return os.path.commonpath([path, root]) == root
122+
except Exception:
123+
return False
124+
125+
@staticmethod
126+
def normalize_module_name(name: str | None) -> str:
127+
if not name:
128+
return ""
129+
try:
130+
cleaned = re.sub(r"[^A-Za-z0-9_\.]+", "_", str(name).strip())
131+
return cleaned.replace("-", "_").strip("._")
132+
except Exception:
133+
return ""
134+
135+
@staticmethod
136+
def top_level_module_name(name: str | None) -> str:
137+
normalized = DependencyService.normalize_module_name(name)
138+
if not normalized:
139+
return ""
140+
return normalized.split(".")[0]
141+
142+
@staticmethod
143+
def extract_imported_modules_from_source(
144+
source: str, file_path: str = "", workspace_dir: str | None = None
145+
) -> set[str]:
146+
modules: set[str] = set()
147+
try:
148+
tree = ast.parse(source, filename=file_path or "<memory>")
149+
except Exception:
150+
return modules
151+
152+
for node in ast.walk(tree):
153+
if isinstance(node, ast.Import):
154+
for alias in node.names:
155+
top = DependencyService.top_level_module_name(alias.name)
156+
if top:
157+
modules.add(top)
158+
elif isinstance(node, ast.ImportFrom):
159+
if node.level and node.level > 0:
160+
# Circular dependency if we import from WorkspaceService here
161+
# For now, we'll implement a minimal version of relative parts
162+
pass
163+
elif node.module:
164+
top = DependencyService.top_level_module_name(node.module)
165+
if top:
166+
modules.add(top)
167+
168+
dynamic_imports = re.findall(r"__import__\(['\"]([\w\.]+)['\"]\)", source)
169+
modules.update(
170+
[top for top in (DependencyService.top_level_module_name(mod) for mod in dynamic_imports) if top]
171+
)
172+
173+
importlib_imports = re.findall(
174+
r"importlib\.import_module\(['\"]([\w\.]+)['\"]\)", source
175+
)
176+
modules.update(
177+
[
178+
top
179+
for top in (DependencyService.top_level_module_name(mod) for mod in importlib_imports)
180+
if top
181+
]
182+
)
183+
return modules
184+
185+
@staticmethod
186+
def extract_imported_modules_from_file(
187+
file_path: str, workspace_dir: str | None = None
188+
) -> set[str]:
189+
try:
190+
with open(file_path, encoding="utf-8") as f:
191+
source = f.read()
192+
except Exception:
193+
return set()
194+
return DependencyService.extract_imported_modules_from_source(
195+
source,
196+
file_path=file_path,
197+
workspace_dir=workspace_dir,
198+
)
199+
200+
@staticmethod
201+
@functools.lru_cache(maxsize=1024)
202+
def classify_module_origin(module_name: str, workspace_dir: str) -> str:
203+
if not module_name:
204+
return "unknown"
205+
if DependencyService.is_stdlib_module(module_name):
206+
return "stdlib"
207+
try:
208+
ws = DependencyService.normalize_realpath(workspace_dir)
209+
# Minimal internal detection
210+
normalized_module = DependencyService.top_level_module_name(module_name)
211+
if normalized_module:
212+
# Check common source roots
213+
for root in (ws, os.path.join(ws, "src"), os.path.join(ws, "lib")):
214+
try:
215+
pkg_init = os.path.join(root, normalized_module, "__init__.py")
216+
mod_file = os.path.join(root, f"{normalized_module}.py")
217+
if os.path.isfile(pkg_init) or os.path.isfile(mod_file):
218+
return "internal"
219+
except Exception:
220+
continue
221+
222+
spec = importlib.util.find_spec(module_name)
223+
if spec is None:
224+
return "unknown"
225+
origin = getattr(spec, "origin", None)
226+
if origin in ("built-in", "frozen"):
227+
return "stdlib"
228+
229+
candidates: list[str] = []
230+
if origin:
231+
candidates.append(DependencyService.normalize_realpath(origin))
232+
for loc in spec.submodule_search_locations or []:
233+
candidates.append(DependencyService.normalize_realpath(loc))
234+
235+
stdlib_path = DependencyService.normalize_realpath(sysconfig.get_path("stdlib") or "")
236+
purelib_path = DependencyService.normalize_realpath(sysconfig.get_path("purelib") or "")
237+
platlib_path = DependencyService.normalize_realpath(sysconfig.get_path("platlib") or "")
238+
for c in candidates:
239+
if ws and DependencyService.is_path_under(c, ws):
240+
return "internal"
241+
if stdlib_path and DependencyService.is_path_under(c, stdlib_path):
242+
return "stdlib"
243+
if purelib_path and DependencyService.is_path_under(c, purelib_path):
244+
return "third_party"
245+
if platlib_path and DependencyService.is_path_under(c, platlib_path):
246+
return "third_party"
247+
if "site-packages" in c or "dist-packages" in c:
248+
return "third_party"
249+
250+
return "unknown"
251+
except Exception:
252+
return "unknown"
253+
254+
@staticmethod
255+
def check_module_installed(module: str) -> bool:
256+
try:
257+
distribution(module)
258+
return True
259+
except PackageNotFoundError:
260+
return False
261+
except Exception:
262+
return False

0 commit comments

Comments
 (0)