From 5c41c8d699f6f52ee647dbfaa14d4eea2dd24cbb Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 10 Dec 2025 13:12:12 -0600 Subject: [PATCH 01/17] Release fixes from 4.41.1 --- eng/ci/integration-tests.yml | 4 + .../official/jobs/ci-simple-e2e-tests.yml | 42 +++++++ .../official/jobs/publish-release.yml | 2 +- .../endtoend/test_basic_http_functions.py | 110 ++++++++++++++++++ 4 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 eng/templates/official/jobs/ci-simple-e2e-tests.yml create mode 100644 workers/tests/endtoend/test_basic_http_functions.py diff --git a/eng/ci/integration-tests.yml b/eng/ci/integration-tests.yml index 5dfb6c7d8..9015bbc84 100644 --- a/eng/ci/integration-tests.yml +++ b/eng/ci/integration-tests.yml @@ -51,3 +51,7 @@ extends: dependsOn: [] jobs: - template: /eng/templates/official/jobs/ci-lc-tests.yml@self + - stage: RunSimpleE2ETests + dependsOn: [] + jobs: + - template: /eng/templates/official/jobs/ci-simple-e2e-tests.yml@self diff --git a/eng/templates/official/jobs/ci-simple-e2e-tests.yml b/eng/templates/official/jobs/ci-simple-e2e-tests.yml new file mode 100644 index 000000000..32785ff7e --- /dev/null +++ b/eng/templates/official/jobs/ci-simple-e2e-tests.yml @@ -0,0 +1,42 @@ +parameters: + PROJECT_DIRECTORY: 'workers' + +jobs: + - job: "TestPython" + displayName: "Run HTTP Trigger Integration Test" + + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + + strategy: + matrix: + Python39: + PYTHON_VERSION: '3.9' + STORAGE_CONNECTION: $(LinuxStorageConnectionString39) + Python313: + PYTHON_VERSION: '3.13' + STORAGE_CONNECTION: $(LinuxStorageConnectionString312) + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: $(PYTHON_VERSION) + - task: UseDotNet@2 + displayName: 'Install .NET 8' + inputs: + version: 8.0.x + - bash: | + python -m pip install --upgrade pip + python -m pip install . pytest~=7.4.4 + + chmod +x eng/scripts/test-setup.sh + eng/scripts/test-setup.sh + displayName: 'Install only worker dependencies and build the host' + - bash: | + python -m pytest tests/endtoend/test_basic_http_functions.py + env: + AzureWebJobsStorage: $(STORAGE_CONNECTION) + PYAZURE_WEBHOST_DEBUG: true + displayName: "Running $(PYTHON_VERSION) Python Simple E2E Tests" + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} diff --git a/eng/templates/official/jobs/publish-release.yml b/eng/templates/official/jobs/publish-release.yml index 54d87313e..2cba69900 100644 --- a/eng/templates/official/jobs/publish-release.yml +++ b/eng/templates/official/jobs/publish-release.yml @@ -123,7 +123,7 @@ jobs: # Modify release_notes.md Write-Host "Adding a new entry in release_notes.md" - Add-Content -Path release_notes.md -Value "`n- Update Python Worker Version to [$newWorkerVersion](https://github.com/Azure/azure-functions-python-worker/releases/tag/$newWorkerVersion)" + Add-Content -Path release_notes.md -Value "- Update Python Worker Version to [$newWorkerVersion](https://github.com/Azure/azure-functions-python-worker/releases/tag/$newWorkerVersion)" # Commit Python Version diff --git a/workers/tests/endtoend/test_basic_http_functions.py b/workers/tests/endtoend/test_basic_http_functions.py new file mode 100644 index 000000000..f4b04ff15 --- /dev/null +++ b/workers/tests/endtoend/test_basic_http_functions.py @@ -0,0 +1,110 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from tests.utils import testutils + +REQUEST_TIMEOUT_SEC = 5 + + +class TestHttpFunctions(testutils.WebHostTestCase): + """Test the native Http Trigger in the local webhost. + + This test class will spawn a webhost from your /build/webhost + folder and replace the built-in Python with azure_functions_worker from + your code base. Since the Http Trigger is a native suport from host, we + don't need to setup any external resources. + + Compared to the unittests/test_http_functions.py, this file is more focus + on testing the E2E flow scenarios. + """ + + def setUp(self): + super().setUp() + + def tearDown(self): + super().tearDown() + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'http_functions' + + def test_function_index_page_should_return_ok(self): + """The index page of Azure Functions should return OK in any + circumstances + """ + r = self.webhost.request('GET', '', no_prefix=True, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + + def test_default_http_template_should_return_ok(self): + """Test if the default template of Http trigger in Python Function app + will return OK + """ + r = self.webhost.request('GET', 'default_template', + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + + def test_default_http_template_should_accept_query_param(self): + """Test if the azure.functions SDK is able to deserialize query + parameter from the default template + """ + r = self.webhost.request('GET', 'default_template', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, query. This HTTP triggered function executed successfully.' + ) + + def test_default_http_template_should_accept_body(self): + """Test if the azure.functions SDK is able to deserialize http body + and pass it to default template + """ + r = self.webhost.request('POST', 'default_template', + data='{ "name": "body" }'.encode('utf-8'), + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'Hello, body. This HTTP triggered function executed successfully.' + ) + + +class TestHttpFunctionsStein(TestHttpFunctions): + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ + 'http_functions_stein' + + def test_return_custom_class(self): + """Test if returning a custom class returns OK + """ + r = self.webhost.request('GET', 'custom_response', + timeout=REQUEST_TIMEOUT_SEC) + self.assertEqual( + r.content, + b'{"status": "healthy"}' + ) + self.assertTrue(r.ok) + + def test_return_custom_class_with_query_param(self): + """Test if query is accepted + """ + r = self.webhost.request('GET', 'custom_response', + params={'name': 'query'}, + timeout=REQUEST_TIMEOUT_SEC) + self.assertTrue(r.ok) + self.assertEqual( + r.content, + b'{"name": "query"}' + ) + + +class TestHttpFunctionsSteinGeneric(TestHttpFunctionsStein): + + @classmethod + def get_script_dir(cls): + return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ + 'http_functions_stein' / \ + 'generic' From ee810ab881b9f38b2a6fafae6c3d978ca42ff84c Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 10 Dec 2025 13:27:20 -0600 Subject: [PATCH 02/17] fix dir --- eng/templates/official/jobs/ci-simple-e2e-tests.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/eng/templates/official/jobs/ci-simple-e2e-tests.yml b/eng/templates/official/jobs/ci-simple-e2e-tests.yml index 32785ff7e..419ac71af 100644 --- a/eng/templates/official/jobs/ci-simple-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-simple-e2e-tests.yml @@ -27,11 +27,12 @@ jobs: inputs: version: 8.0.x - bash: | - python -m pip install --upgrade pip - python -m pip install . pytest~=7.4.4 - chmod +x eng/scripts/test-setup.sh eng/scripts/test-setup.sh + + cd ${{ parameters.PROJECT_DIRECTORY }} + python -m pip install --upgrade pip + python -m pip install . pytest~=7.4.4 displayName: 'Install only worker dependencies and build the host' - bash: | python -m pytest tests/endtoend/test_basic_http_functions.py From ce194518f6aad575769e1e1030c1e4c2d87391fe Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 11 Dec 2025 13:37:43 -0600 Subject: [PATCH 03/17] run for all versions, isntall invoke --- .../official/jobs/ci-simple-e2e-tests.yml | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/eng/templates/official/jobs/ci-simple-e2e-tests.yml b/eng/templates/official/jobs/ci-simple-e2e-tests.yml index 419ac71af..fe35ea727 100644 --- a/eng/templates/official/jobs/ci-simple-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-simple-e2e-tests.yml @@ -14,10 +14,16 @@ jobs: matrix: Python39: PYTHON_VERSION: '3.9' - STORAGE_CONNECTION: $(LinuxStorageConnectionString39) + Python310: + PYTHON_VERSION: '3.10' + Python311: + PYTHON_VERSION: '3.11' + Python312: + PYTHON_VERSION: '3.12' Python313: PYTHON_VERSION: '3.13' - STORAGE_CONNECTION: $(LinuxStorageConnectionString312) + Python314: + PYTHON_VERSION: '3.14' steps: - task: UsePythonVersion@0 inputs: @@ -27,17 +33,19 @@ jobs: inputs: version: 8.0.x - bash: | + cd workers + python -m pip install --upgrade pip + python -m pip install . pytest~=7.4.4 invoke + + cd .. chmod +x eng/scripts/test-setup.sh eng/scripts/test-setup.sh - cd ${{ parameters.PROJECT_DIRECTORY }} - python -m pip install --upgrade pip - python -m pip install . pytest~=7.4.4 displayName: 'Install only worker dependencies and build the host' - bash: | python -m pytest tests/endtoend/test_basic_http_functions.py env: - AzureWebJobsStorage: $(STORAGE_CONNECTION) + AzureWebJobsStorage: $(LinuxStorageConnectionString312) PYAZURE_WEBHOST_DEBUG: true displayName: "Running $(PYTHON_VERSION) Python Simple E2E Tests" workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} From e267875c52517042cea80e0e068dc62407912fa3 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 11 Dec 2025 14:25:03 -0600 Subject: [PATCH 04/17] add requests --- eng/templates/official/jobs/ci-simple-e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/templates/official/jobs/ci-simple-e2e-tests.yml b/eng/templates/official/jobs/ci-simple-e2e-tests.yml index fe35ea727..3ee06b164 100644 --- a/eng/templates/official/jobs/ci-simple-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-simple-e2e-tests.yml @@ -35,7 +35,7 @@ jobs: - bash: | cd workers python -m pip install --upgrade pip - python -m pip install . pytest~=7.4.4 invoke + python -m pip install . pytest~=7.4.4 invoke requests cd .. chmod +x eng/scripts/test-setup.sh From 00c1005c5850ec00d76555995ffcdc6063e7dbd7 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 11 Dec 2025 15:40:34 -0600 Subject: [PATCH 05/17] fix worker installation --- eng/templates/official/jobs/ci-simple-e2e-tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eng/templates/official/jobs/ci-simple-e2e-tests.yml b/eng/templates/official/jobs/ci-simple-e2e-tests.yml index 3ee06b164..ce8a8d0ea 100644 --- a/eng/templates/official/jobs/ci-simple-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-simple-e2e-tests.yml @@ -37,10 +37,13 @@ jobs: python -m pip install --upgrade pip python -m pip install . pytest~=7.4.4 invoke requests + # Remove from virtual environment + pip uninstall -y azure-functions-worker + pip uninstall -y proxy-worker + cd .. chmod +x eng/scripts/test-setup.sh eng/scripts/test-setup.sh - displayName: 'Install only worker dependencies and build the host' - bash: | python -m pytest tests/endtoend/test_basic_http_functions.py From 53764c66b29ad1c5ab7b8f3f5ee66a9aa6cfc54a Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 12 Dec 2025 10:06:10 -0600 Subject: [PATCH 06/17] debug --- eng/templates/official/jobs/ci-simple-e2e-tests.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/eng/templates/official/jobs/ci-simple-e2e-tests.yml b/eng/templates/official/jobs/ci-simple-e2e-tests.yml index ce8a8d0ea..aaf2ab096 100644 --- a/eng/templates/official/jobs/ci-simple-e2e-tests.yml +++ b/eng/templates/official/jobs/ci-simple-e2e-tests.yml @@ -35,16 +35,18 @@ jobs: - bash: | cd workers python -m pip install --upgrade pip - python -m pip install . pytest~=7.4.4 invoke requests - - # Remove from virtual environment - pip uninstall -y azure-functions-worker - pip uninstall -y proxy-worker + python -m pip install . pytest~=7.4.4 invoke requests==2.* cd .. chmod +x eng/scripts/test-setup.sh eng/scripts/test-setup.sh displayName: 'Install only worker dependencies and build the host' + - bash: | + echo "Current directory: $(pwd)" + echo "Build.SourcesDirectory: $(Build.SourcesDirectory)" + echo "Contents of Build.SourcesDirectory:" + ls -la $(Build.SourcesDirectory) + displayName: 'Debug directory information' - bash: | python -m pytest tests/endtoend/test_basic_http_functions.py env: From 59303f4bc95352027285f7a8868708bf15d3937c Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 12 Dec 2025 10:29:52 -0600 Subject: [PATCH 07/17] change in approach --- eng/ci/integration-tests.yml | 4 - eng/ci/public-build.yml | 4 + eng/templates/jobs/ci-dependency-check.yml | 83 +++++++++++++++++++ .../official/jobs/ci-simple-e2e-tests.yml | 56 ------------- 4 files changed, 87 insertions(+), 60 deletions(-) create mode 100644 eng/templates/jobs/ci-dependency-check.yml delete mode 100644 eng/templates/official/jobs/ci-simple-e2e-tests.yml diff --git a/eng/ci/integration-tests.yml b/eng/ci/integration-tests.yml index 9015bbc84..5dfb6c7d8 100644 --- a/eng/ci/integration-tests.yml +++ b/eng/ci/integration-tests.yml @@ -51,7 +51,3 @@ extends: dependsOn: [] jobs: - template: /eng/templates/official/jobs/ci-lc-tests.yml@self - - stage: RunSimpleE2ETests - dependsOn: [] - jobs: - - template: /eng/templates/official/jobs/ci-simple-e2e-tests.yml@self diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 69ae8576f..19c350e10 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -60,6 +60,10 @@ extends: PROJECT_DIRECTORY: 'workers' # Skip the build stage for SDK and Extensions release branches. This stage will fail because pyproject.toml contains the updated (and unreleased) library version condition: and(eq(variables.isSdkRelease, false), eq(variables.isExtensionsRelease, false), eq(variables['USETESTPYTHONSDK'], false), eq(variables['USETESTPYTHONEXTENSIONS'], false)) + - stage: CheckPythonWorkerDependencies + dependsOn: BuildPythonWorker + jobs: + - template: /eng/templates/jobs/ci-dependency-check.yml@self - stage: RunWorkerUnitTests dependsOn: BuildPythonWorker jobs: diff --git a/eng/templates/jobs/ci-dependency-check.yml b/eng/templates/jobs/ci-dependency-check.yml new file mode 100644 index 000000000..d86378e6d --- /dev/null +++ b/eng/templates/jobs/ci-dependency-check.yml @@ -0,0 +1,83 @@ +parameters: + PROJECT_DIRECTORY: 'workers' + +jobs: + - job: "TestPython" + displayName: "Run HTTP Trigger Integration Test" + + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + + strategy: + matrix: + Python39: + PYTHON_VERSION: '3.9' + Python310: + PYTHON_VERSION: '3.10' + Python311: + PYTHON_VERSION: '3.11' + Python312: + PYTHON_VERSION: '3.12' + Python313: + PYTHON_VERSION: '3.13' + Python314: + PYTHON_VERSION: '3.14' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: $(PYTHON_VERSION) + - bash: | + cd workers + pip install --no-deps . + + PY_VER="$(PYTHON_VERSION)" + echo "Python version: $PY_VER" + + # Extract minor version + PY_MINOR="${PY_VER#*.}" + + python -c "import pkgutil, sys; + + if [ "$PY_MINOR" -ge 13 ]; then + import proxy_worker; + else + import azure_functions_worker; + fi + print('OK: imports resolved')" + displayName: 'Python Worker: check for missing dependencies' + - bash: | + cd runtimes/v1 + pip install --no-deps . + + PY_VER="$(PYTHON_VERSION)" + echo "Python version: $PY_VER" + + # Extract minor version + PY_MINOR="${PY_VER#*.}" + + python -c "import pkgutil, sys; + + if [ "$PY_MINOR" -ge 13 ]; then + import azure_functions_runtime_v1; + fi + print('OK: imports resolved')" + displayName: 'Python Library V1: check for missing dependencies' + - bash: | + cd runtimes/v2 + pip install --no-deps . + + PY_VER="$(PYTHON_VERSION)" + echo "Python version: $PY_VER" + + # Extract minor version + PY_MINOR="${PY_VER#*.}" + + python -c "import pkgutil, sys; + + if [ "$PY_MINOR" -ge 13 ]; then + import azure_functions_runtime; + fi + print('OK: imports resolved')" + displayName: 'Python Library V2: check for missing dependencies' diff --git a/eng/templates/official/jobs/ci-simple-e2e-tests.yml b/eng/templates/official/jobs/ci-simple-e2e-tests.yml deleted file mode 100644 index aaf2ab096..000000000 --- a/eng/templates/official/jobs/ci-simple-e2e-tests.yml +++ /dev/null @@ -1,56 +0,0 @@ -parameters: - PROJECT_DIRECTORY: 'workers' - -jobs: - - job: "TestPython" - displayName: "Run HTTP Trigger Integration Test" - - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - - strategy: - matrix: - Python39: - PYTHON_VERSION: '3.9' - Python310: - PYTHON_VERSION: '3.10' - Python311: - PYTHON_VERSION: '3.11' - Python312: - PYTHON_VERSION: '3.12' - Python313: - PYTHON_VERSION: '3.13' - Python314: - PYTHON_VERSION: '3.14' - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: $(PYTHON_VERSION) - - task: UseDotNet@2 - displayName: 'Install .NET 8' - inputs: - version: 8.0.x - - bash: | - cd workers - python -m pip install --upgrade pip - python -m pip install . pytest~=7.4.4 invoke requests==2.* - - cd .. - chmod +x eng/scripts/test-setup.sh - eng/scripts/test-setup.sh - displayName: 'Install only worker dependencies and build the host' - - bash: | - echo "Current directory: $(pwd)" - echo "Build.SourcesDirectory: $(Build.SourcesDirectory)" - echo "Contents of Build.SourcesDirectory:" - ls -la $(Build.SourcesDirectory) - displayName: 'Debug directory information' - - bash: | - python -m pytest tests/endtoend/test_basic_http_functions.py - env: - AzureWebJobsStorage: $(LinuxStorageConnectionString312) - PYAZURE_WEBHOST_DEBUG: true - displayName: "Running $(PYTHON_VERSION) Python Simple E2E Tests" - workingDirectory: $(Build.SourcesDirectory)/${{ parameters.PROJECT_DIRECTORY }} From fa90875213ceef87b511701edf2ff00f8e0d2c17 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 12 Dec 2025 10:57:50 -0600 Subject: [PATCH 08/17] fix pool --- eng/ci/public-build.yml | 2 ++ eng/templates/jobs/ci-dependency-check.yml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/eng/ci/public-build.yml b/eng/ci/public-build.yml index 19c350e10..17d8000c3 100644 --- a/eng/ci/public-build.yml +++ b/eng/ci/public-build.yml @@ -64,6 +64,8 @@ extends: dependsOn: BuildPythonWorker jobs: - template: /eng/templates/jobs/ci-dependency-check.yml@self + parameters: + PoolName: 1es-pool-azfunc-public - stage: RunWorkerUnitTests dependsOn: BuildPythonWorker jobs: diff --git a/eng/templates/jobs/ci-dependency-check.yml b/eng/templates/jobs/ci-dependency-check.yml index d86378e6d..15ad87997 100644 --- a/eng/templates/jobs/ci-dependency-check.yml +++ b/eng/templates/jobs/ci-dependency-check.yml @@ -3,10 +3,10 @@ parameters: jobs: - job: "TestPython" - displayName: "Run HTTP Trigger Integration Test" + displayName: "Run Dependency Checks" pool: - name: 1es-pool-azfunc + name: ${{ parameters.PoolName }} image: 1es-ubuntu-22.04 os: linux From 77e066fb12237954233e5619993552d28d9d94df Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Fri, 12 Dec 2025 14:42:16 -0600 Subject: [PATCH 09/17] fix? --- eng/templates/jobs/ci-dependency-check.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/eng/templates/jobs/ci-dependency-check.yml b/eng/templates/jobs/ci-dependency-check.yml index 15ad87997..13dfc89ec 100644 --- a/eng/templates/jobs/ci-dependency-check.yml +++ b/eng/templates/jobs/ci-dependency-check.yml @@ -36,16 +36,17 @@ jobs: echo "Python version: $PY_VER" # Extract minor version - PY_MINOR="${PY_VER#*.}" - - python -c "import pkgutil, sys; + PY_MINOR="${PY_VER#*.}" if [ "$PY_MINOR" -ge 13 ]; then + python -c "import pkgutil, sys; import proxy_worker; + print('OK: imports resolved')" else + python -c "import pkgutil, sys; import azure_functions_worker; + print('OK: imports resolved')" fi - print('OK: imports resolved')" displayName: 'Python Worker: check for missing dependencies' - bash: | cd runtimes/v1 @@ -57,12 +58,11 @@ jobs: # Extract minor version PY_MINOR="${PY_VER#*.}" - python -c "import pkgutil, sys; - if [ "$PY_MINOR" -ge 13 ]; then + python -c "import pkgutil, sys; import azure_functions_runtime_v1; + print('OK: imports resolved')" fi - print('OK: imports resolved')" displayName: 'Python Library V1: check for missing dependencies' - bash: | cd runtimes/v2 @@ -72,12 +72,11 @@ jobs: echo "Python version: $PY_VER" # Extract minor version - PY_MINOR="${PY_VER#*.}" - - python -c "import pkgutil, sys; + PY_MINOR="${PY_VER#*.}" if [ "$PY_MINOR" -ge 13 ]; then + python -c "import pkgutil, sys; import azure_functions_runtime; + print('OK: imports resolved')" fi - print('OK: imports resolved')" displayName: 'Python Library V2: check for missing dependencies' From 294772e905e33508c2f0772bd309b3b820f331bf Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 15 Dec 2025 10:35:37 -0600 Subject: [PATCH 10/17] docker image fix --- eng/templates/jobs/ci-dependency-check.yml | 16 ++++------------ .../jobs/ci-docker-consumption-tests.yml | 1 + .../official/jobs/ci-docker-dedicated-tests.yml | 1 + workers/tests/utils/testutils.py | 4 ++-- workers/tests/utils/testutils_docker.py | 12 +++++++++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/eng/templates/jobs/ci-dependency-check.yml b/eng/templates/jobs/ci-dependency-check.yml index 13dfc89ec..d9296b10d 100644 --- a/eng/templates/jobs/ci-dependency-check.yml +++ b/eng/templates/jobs/ci-dependency-check.yml @@ -39,13 +39,9 @@ jobs: PY_MINOR="${PY_VER#*.}" if [ "$PY_MINOR" -ge 13 ]; then - python -c "import pkgutil, sys; - import proxy_worker; - print('OK: imports resolved')" + python -c "import proxy_worker; print('OK: imports resolved')" else - python -c "import pkgutil, sys; - import azure_functions_worker; - print('OK: imports resolved')" + python -c "import azure_functions_worker; print('OK: imports resolved')" fi displayName: 'Python Worker: check for missing dependencies' - bash: | @@ -59,9 +55,7 @@ jobs: PY_MINOR="${PY_VER#*.}" if [ "$PY_MINOR" -ge 13 ]; then - python -c "import pkgutil, sys; - import azure_functions_runtime_v1; - print('OK: imports resolved')" + python -c "import azure_functions_runtime_v1; print('OK: imports resolved')" fi displayName: 'Python Library V1: check for missing dependencies' - bash: | @@ -75,8 +69,6 @@ jobs: PY_MINOR="${PY_VER#*.}" if [ "$PY_MINOR" -ge 13 ]; then - python -c "import pkgutil, sys; - import azure_functions_runtime; - print('OK: imports resolved')" + python -c "import azure_functions_runtime; print('OK: imports resolved')" fi displayName: 'Python Library V2: check for missing dependencies' diff --git a/eng/templates/official/jobs/ci-docker-consumption-tests.yml b/eng/templates/official/jobs/ci-docker-consumption-tests.yml index e5653455b..5d24c2eb8 100644 --- a/eng/templates/official/jobs/ci-docker-consumption-tests.yml +++ b/eng/templates/official/jobs/ci-docker-consumption-tests.yml @@ -56,6 +56,7 @@ jobs: env: CONSUMPTION_DOCKER_TEST: "true" AzureWebJobsStorage: $(STORAGE_CONNECTION) + STORAGE_CONNECTION: $(STORAGE_CONNECTION) AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) AzureWebJobsEventHubConnectionString: $(EVENTHUB_CONNECTION) AzureWebJobsServiceBusConnectionString: $(SERVICEBUS_CONNECTION) diff --git a/eng/templates/official/jobs/ci-docker-dedicated-tests.yml b/eng/templates/official/jobs/ci-docker-dedicated-tests.yml index 728caa42b..360a15398 100644 --- a/eng/templates/official/jobs/ci-docker-dedicated-tests.yml +++ b/eng/templates/official/jobs/ci-docker-dedicated-tests.yml @@ -56,6 +56,7 @@ jobs: env: DEDICATED_DOCKER_TEST: "true" AzureWebJobsStorage: $(STORAGE_CONNECTION) + STORAGE_CONNECTION: $(STORAGE_CONNECTION) AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) AzureWebJobsEventHubConnectionString: $(EVENTHUB_CONNECTION) AzureWebJobsServiceBusConnectionString: $(SERVICEBUS_CONNECTION) diff --git a/workers/tests/utils/testutils.py b/workers/tests/utils/testutils.py index f90bd3258..ecd7c37f8 100644 --- a/workers/tests/utils/testutils.py +++ b/workers/tests/utils/testutils.py @@ -144,7 +144,7 @@ class AsyncTestCase(unittest.TestCase, metaclass=AsyncTestCaseMeta): class WebHostTestCaseMeta(type(unittest.TestCase)): def __new__(mcls, name, bases, dct): - if is_envvar_true(DEDICATED_DOCKER_TEST) \ + if True \ or is_envvar_true(CONSUMPTION_DOCKER_TEST): return super().__new__(mcls, name, bases, dct) @@ -221,7 +221,7 @@ def docker_tests_enabled(self) -> (bool, str): """ if is_envvar_true(CONSUMPTION_DOCKER_TEST): return True, CONSUMPTION_DOCKER_TEST - elif is_envvar_true(DEDICATED_DOCKER_TEST): + elif True: return True, DEDICATED_DOCKER_TEST else: return False, None diff --git a/workers/tests/utils/testutils_docker.py b/workers/tests/utils/testutils_docker.py index feaa186d4..674f25f55 100644 --- a/workers/tests/utils/testutils_docker.py +++ b/workers/tests/utils/testutils_docker.py @@ -68,7 +68,8 @@ class WebHostDockerContainerBase(unittest.TestCase): def find_latest_image(image_repo: str, image_url: str) -> str: - regex = re.compile(_HOST_VERSION + r'.\d+.\d+-python' + _python_version) + # New regex to match version format: 4.1042.100-4-python3.11 + regex = re.compile(_HOST_VERSION + r'\.10\d+\.\d+(-\d+)?-python' + '3.11' + r'(-appservice)?$') response = requests.get(image_url, allow_redirects=True) if not response.ok: @@ -88,8 +89,13 @@ def find_latest_image(image_repo: str, # sorting all the python versions based on the runtime version and # getting the latest released runtime version for python. - latest_version = sorted(python_versions, key=lambda x: float( - x.split(_HOST_VERSION + '.')[-1].split("-python")[0]))[-1] + # Parse version format: 4.1042.100-4-python3.11 + def parse_version(tag): + version_part = tag.split('-python')[0] # "4.1042.100-4" + parts = version_part.replace('-', '.').split('.') # ["4", "1042", "100", "4"] + return tuple(int(p) for p in parts) # (4, 1042, 100, 4) + + latest_version = sorted(python_versions, key=parse_version)[-1] image_tag = f'{image_repo}:{latest_version}' return image_tag From ff4b24f8f8a2dc9f9596a7fd51ba2ede846ff167 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 15 Dec 2025 10:36:40 -0600 Subject: [PATCH 11/17] docker image fix --- workers/tests/utils/testutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workers/tests/utils/testutils.py b/workers/tests/utils/testutils.py index ecd7c37f8..f90bd3258 100644 --- a/workers/tests/utils/testutils.py +++ b/workers/tests/utils/testutils.py @@ -144,7 +144,7 @@ class AsyncTestCase(unittest.TestCase, metaclass=AsyncTestCaseMeta): class WebHostTestCaseMeta(type(unittest.TestCase)): def __new__(mcls, name, bases, dct): - if True \ + if is_envvar_true(DEDICATED_DOCKER_TEST) \ or is_envvar_true(CONSUMPTION_DOCKER_TEST): return super().__new__(mcls, name, bases, dct) @@ -221,7 +221,7 @@ def docker_tests_enabled(self) -> (bool, str): """ if is_envvar_true(CONSUMPTION_DOCKER_TEST): return True, CONSUMPTION_DOCKER_TEST - elif True: + elif is_envvar_true(DEDICATED_DOCKER_TEST): return True, DEDICATED_DOCKER_TEST else: return False, None From 7ca94835e0733dc025e07bd00d78968ee6c28ba8 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 15 Dec 2025 11:00:14 -0600 Subject: [PATCH 12/17] test that packaging would have failed --- .../jobs/ci-docker-consumption-tests.yml | 18 ++++++------------ .../jobs/ci-docker-dedicated-tests.yml | 18 ++++++------------ workers/proxy_worker/dispatcher.py | 1 + 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/eng/templates/official/jobs/ci-docker-consumption-tests.yml b/eng/templates/official/jobs/ci-docker-consumption-tests.yml index 5d24c2eb8..c4af5c014 100644 --- a/eng/templates/official/jobs/ci-docker-consumption-tests.yml +++ b/eng/templates/official/jobs/ci-docker-consumption-tests.yml @@ -15,30 +15,27 @@ jobs: Python39: PYTHON_VERSION: '3.9' STORAGE_CONNECTION: $(LinuxStorageConnectionString39) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString39) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString39) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString39) SQL_CONNECTION: $(LinuxSqlConnectionString39) EVENTGRID_URI: $(LinuxEventGridTopicUriString39) EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString39) Python310: PYTHON_VERSION: '3.10' STORAGE_CONNECTION: $(LinuxStorageConnectionString310) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString310) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString310) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString310) SQL_CONNECTION: $(LinuxSqlConnectionString310) EVENTGRID_URI: $(LinuxEventGridTopicUriString310) EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString310) Python311: PYTHON_VERSION: '3.11' STORAGE_CONNECTION: $(LinuxStorageConnectionString311) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString311) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString311) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString311) SQL_CONNECTION: $(LinuxSqlConnectionString311) EVENTGRID_URI: $(LinuxEventGridTopicUriString311) EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString311) + Python312: + PYTHON_VERSION: '3.12' + STORAGE_CONNECTION: $(LinuxStorageConnectionString312) + SQL_CONNECTION: $(LinuxSqlConnectionString312) + EVENTGRID_URI: $(LinuxEventGridTopicUriString312) + EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString312) steps: - task: UsePythonVersion@0 @@ -57,9 +54,6 @@ jobs: CONSUMPTION_DOCKER_TEST: "true" AzureWebJobsStorage: $(STORAGE_CONNECTION) STORAGE_CONNECTION: $(STORAGE_CONNECTION) - AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) - AzureWebJobsEventHubConnectionString: $(EVENTHUB_CONNECTION) - AzureWebJobsServiceBusConnectionString: $(SERVICEBUS_CONNECTION) AzureWebJobsSqlConnectionString: $(SQL_CONNECTION) AzureWebJobsEventGridTopicUri: $(EVENTGRID_URI) AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) diff --git a/eng/templates/official/jobs/ci-docker-dedicated-tests.yml b/eng/templates/official/jobs/ci-docker-dedicated-tests.yml index 360a15398..dff9f5d1f 100644 --- a/eng/templates/official/jobs/ci-docker-dedicated-tests.yml +++ b/eng/templates/official/jobs/ci-docker-dedicated-tests.yml @@ -15,30 +15,27 @@ jobs: Python39: PYTHON_VERSION: '3.9' STORAGE_CONNECTION: $(LinuxStorageConnectionString39) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString39) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString39) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString39) SQL_CONNECTION: $(LinuxSqlConnectionString39) EVENTGRID_URI: $(LinuxEventGridTopicUriString39) EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString39) Python310: PYTHON_VERSION: '3.10' STORAGE_CONNECTION: $(LinuxStorageConnectionString310) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString310) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString310) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString310) SQL_CONNECTION: $(LinuxSqlConnectionString310) EVENTGRID_URI: $(LinuxEventGridTopicUriString310) EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString310) Python311: PYTHON_VERSION: '3.11' STORAGE_CONNECTION: $(LinuxStorageConnectionString311) - COSMOSDB_CONNECTION: $(LinuxCosmosDBConnectionString311) - EVENTHUB_CONNECTION: $(LinuxEventHubConnectionString311) - SERVICEBUS_CONNECTION: $(LinuxServiceBusConnectionString311) SQL_CONNECTION: $(LinuxSqlConnectionString311) EVENTGRID_URI: $(LinuxEventGridTopicUriString311) EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString311) + Python312: + PYTHON_VERSION: '3.12' + STORAGE_CONNECTION: $(LinuxStorageConnectionString312) + SQL_CONNECTION: $(LinuxSqlConnectionString312) + EVENTGRID_URI: $(LinuxEventGridTopicUriString312) + EVENTGRID_CONNECTION: $(LinuxEventGridConnectionKeyString312) steps: - task: UsePythonVersion@0 @@ -57,9 +54,6 @@ jobs: DEDICATED_DOCKER_TEST: "true" AzureWebJobsStorage: $(STORAGE_CONNECTION) STORAGE_CONNECTION: $(STORAGE_CONNECTION) - AzureWebJobsCosmosDBConnectionString: $(COSMOSDB_CONNECTION) - AzureWebJobsEventHubConnectionString: $(EVENTHUB_CONNECTION) - AzureWebJobsServiceBusConnectionString: $(SERVICEBUS_CONNECTION) AzureWebJobsSqlConnectionString: $(SQL_CONNECTION) AzureWebJobsEventGridTopicUri: $(EVENTGRID_URI) AzureWebJobsEventGridConnectionKey: $(EVENTGRID_CONNECTION) diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index 55aaeb3b1..4a6dcb048 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -12,6 +12,7 @@ from asyncio import AbstractEventLoop from dataclasses import dataclass from typing import Any, Optional +import packaging import grpc From c4a237d26d08b2c1f8118c2f98a0f205cd628243 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 16 Dec 2025 11:09:38 -0600 Subject: [PATCH 13/17] final fixes + testing that dep check fails --- eng/templates/jobs/ci-dependency-check.yml | 21 +-- .../endtoend/test_basic_http_functions.py | 110 ------------- workers/tests/unittests/check_imports.py | 152 ++++++++++++++++++ workers/tests/utils/testutils_docker.py | 2 +- 4 files changed, 160 insertions(+), 125 deletions(-) delete mode 100644 workers/tests/endtoend/test_basic_http_functions.py create mode 100644 workers/tests/unittests/check_imports.py diff --git a/eng/templates/jobs/ci-dependency-check.yml b/eng/templates/jobs/ci-dependency-check.yml index d9296b10d..613897dba 100644 --- a/eng/templates/jobs/ci-dependency-check.yml +++ b/eng/templates/jobs/ci-dependency-check.yml @@ -29,25 +29,21 @@ jobs: inputs: versionSpec: $(PYTHON_VERSION) - bash: | - cd workers - pip install --no-deps . - PY_VER="$(PYTHON_VERSION)" echo "Python version: $PY_VER" # Extract minor version - PY_MINOR="${PY_VER#*.}" + PY_MINOR="${PY_VER#*.}" if [ "$PY_MINOR" -ge 13 ]; then - python -c "import proxy_worker; print('OK: imports resolved')" + echo "Checking proxy_worker (Python >= 3.13)..." + python workers/tests/unittests/check_imports.py workers proxy_worker else - python -c "import azure_functions_worker; print('OK: imports resolved')" + echo "Checking azure_functions_worker (Python < 3.13)..." + python workers/tests/unittests/check_imports.py workers azure_functions_worker fi displayName: 'Python Worker: check for missing dependencies' - bash: | - cd runtimes/v1 - pip install --no-deps . - PY_VER="$(PYTHON_VERSION)" echo "Python version: $PY_VER" @@ -55,13 +51,10 @@ jobs: PY_MINOR="${PY_VER#*.}" if [ "$PY_MINOR" -ge 13 ]; then - python -c "import azure_functions_runtime_v1; print('OK: imports resolved')" + python workers/tests/unittests/check_imports.py runtimes/v1 azure-functions-runtime-v1 fi displayName: 'Python Library V1: check for missing dependencies' - bash: | - cd runtimes/v2 - pip install --no-deps . - PY_VER="$(PYTHON_VERSION)" echo "Python version: $PY_VER" @@ -69,6 +62,6 @@ jobs: PY_MINOR="${PY_VER#*.}" if [ "$PY_MINOR" -ge 13 ]; then - python -c "import azure_functions_runtime; print('OK: imports resolved')" + python workers/tests/unittests/check_imports.py runtimes/v2 azure-functions-runtime fi displayName: 'Python Library V2: check for missing dependencies' diff --git a/workers/tests/endtoend/test_basic_http_functions.py b/workers/tests/endtoend/test_basic_http_functions.py deleted file mode 100644 index f4b04ff15..000000000 --- a/workers/tests/endtoend/test_basic_http_functions.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -from tests.utils import testutils - -REQUEST_TIMEOUT_SEC = 5 - - -class TestHttpFunctions(testutils.WebHostTestCase): - """Test the native Http Trigger in the local webhost. - - This test class will spawn a webhost from your /build/webhost - folder and replace the built-in Python with azure_functions_worker from - your code base. Since the Http Trigger is a native suport from host, we - don't need to setup any external resources. - - Compared to the unittests/test_http_functions.py, this file is more focus - on testing the E2E flow scenarios. - """ - - def setUp(self): - super().setUp() - - def tearDown(self): - super().tearDown() - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' - - def test_function_index_page_should_return_ok(self): - """The index page of Azure Functions should return OK in any - circumstances - """ - r = self.webhost.request('GET', '', no_prefix=True, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - def test_default_http_template_should_return_ok(self): - """Test if the default template of Http trigger in Python Function app - will return OK - """ - r = self.webhost.request('GET', 'default_template', - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - - def test_default_http_template_should_accept_query_param(self): - """Test if the azure.functions SDK is able to deserialize query - parameter from the default template - """ - r = self.webhost.request('GET', 'default_template', - params={'name': 'query'}, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertEqual( - r.content, - b'Hello, query. This HTTP triggered function executed successfully.' - ) - - def test_default_http_template_should_accept_body(self): - """Test if the azure.functions SDK is able to deserialize http body - and pass it to default template - """ - r = self.webhost.request('POST', 'default_template', - data='{ "name": "body" }'.encode('utf-8'), - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertEqual( - r.content, - b'Hello, body. This HTTP triggered function executed successfully.' - ) - - -class TestHttpFunctionsStein(TestHttpFunctions): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ - 'http_functions_stein' - - def test_return_custom_class(self): - """Test if returning a custom class returns OK - """ - r = self.webhost.request('GET', 'custom_response', - timeout=REQUEST_TIMEOUT_SEC) - self.assertEqual( - r.content, - b'{"status": "healthy"}' - ) - self.assertTrue(r.ok) - - def test_return_custom_class_with_query_param(self): - """Test if query is accepted - """ - r = self.webhost.request('GET', 'custom_response', - params={'name': 'query'}, - timeout=REQUEST_TIMEOUT_SEC) - self.assertTrue(r.ok) - self.assertEqual( - r.content, - b'{"name": "query"}' - ) - - -class TestHttpFunctionsSteinGeneric(TestHttpFunctionsStein): - - @classmethod - def get_script_dir(cls): - return testutils.E2E_TESTS_FOLDER / 'http_functions' / \ - 'http_functions_stein' / \ - 'generic' diff --git a/workers/tests/unittests/check_imports.py b/workers/tests/unittests/check_imports.py new file mode 100644 index 000000000..e0b953847 --- /dev/null +++ b/workers/tests/unittests/check_imports.py @@ -0,0 +1,152 @@ +import ast +import pathlib +import tomllib +import sys + +IMPORT_TO_PACKAGE = { + "google": "protobuf", + "dateutil": "python_dateutil", + "grpc": "grpcio", + "azure.functions": "azure_functions", + "azurefunctions.extensions.base": "azurefunctions_extensions_base", +} + + +def normalize_import(import_name): + return IMPORT_TO_PACKAGE.get(import_name, import_name) + + +def find_local_modules(src_dir): + local = set() + for py in src_dir.rglob("*.py"): + if py.name == "__init__.py": + local.add(py.parent.name) + else: + local.add(py.stem) + return local + + +def find_imports(src_dir): + imports = set() + for py in src_dir.rglob("*.py"): + with open(py, "r", encoding="utf8") as f: + tree = ast.parse(f.read(), filename=str(py)) + + for node in ast.walk(tree): + # import x.y + if isinstance(node, ast.Import): + for n in node.names: + if n.name == "azurefunctions.extensions.base": + imports.add("azurefunctions.extensions.base") + elif n.name == "azure.functions": + imports.add("azure.functions") + else: + imports.add(n.name.split(".")[0]) + + # from x import y + elif isinstance(node, ast.ImportFrom): + # 🔹 Ignore relative imports + if node.level > 0: + continue + + if node.module: + if node.module == "azure.functions": + imports.add("azure.functions") + elif node.module == "azurefunctions.extensions.base": + imports.add("azurefunctions.extensions.base") + # Special cases to ignore + elif str(src_dir).startswith("workers") and ( + node.module == "azure.monitor.opentelemetry" + or node.module == "opentelemetry" + or node.module == "opentelemetry.trace.propagation.tracecontext" + or node.module == "Cookie"): + pass + elif str(src_dir).startswith("runtimes\\v1\\azure_functions_runtime_v1") and ( + node.module == "google.protobuf.timestamp_pb2" + or node.module == "azure.monitor.opentelemetry" + or node.module == "opentelemetry" + or node.module == "opentelemetry.trace.propagation.tracecontext" + or node.module == "Cookie"): + pass + elif str(src_dir).startswith("runtimes\\v2\\azure_functions_runtime") and ( + node.module == "google.protobuf.duration_pb2" + or node.module == "google.protobuf.timestamp_pb2" + or node.module == "azure.monitor.opentelemetry" + or node.module == "opentelemetry" + or node.module == "opentelemetry.trace.propagation.tracecontext" + or node.module == "Cookie"): + pass + else: + imports.add(node.module.split(".")[0]) + + return imports + + +def load_declared_dependencies(pyproject): + data = tomllib.loads(pyproject.read_text()) + deps = data["project"]["dependencies"] + # Strip extras/markers, e.g. "protobuf~=4.25.3; python_version < '3.13'" + normalized = set() + for d in deps: + name = d.split(";")[0].strip() # strip environment marker + name = name.split("[")[0].strip() # strip extras + pkg = name.split("==")[0].split("~=")[0].split(">=")[0].split("<=")[0] + normalized.add(pkg.lower().replace("-", "_")) + return normalized + + +def check_package(pkg_root, package_name): + pyproject = pkg_root / "pyproject.toml" + src_dir = pkg_root / package_name + + imports = find_imports(src_dir) + deps = load_declared_dependencies(pyproject) + stdlib = set(stdlib_modules()) + local_modules = find_local_modules(src_dir) + print("Found imports:", imports) + print("Declared dependencies:", deps) + + missing = [] + + for imp in imports: + + normalized = normalize_import(imp) + if ( + normalized not in deps + and imp not in stdlib + and imp not in local_modules + and imp != package_name + ): + missing.append(imp) + + + + if missing: + print("Missing required dependencies:") + for m in missing: + print(" -", m) + raise SystemExit(1) + + +def stdlib_modules(): + # simple version + import sys + return set(sys.stdlib_module_names) + + +def main(): + roots = sys.argv[1] + package_name = sys.argv[2] + + if not roots: + print("Usage: python check_imports.py [ ...]") + sys.exit(2) + + failed = False + check_package(pathlib.Path(roots), package_name) + + sys.exit(1 if failed else 0) + + +if __name__ == "__main__": + main() diff --git a/workers/tests/utils/testutils_docker.py b/workers/tests/utils/testutils_docker.py index 674f25f55..db0109f61 100644 --- a/workers/tests/utils/testutils_docker.py +++ b/workers/tests/utils/testutils_docker.py @@ -69,7 +69,7 @@ def find_latest_image(image_repo: str, image_url: str) -> str: # New regex to match version format: 4.1042.100-4-python3.11 - regex = re.compile(_HOST_VERSION + r'\.10\d+\.\d+(-\d+)?-python' + '3.11' + r'(-appservice)?$') + regex = re.compile(_HOST_VERSION + r'\.10\d+\.\d+(-\d+)?-python' + _python_version + r'(-appservice)?$') response = requests.get(image_url, allow_redirects=True) if not response.ok: From fd28f687742d1e71f8457371411e87e9fbd1038a Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 16 Dec 2025 12:17:05 -0600 Subject: [PATCH 14/17] flake --- eng/templates/jobs/ci-dependency-check.yml | 4 --- workers/proxy_worker/dispatcher.py | 1 - workers/tests/unittests/check_imports.py | 36 ++++++++++------------ workers/tests/utils/testutils_docker.py | 15 ++++++--- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/eng/templates/jobs/ci-dependency-check.yml b/eng/templates/jobs/ci-dependency-check.yml index 613897dba..dfd8cc91c 100644 --- a/eng/templates/jobs/ci-dependency-check.yml +++ b/eng/templates/jobs/ci-dependency-check.yml @@ -12,10 +12,6 @@ jobs: strategy: matrix: - Python39: - PYTHON_VERSION: '3.9' - Python310: - PYTHON_VERSION: '3.10' Python311: PYTHON_VERSION: '3.11' Python312: diff --git a/workers/proxy_worker/dispatcher.py b/workers/proxy_worker/dispatcher.py index 4a6dcb048..55aaeb3b1 100644 --- a/workers/proxy_worker/dispatcher.py +++ b/workers/proxy_worker/dispatcher.py @@ -12,7 +12,6 @@ from asyncio import AbstractEventLoop from dataclasses import dataclass from typing import Any, Optional -import packaging import grpc diff --git a/workers/tests/unittests/check_imports.py b/workers/tests/unittests/check_imports.py index e0b953847..d4e9726f1 100644 --- a/workers/tests/unittests/check_imports.py +++ b/workers/tests/unittests/check_imports.py @@ -56,25 +56,25 @@ def find_imports(src_dir): imports.add("azurefunctions.extensions.base") # Special cases to ignore elif str(src_dir).startswith("workers") and ( - node.module == "azure.monitor.opentelemetry" - or node.module == "opentelemetry" - or node.module == "opentelemetry.trace.propagation.tracecontext" - or node.module == "Cookie"): + node.module == "azure.monitor.opentelemetry" + or node.module == "opentelemetry" + or node.module == "opentelemetry.trace.propagation.tracecontext" # noqa + or node.module == "Cookie"): pass - elif str(src_dir).startswith("runtimes\\v1\\azure_functions_runtime_v1") and ( - node.module == "google.protobuf.timestamp_pb2" - or node.module == "azure.monitor.opentelemetry" - or node.module == "opentelemetry" - or node.module == "opentelemetry.trace.propagation.tracecontext" - or node.module == "Cookie"): + elif str(src_dir).startswith("runtimes\\v1\\azure_functions_runtime_v1") and ( # noqa + node.module == "google.protobuf.timestamp_pb2" + or node.module == "azure.monitor.opentelemetry" + or node.module == "opentelemetry" + or node.module == "opentelemetry.trace.propagation.tracecontext" # noqa + or node.module == "Cookie"): pass - elif str(src_dir).startswith("runtimes\\v2\\azure_functions_runtime") and ( - node.module == "google.protobuf.duration_pb2" - or node.module == "google.protobuf.timestamp_pb2" - or node.module == "azure.monitor.opentelemetry" - or node.module == "opentelemetry" - or node.module == "opentelemetry.trace.propagation.tracecontext" - or node.module == "Cookie"): + elif str(src_dir).startswith("runtimes\\v2\\azure_functions_runtime")and ( # noqa + node.module == "google.protobuf.duration_pb2" + or node.module == "google.protobuf.timestamp_pb2" + or node.module == "azure.monitor.opentelemetry" + or node.module == "opentelemetry" + or node.module == "opentelemetry.trace.propagation.tracecontext" # noqa + or node.module == "Cookie"): pass else: imports.add(node.module.split(".")[0]) @@ -119,8 +119,6 @@ def check_package(pkg_root, package_name): ): missing.append(imp) - - if missing: print("Missing required dependencies:") for m in missing: diff --git a/workers/tests/utils/testutils_docker.py b/workers/tests/utils/testutils_docker.py index db0109f61..05d3545df 100644 --- a/workers/tests/utils/testutils_docker.py +++ b/workers/tests/utils/testutils_docker.py @@ -69,7 +69,9 @@ def find_latest_image(image_repo: str, image_url: str) -> str: # New regex to match version format: 4.1042.100-4-python3.11 - regex = re.compile(_HOST_VERSION + r'\.10\d+\.\d+(-\d+)?-python' + _python_version + r'(-appservice)?$') + regex = re.compile(_HOST_VERSION + + r'\.10\d+\.\d+(-\d+)?-python' + + _python_version + r'(-appservice)?$') response = requests.get(image_url, allow_redirects=True) if not response.ok: @@ -91,10 +93,13 @@ def find_latest_image(image_repo: str, # getting the latest released runtime version for python. # Parse version format: 4.1042.100-4-python3.11 def parse_version(tag): - version_part = tag.split('-python')[0] # "4.1042.100-4" - parts = version_part.replace('-', '.').split('.') # ["4", "1042", "100", "4"] - return tuple(int(p) for p in parts) # (4, 1042, 100, 4) - + # "4.1042.100-4" + version_part = tag.split('-python')[0] + # ["4", "1042", "100", "4"] + parts = version_part.replace('-', '.').split('.') + # (4, 1042, 100, 4) + return tuple(int(p) for p in parts) + latest_version = sorted(python_versions, key=parse_version)[-1] image_tag = f'{image_repo}:{latest_version}' From ba509c7036e3b82571d91e5107274788ae00221a Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 17 Dec 2025 09:55:42 -0600 Subject: [PATCH 15/17] simplify dep check --- eng/templates/jobs/ci-dependency-check.yml | 74 +++++----- workers/tests/unittests/check_imports.py | 150 --------------------- 2 files changed, 41 insertions(+), 183 deletions(-) delete mode 100644 workers/tests/unittests/check_imports.py diff --git a/eng/templates/jobs/ci-dependency-check.yml b/eng/templates/jobs/ci-dependency-check.yml index dfd8cc91c..651018f0c 100644 --- a/eng/templates/jobs/ci-dependency-check.yml +++ b/eng/templates/jobs/ci-dependency-check.yml @@ -1,6 +1,3 @@ -parameters: - PROJECT_DIRECTORY: 'workers' - jobs: - job: "TestPython" displayName: "Run Dependency Checks" @@ -12,6 +9,10 @@ jobs: strategy: matrix: + Python39: + PYTHON_VERSION: '3.9' + Python310: + PYTHON_VERSION: '3.10' Python311: PYTHON_VERSION: '3.11' Python312: @@ -24,40 +25,47 @@ jobs: - task: UsePythonVersion@0 inputs: versionSpec: $(PYTHON_VERSION) - - bash: | - PY_VER="$(PYTHON_VERSION)" - echo "Python version: $PY_VER" + - powershell: | + $PY_VER = "$(PYTHON_VERSION)" + Write-Host "Python version: $PY_VER" - # Extract minor version - PY_MINOR="${PY_VER#*.}" + # Extract minor version as integers + $versionParts = $PY_VER.Split('.') + $PY_MINOR = [int]$versionParts[1] + Write-Host "Minor version: $PY_MINOR" + Write-Host "##vso[task.setvariable variable=minorVersion;]$PY_MINOR" - if [ "$PY_MINOR" -ge 13 ]; then - echo "Checking proxy_worker (Python >= 3.13)..." - python workers/tests/unittests/check_imports.py workers proxy_worker + # Set build-related variables based on Python minor version + if( $PY_MINOR -ge 13 ) + { + Write-Host "##vso[task.setvariable variable=proxyWorker;]true" + } else - echo "Checking azure_functions_worker (Python < 3.13)..." - python workers/tests/unittests/check_imports.py workers azure_functions_worker - fi - displayName: 'Python Worker: check for missing dependencies' + { + Write-Host "##vso[task.setvariable variable=proxyWorker;]false" + } + displayName: 'Set necessary variables' - bash: | - PY_VER="$(PYTHON_VERSION)" - echo "Python version: $PY_VER" - - # Extract minor version - PY_MINOR="${PY_VER#*.}" - - if [ "$PY_MINOR" -ge 13 ]; then - python workers/tests/unittests/check_imports.py runtimes/v1 azure-functions-runtime-v1 - fi + echo "Checking azure_functions_worker (Python < 3.13)..." + cd workers + python -c "import pkgutil, importlib; [importlib.import_module(f'azure_functions_worker.{name}') for _, name, _ in pkgutil.walk_packages(['azure_functions_worker'])]" + displayName: 'Python Azure Functions Worker: check for missing dependencies' + condition: eq(variables['proxyWorker'], false) + - bash: | + echo "Checking proxy_worker (Python >= 3.13)..." + cd workers + python -c "import pkgutil, importlib; [importlib.import_module(f'proxy_worker.{name}') for _, name, _ in pkgutil.walk_packages(['proxy_worker'])]" + displayName: 'Python Proxy Worker: check for missing dependencies' + condition: eq(variables['proxyWorker'], true) + - bash: | + echo "Checking V1 Library Worker (Python >= 3.13)..." + cd runtimes/v1 + python -c "import pkgutil, importlib; [importlib.import_module(f'azure_functions_runtime_v1.{name}') for _, name, _ in pkgutil.walk_packages(['azure_functions_runtime_v1'])]" displayName: 'Python Library V1: check for missing dependencies' + condition: eq(variables['proxyWorker'], true) - bash: | - PY_VER="$(PYTHON_VERSION)" - echo "Python version: $PY_VER" - - # Extract minor version - PY_MINOR="${PY_VER#*.}" - - if [ "$PY_MINOR" -ge 13 ]; then - python workers/tests/unittests/check_imports.py runtimes/v2 azure-functions-runtime - fi + echo "Checking V2 Library Worker (Python >= 3.13)..." + cd runtimes/v2 + python -c "import pkgutil, importlib; [importlib.import_module(f'azure_functions_runtime.{name}') for _, name, _ in pkgutil.walk_packages(['azure_functions_runtime'])]" displayName: 'Python Library V2: check for missing dependencies' + condition: eq(variables['proxyWorker'], true) diff --git a/workers/tests/unittests/check_imports.py b/workers/tests/unittests/check_imports.py deleted file mode 100644 index d4e9726f1..000000000 --- a/workers/tests/unittests/check_imports.py +++ /dev/null @@ -1,150 +0,0 @@ -import ast -import pathlib -import tomllib -import sys - -IMPORT_TO_PACKAGE = { - "google": "protobuf", - "dateutil": "python_dateutil", - "grpc": "grpcio", - "azure.functions": "azure_functions", - "azurefunctions.extensions.base": "azurefunctions_extensions_base", -} - - -def normalize_import(import_name): - return IMPORT_TO_PACKAGE.get(import_name, import_name) - - -def find_local_modules(src_dir): - local = set() - for py in src_dir.rglob("*.py"): - if py.name == "__init__.py": - local.add(py.parent.name) - else: - local.add(py.stem) - return local - - -def find_imports(src_dir): - imports = set() - for py in src_dir.rglob("*.py"): - with open(py, "r", encoding="utf8") as f: - tree = ast.parse(f.read(), filename=str(py)) - - for node in ast.walk(tree): - # import x.y - if isinstance(node, ast.Import): - for n in node.names: - if n.name == "azurefunctions.extensions.base": - imports.add("azurefunctions.extensions.base") - elif n.name == "azure.functions": - imports.add("azure.functions") - else: - imports.add(n.name.split(".")[0]) - - # from x import y - elif isinstance(node, ast.ImportFrom): - # 🔹 Ignore relative imports - if node.level > 0: - continue - - if node.module: - if node.module == "azure.functions": - imports.add("azure.functions") - elif node.module == "azurefunctions.extensions.base": - imports.add("azurefunctions.extensions.base") - # Special cases to ignore - elif str(src_dir).startswith("workers") and ( - node.module == "azure.monitor.opentelemetry" - or node.module == "opentelemetry" - or node.module == "opentelemetry.trace.propagation.tracecontext" # noqa - or node.module == "Cookie"): - pass - elif str(src_dir).startswith("runtimes\\v1\\azure_functions_runtime_v1") and ( # noqa - node.module == "google.protobuf.timestamp_pb2" - or node.module == "azure.monitor.opentelemetry" - or node.module == "opentelemetry" - or node.module == "opentelemetry.trace.propagation.tracecontext" # noqa - or node.module == "Cookie"): - pass - elif str(src_dir).startswith("runtimes\\v2\\azure_functions_runtime")and ( # noqa - node.module == "google.protobuf.duration_pb2" - or node.module == "google.protobuf.timestamp_pb2" - or node.module == "azure.monitor.opentelemetry" - or node.module == "opentelemetry" - or node.module == "opentelemetry.trace.propagation.tracecontext" # noqa - or node.module == "Cookie"): - pass - else: - imports.add(node.module.split(".")[0]) - - return imports - - -def load_declared_dependencies(pyproject): - data = tomllib.loads(pyproject.read_text()) - deps = data["project"]["dependencies"] - # Strip extras/markers, e.g. "protobuf~=4.25.3; python_version < '3.13'" - normalized = set() - for d in deps: - name = d.split(";")[0].strip() # strip environment marker - name = name.split("[")[0].strip() # strip extras - pkg = name.split("==")[0].split("~=")[0].split(">=")[0].split("<=")[0] - normalized.add(pkg.lower().replace("-", "_")) - return normalized - - -def check_package(pkg_root, package_name): - pyproject = pkg_root / "pyproject.toml" - src_dir = pkg_root / package_name - - imports = find_imports(src_dir) - deps = load_declared_dependencies(pyproject) - stdlib = set(stdlib_modules()) - local_modules = find_local_modules(src_dir) - print("Found imports:", imports) - print("Declared dependencies:", deps) - - missing = [] - - for imp in imports: - - normalized = normalize_import(imp) - if ( - normalized not in deps - and imp not in stdlib - and imp not in local_modules - and imp != package_name - ): - missing.append(imp) - - if missing: - print("Missing required dependencies:") - for m in missing: - print(" -", m) - raise SystemExit(1) - - -def stdlib_modules(): - # simple version - import sys - return set(sys.stdlib_module_names) - - -def main(): - roots = sys.argv[1] - package_name = sys.argv[2] - - if not roots: - print("Usage: python check_imports.py [ ...]") - sys.exit(2) - - failed = False - check_package(pathlib.Path(roots), package_name) - - sys.exit(1 if failed else 0) - - -if __name__ == "__main__": - main() From 59b2ebc315bd078214d7c8c9e9261b91f2829fc0 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 17 Dec 2025 10:25:13 -0600 Subject: [PATCH 16/17] oops --- eng/templates/jobs/ci-dependency-check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/eng/templates/jobs/ci-dependency-check.yml b/eng/templates/jobs/ci-dependency-check.yml index 651018f0c..4a0bcfa7c 100644 --- a/eng/templates/jobs/ci-dependency-check.yml +++ b/eng/templates/jobs/ci-dependency-check.yml @@ -48,24 +48,28 @@ jobs: - bash: | echo "Checking azure_functions_worker (Python < 3.13)..." cd workers + pip install . python -c "import pkgutil, importlib; [importlib.import_module(f'azure_functions_worker.{name}') for _, name, _ in pkgutil.walk_packages(['azure_functions_worker'])]" displayName: 'Python Azure Functions Worker: check for missing dependencies' condition: eq(variables['proxyWorker'], false) - bash: | echo "Checking proxy_worker (Python >= 3.13)..." cd workers + pip install . python -c "import pkgutil, importlib; [importlib.import_module(f'proxy_worker.{name}') for _, name, _ in pkgutil.walk_packages(['proxy_worker'])]" displayName: 'Python Proxy Worker: check for missing dependencies' condition: eq(variables['proxyWorker'], true) - bash: | echo "Checking V1 Library Worker (Python >= 3.13)..." cd runtimes/v1 + pip install . python -c "import pkgutil, importlib; [importlib.import_module(f'azure_functions_runtime_v1.{name}') for _, name, _ in pkgutil.walk_packages(['azure_functions_runtime_v1'])]" displayName: 'Python Library V1: check for missing dependencies' condition: eq(variables['proxyWorker'], true) - bash: | echo "Checking V2 Library Worker (Python >= 3.13)..." cd runtimes/v2 + pip install . python -c "import pkgutil, importlib; [importlib.import_module(f'azure_functions_runtime.{name}') for _, name, _ in pkgutil.walk_packages(['azure_functions_runtime'])]" displayName: 'Python Library V2: check for missing dependencies' condition: eq(variables['proxyWorker'], true) From adb309f83c2be3998d837328cea711c4ef8f0eb4 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 17 Dec 2025 10:48:49 -0600 Subject: [PATCH 17/17] fix --- eng/templates/jobs/ci-dependency-check.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/eng/templates/jobs/ci-dependency-check.yml b/eng/templates/jobs/ci-dependency-check.yml index 4a0bcfa7c..3ce00f954 100644 --- a/eng/templates/jobs/ci-dependency-check.yml +++ b/eng/templates/jobs/ci-dependency-check.yml @@ -48,14 +48,20 @@ jobs: - bash: | echo "Checking azure_functions_worker (Python < 3.13)..." cd workers - pip install . + pip install . invoke + cd tests + python -m invoke -c test_setup build-protos + cd .. python -c "import pkgutil, importlib; [importlib.import_module(f'azure_functions_worker.{name}') for _, name, _ in pkgutil.walk_packages(['azure_functions_worker'])]" displayName: 'Python Azure Functions Worker: check for missing dependencies' condition: eq(variables['proxyWorker'], false) - bash: | echo "Checking proxy_worker (Python >= 3.13)..." cd workers - pip install . + pip install . invoke + cd tests + python -m invoke -c test_setup build-protos + cd .. python -c "import pkgutil, importlib; [importlib.import_module(f'proxy_worker.{name}') for _, name, _ in pkgutil.walk_packages(['proxy_worker'])]" displayName: 'Python Proxy Worker: check for missing dependencies' condition: eq(variables['proxyWorker'], true)