Skip to content

Custom Eval - Upload#45678

Merged
w-javed merged 10 commits intofeature/azure-ai-projects/2.0.2from
feature/custom-evaluator-upload
Mar 23, 2026
Merged

Custom Eval - Upload#45678
w-javed merged 10 commits intofeature/azure-ai-projects/2.0.2from
feature/custom-evaluator-upload

Conversation

@w-javed
Copy link
Contributor

@w-javed w-javed commented Mar 13, 2026

No description provided.

@github-actions
Copy link

github-actions bot commented Mar 13, 2026

API Change Check

APIView identified API level changes in this PR and created the following API reviews

azure-ai-projects

@w-javed w-javed changed the title Custom Eval - Upload Custom Eval - Upload (WIP) Mar 13, 2026
@w-javed w-javed changed the title Custom Eval - Upload (WIP) Custom Eval - Upload Mar 16, 2026
Copy link
Member

@nagkumar91 nagkumar91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

🔴 overwrite=True silently ignored (High)

Both samples pass overwrite=True to upload(), but the method signature is:

def upload(self, name, evaluator_version, *, folder, connection_name=None, **kwargs):

overwrite is not explicitly defined — it gets swallowed by **kwargs and forwarded to container_client.upload_blob() where it has no effect. Users will believe overwrite is supported when it isn't. If a version already exists, create_version() will likely fail with no overwrite handling.

Fix: Either implement overwrite logic (check if version exists, delete first) or remove overwrite=True from the samples.


🟡 Race condition in version auto-increment (Medium)

_get_next_version() fetches all existing versions, finds the max, and returns max+1. Between that call and the subsequent create_version(), another client could create the same version — classic TOCTOU race. Two concurrent upload() calls for the same evaluator name will both compute the same next version, and the second will fail with a conflict.

Fix: Document this as a known limitation, add retry logic on conflict, or have the service assign versions atomically.


🟡 Input mutation side effect (Medium)

upload() mutates the caller's evaluator_version object by setting definition.blob_uri:

if isinstance(evaluator_version, dict):
    definition["blob_uri"] = blob_uri  # mutates caller's dict
else:
    evaluator_version.definition.blob_uri = blob_uri  # mutates caller's object

This is undocumented and surprising. If a caller reuses the same EvaluatorVersion for multiple uploads, they'll get unexpected behavior.

Fix: Work on a deep copy of the input, or document the mutation clearly in the docstring.


🟡 Blocking I/O in async method (Medium)

The async version in _patch_evaluators_async.py uses synchronous os.walk() and open() inside the event loop:

async def upload(...):
    for root, dirs, files in os.walk(folder):  # blocking
        with open(file=file_path, mode="rb") as data:  # blocking
            await container_client.upload_blob(...)

This blocks the entire event loop for directory traversal and file reads.

Fix: Use asyncio.to_thread() to offload file I/O, or use aiofiles.


🟡 Missing async wiring in aio/operations/_patch.py (Medium)

The diff only patches the sync operations/_patch.py. The async aio/operations/_patch.py still imports BetaEvaluatorsOperations from the generated code (line 18), so the async client won't expose the new upload() method.

Fix: Apply the same patching to aio/operations/_patch.py — import from _patch_evaluators_async and override self.evaluators in __init__.


🟠 Unused variable output_version (Low)

In both sync and async upload():

container_client, output_version, blob_uri = self._start_pending_upload_and_get_container_client(...)

output_version is assigned but never used — dead code.

Fix: Return only 2 values from _start_pending_upload_and_get_container_client() or use _ for the unused variable.


🟠 No async tests (Low-Medium)

All 450 lines of tests cover the sync EvaluatorsOperations only. There is zero test coverage for the async path in _patch_evaluators_async.py.

@w-javed
Copy link
Contributor Author

w-javed commented Mar 19, 2026

Code Review

🔴 overwrite=True silently ignored (High)

Both samples pass overwrite=True to upload(), but the method signature is:

def upload(self, name, evaluator_version, *, folder, connection_name=None, **kwargs):

overwrite is not explicitly defined — it gets swallowed by **kwargs and forwarded to container_client.upload_blob() where it has no effect. Users will believe overwrite is supported when it isn't. If a version already exists, create_version() will likely fail with no overwrite handling.

Fix: Either implement overwrite logic (check if version exists, delete first) or remove overwrite=True from the samples.

🟡 Race condition in version auto-increment (Medium)

_get_next_version() fetches all existing versions, finds the max, and returns max+1. Between that call and the subsequent create_version(), another client could create the same version — classic TOCTOU race. Two concurrent upload() calls for the same evaluator name will both compute the same next version, and the second will fail with a conflict.

Fix: Document this as a known limitation, add retry logic on conflict, or have the service assign versions atomically.

🟡 Input mutation side effect (Medium)

upload() mutates the caller's evaluator_version object by setting definition.blob_uri:

if isinstance(evaluator_version, dict):
    definition["blob_uri"] = blob_uri  # mutates caller's dict
else:
    evaluator_version.definition.blob_uri = blob_uri  # mutates caller's object

This is undocumented and surprising. If a caller reuses the same EvaluatorVersion for multiple uploads, they'll get unexpected behavior.

Fix: Work on a deep copy of the input, or document the mutation clearly in the docstring.

🟡 Blocking I/O in async method (Medium)

The async version in _patch_evaluators_async.py uses synchronous os.walk() and open() inside the event loop:

async def upload(...):
    for root, dirs, files in os.walk(folder):  # blocking
        with open(file=file_path, mode="rb") as data:  # blocking
            await container_client.upload_blob(...)

This blocks the entire event loop for directory traversal and file reads.

Fix: Use asyncio.to_thread() to offload file I/O, or use aiofiles.

🟡 Missing async wiring in aio/operations/_patch.py (Medium)

The diff only patches the sync operations/_patch.py. The async aio/operations/_patch.py still imports BetaEvaluatorsOperations from the generated code (line 18), so the async client won't expose the new upload() method.

Fix: Apply the same patching to aio/operations/_patch.py — import from _patch_evaluators_async and override self.evaluators in __init__.

🟠 Unused variable output_version (Low)

In both sync and async upload():

container_client, output_version, blob_uri = self._start_pending_upload_and_get_container_client(...)

output_version is assigned but never used — dead code.

Fix: Return only 2 values from _start_pending_upload_and_get_container_client() or use _ for the unused variable.

🟠 No async tests (Low-Medium)

All 450 lines of tests cover the sync EvaluatorsOperations only. There is zero test coverage for the async path in _patch_evaluators_async.py.

Resolved

@w-javed w-javed merged commit 8dc5bb8 into feature/azure-ai-projects/2.0.2 Mar 23, 2026
11 of 20 checks passed
@w-javed w-javed deleted the feature/custom-evaluator-upload branch March 23, 2026 17:47
w-javed added a commit that referenced this pull request Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants