Skip to content

Commit c4a237d

Browse files
committed
final fixes + testing that dep check fails
1 parent 7ca9483 commit c4a237d

File tree

4 files changed

+160
-125
lines changed

4 files changed

+160
-125
lines changed

eng/templates/jobs/ci-dependency-check.yml

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,46 +29,39 @@ jobs:
2929
inputs:
3030
versionSpec: $(PYTHON_VERSION)
3131
- bash: |
32-
cd workers
33-
pip install --no-deps .
34-
3532
PY_VER="$(PYTHON_VERSION)"
3633
echo "Python version: $PY_VER"
3734
3835
# Extract minor version
39-
PY_MINOR="${PY_VER#*.}"
36+
PY_MINOR="${PY_VER#*.}"
4037
4138
if [ "$PY_MINOR" -ge 13 ]; then
42-
python -c "import proxy_worker; print('OK: imports resolved')"
39+
echo "Checking proxy_worker (Python >= 3.13)..."
40+
python workers/tests/unittests/check_imports.py workers proxy_worker
4341
else
44-
python -c "import azure_functions_worker; print('OK: imports resolved')"
42+
echo "Checking azure_functions_worker (Python < 3.13)..."
43+
python workers/tests/unittests/check_imports.py workers azure_functions_worker
4544
fi
4645
displayName: 'Python Worker: check for missing dependencies'
4746
- bash: |
48-
cd runtimes/v1
49-
pip install --no-deps .
50-
5147
PY_VER="$(PYTHON_VERSION)"
5248
echo "Python version: $PY_VER"
5349
5450
# Extract minor version
5551
PY_MINOR="${PY_VER#*.}"
5652
5753
if [ "$PY_MINOR" -ge 13 ]; then
58-
python -c "import azure_functions_runtime_v1; print('OK: imports resolved')"
54+
python workers/tests/unittests/check_imports.py runtimes/v1 azure-functions-runtime-v1
5955
fi
6056
displayName: 'Python Library V1: check for missing dependencies'
6157
- bash: |
62-
cd runtimes/v2
63-
pip install --no-deps .
64-
6558
PY_VER="$(PYTHON_VERSION)"
6659
echo "Python version: $PY_VER"
6760
6861
# Extract minor version
6962
PY_MINOR="${PY_VER#*.}"
7063
7164
if [ "$PY_MINOR" -ge 13 ]; then
72-
python -c "import azure_functions_runtime; print('OK: imports resolved')"
65+
python workers/tests/unittests/check_imports.py runtimes/v2 azure-functions-runtime
7366
fi
7467
displayName: 'Python Library V2: check for missing dependencies'

workers/tests/endtoend/test_basic_http_functions.py

Lines changed: 0 additions & 110 deletions
This file was deleted.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import ast
2+
import pathlib
3+
import tomllib
4+
import sys
5+
6+
IMPORT_TO_PACKAGE = {
7+
"google": "protobuf",
8+
"dateutil": "python_dateutil",
9+
"grpc": "grpcio",
10+
"azure.functions": "azure_functions",
11+
"azurefunctions.extensions.base": "azurefunctions_extensions_base",
12+
}
13+
14+
15+
def normalize_import(import_name):
16+
return IMPORT_TO_PACKAGE.get(import_name, import_name)
17+
18+
19+
def find_local_modules(src_dir):
20+
local = set()
21+
for py in src_dir.rglob("*.py"):
22+
if py.name == "__init__.py":
23+
local.add(py.parent.name)
24+
else:
25+
local.add(py.stem)
26+
return local
27+
28+
29+
def find_imports(src_dir):
30+
imports = set()
31+
for py in src_dir.rglob("*.py"):
32+
with open(py, "r", encoding="utf8") as f:
33+
tree = ast.parse(f.read(), filename=str(py))
34+
35+
for node in ast.walk(tree):
36+
# import x.y
37+
if isinstance(node, ast.Import):
38+
for n in node.names:
39+
if n.name == "azurefunctions.extensions.base":
40+
imports.add("azurefunctions.extensions.base")
41+
elif n.name == "azure.functions":
42+
imports.add("azure.functions")
43+
else:
44+
imports.add(n.name.split(".")[0])
45+
46+
# from x import y
47+
elif isinstance(node, ast.ImportFrom):
48+
# 🔹 Ignore relative imports
49+
if node.level > 0:
50+
continue
51+
52+
if node.module:
53+
if node.module == "azure.functions":
54+
imports.add("azure.functions")
55+
elif node.module == "azurefunctions.extensions.base":
56+
imports.add("azurefunctions.extensions.base")
57+
# Special cases to ignore
58+
elif str(src_dir).startswith("workers") and (
59+
node.module == "azure.monitor.opentelemetry"
60+
or node.module == "opentelemetry"
61+
or node.module == "opentelemetry.trace.propagation.tracecontext"
62+
or node.module == "Cookie"):
63+
pass
64+
elif str(src_dir).startswith("runtimes\\v1\\azure_functions_runtime_v1") and (
65+
node.module == "google.protobuf.timestamp_pb2"
66+
or node.module == "azure.monitor.opentelemetry"
67+
or node.module == "opentelemetry"
68+
or node.module == "opentelemetry.trace.propagation.tracecontext"
69+
or node.module == "Cookie"):
70+
pass
71+
elif str(src_dir).startswith("runtimes\\v2\\azure_functions_runtime") and (
72+
node.module == "google.protobuf.duration_pb2"
73+
or node.module == "google.protobuf.timestamp_pb2"
74+
or node.module == "azure.monitor.opentelemetry"
75+
or node.module == "opentelemetry"
76+
or node.module == "opentelemetry.trace.propagation.tracecontext"
77+
or node.module == "Cookie"):
78+
pass
79+
else:
80+
imports.add(node.module.split(".")[0])
81+
82+
return imports
83+
84+
85+
def load_declared_dependencies(pyproject):
86+
data = tomllib.loads(pyproject.read_text())
87+
deps = data["project"]["dependencies"]
88+
# Strip extras/markers, e.g. "protobuf~=4.25.3; python_version < '3.13'"
89+
normalized = set()
90+
for d in deps:
91+
name = d.split(";")[0].strip() # strip environment marker
92+
name = name.split("[")[0].strip() # strip extras
93+
pkg = name.split("==")[0].split("~=")[0].split(">=")[0].split("<=")[0]
94+
normalized.add(pkg.lower().replace("-", "_"))
95+
return normalized
96+
97+
98+
def check_package(pkg_root, package_name):
99+
pyproject = pkg_root / "pyproject.toml"
100+
src_dir = pkg_root / package_name
101+
102+
imports = find_imports(src_dir)
103+
deps = load_declared_dependencies(pyproject)
104+
stdlib = set(stdlib_modules())
105+
local_modules = find_local_modules(src_dir)
106+
print("Found imports:", imports)
107+
print("Declared dependencies:", deps)
108+
109+
missing = []
110+
111+
for imp in imports:
112+
113+
normalized = normalize_import(imp)
114+
if (
115+
normalized not in deps
116+
and imp not in stdlib
117+
and imp not in local_modules
118+
and imp != package_name
119+
):
120+
missing.append(imp)
121+
122+
123+
124+
if missing:
125+
print("Missing required dependencies:")
126+
for m in missing:
127+
print(" -", m)
128+
raise SystemExit(1)
129+
130+
131+
def stdlib_modules():
132+
# simple version
133+
import sys
134+
return set(sys.stdlib_module_names)
135+
136+
137+
def main():
138+
roots = sys.argv[1]
139+
package_name = sys.argv[2]
140+
141+
if not roots:
142+
print("Usage: python check_imports.py <pkg_dir> [<pkg_dir> ...]")
143+
sys.exit(2)
144+
145+
failed = False
146+
check_package(pathlib.Path(roots), package_name)
147+
148+
sys.exit(1 if failed else 0)
149+
150+
151+
if __name__ == "__main__":
152+
main()

workers/tests/utils/testutils_docker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def find_latest_image(image_repo: str,
6969
image_url: str) -> str:
7070

7171
# New regex to match version format: 4.1042.100-4-python3.11
72-
regex = re.compile(_HOST_VERSION + r'\.10\d+\.\d+(-\d+)?-python' + '3.11' + r'(-appservice)?$')
72+
regex = re.compile(_HOST_VERSION + r'\.10\d+\.\d+(-\d+)?-python' + _python_version + r'(-appservice)?$')
7373

7474
response = requests.get(image_url, allow_redirects=True)
7575
if not response.ok:

0 commit comments

Comments
 (0)