Skip to content

Conversation

@BryanFauble
Copy link
Member

Problem:

  • The synapseclient library requires users to instantiate the Synapse class and call methods on it for common utility operations, which creates unnecessary friction and doesn't align with modern Python patterns.
  • Following the factory-style pattern established with get/get_async and extended in the previous PR with store/delete, the remaining utility methods from the Jira ticket need to be exposed as standalone functions.
  • This PR addresses the remaining methods identified in the ticket: findEntityId, is_synapse_id, onweb, and md5Query.

Solution:

  • Implemented new factory functions following the established pattern:
    • find_entity_id / find_entity_id_async: Find an entity by name and optional parent, returning the entity ID or None if not found. Supports both string IDs and entity objects for the parent parameter.
    • is_synapse_id / is_synapse_id_async: Validate whether a Synapse ID string corresponds to an actual entity. Returns True for valid IDs (including those the user lacks permission to access), False for invalid/non-existent IDs.
    • onweb / onweb_async: Open an entity's page in the default web browser. Supports optional subpage_id for wiki subpages. Returns the URL that was opened.
    • md5_query / md5_query_async: Find entities with attached files matching a given MD5 hash. Returns a list of entity header dictionaries.
  • Added underlying API functions:
    • synapseclient.api.entity_services.is_synapse_id: Async implementation with proper error handling for 400/403/404 status codes
    • synapseclient.api.web_services.open_entity_in_browser: Async implementation that constructs and opens Synapse portal URLs
  • Added deprecation warnings (version 4.11.0, removal in 5.0.0) to the legacy Synapse class methods: is_synapse_id, onweb, get, store, delete, md5Query, findEntityId

Testing:

  • Added comprehensive integration test suites:
    • test_utility_operations_async.py: Tests for find_entity_id_async (project lookup, file in parent, not found cases, parent object support), is_synapse_id_async (valid/invalid IDs, non-string input), md5_query_async (matching files, non-existent hashes), onweb_async (ID string, entity object, wiki subpage URLs)
    • test_utility_operations.py: Synchronous equivalents of all async tests
  • Added unit test suites:
    • unit_test_entity_services.py: Tests for get_child (found/not found/project lookup), is_synapse_id (valid/invalid/403 permission errors/non-string), get_entities_by_md5 (with/without results)
    • unit_test_web_services.py: Tests for open_entity_in_browser (Synapse ID, entity object, subpage, file path handling, custom portal endpoint)
    • unit_test_utility_operations.py: Tests for all wrapper functions ensuring proper delegation to underlying API functions

@BryanFauble BryanFauble requested a review from a team as a code owner January 2, 2026 22:34
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces factory-style standalone functions for utility operations in the Synapse client, following the established pattern from previous PRs. The changes expose four utility methods (find_entity_id, is_synapse_id, onweb, and md5_query) as standalone functions with both synchronous and asynchronous versions, while adding deprecation warnings to the corresponding legacy methods on the Synapse class.

Key changes include:

  • Factory functions in synapseclient.operations.utility_operations module with comprehensive documentation
  • New API layer functions in synapseclient.api.entity_services and synapseclient.api.web_services
  • Deprecation warnings (version 4.11.0, removal in 5.0.0) for legacy Synapse class methods

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
synapseclient/operations/utility_operations.py Implements the main factory functions (find_entity_id, is_synapse_id, onweb, md5_query) and their async variants with comprehensive docstrings and examples
synapseclient/api/entity_services.py Adds the is_synapse_id API function with error handling for 400/403/404 status codes
synapseclient/api/web_services.py Implements open_entity_in_browser function to construct and open Synapse portal URLs
synapseclient/operations/__init__.py Exports the new utility operation functions for public API
synapseclient/api/__init__.py Exports new API layer functions (is_synapse_id, open_entity_in_browser)
synapseclient/client.py Adds deprecation decorators to legacy methods (is_synapse_id, onweb, get, store, delete, md5Query, findEntityId)
tests/unit/synapseclient/operations/unit_test_utility_operations.py Unit tests for wrapper functions ensuring proper delegation
tests/unit/synapseclient/api/unit_test_entity_services.py Unit tests for get_child, is_synapse_id, and get_entities_by_md5 API functions
tests/unit/synapseclient/api/unit_test_web_services.py Unit tests for open_entity_in_browser with various input types
tests/integration/synapseclient/operations/synchronous/test_utility_operations.py Integration tests for synchronous versions of utility functions
tests/integration/synapseclient/operations/async/test_utility_operations_async.py Integration tests for asynchronous versions of utility functions
tests/unit/synapseclient/operations/__init__.py Module docstring for unit test package
Comments suppressed due to low confidence (1)

synapseclient/client.py:1591

  • The onweb method in the legacy Synapse class does not return a value (returns None), but the new onweb function returns the URL that was opened. While this is an improvement in the API design, it represents a breaking change in behavior for users who might be expecting None as the return value. Consider documenting this change explicitly in the deprecation message or migration guide to avoid confusion.
    def onweb(self, entity, subpageId=None):
        """Opens up a browser window to the entity page or wiki-subpage.

        Arguments:
            entity:    Either an Entity or a Synapse ID
            subpageId: (Optional) ID of one of the wiki's sub-pages

        Returns:
            None
        """
        if isinstance(entity, str) and os.path.isfile(entity):
            entity = self.get(entity, downloadFile=False)
        synId = id_of(entity)
        if subpageId is None:
            webbrowser.open("%s#!Synapse:%s" % (self.portalEndpoint, synId))
        else:
            webbrowser.open(
                "%s#!Wiki:%s/ENTITY/%s" % (self.portalEndpoint, synId, subpageId)
            )


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +81
from synapseclient.api import get_entity

client = Synapse.get_client(synapse_client=synapse_client)

# If it's a file path, get the entity first
if isinstance(entity, str) and os.path.isfile(entity):
entity_dict = await get_entity(entity_id=entity, synapse_client=synapse_client)
synapse_id = entity_dict.get("id")
else:
synapse_id = id_of(entity)

# Construct the URL
portal_endpoint = client.portalEndpoint
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

When the entity parameter is a file path (line 74), the code calls get_entity(entity_id=entity, ...) where entity is still the file path string, not a Synapse ID. This will likely fail because get_entity expects a Synapse ID, not a file path. The legacy implementation (line 1583 in client.py) calls self.get(entity, downloadFile=False) which can handle file paths. Consider using a similar approach or clarifying the expected behavior for file path inputs.

Suggested change
from synapseclient.api import get_entity
client = Synapse.get_client(synapse_client=synapse_client)
# If it's a file path, get the entity first
if isinstance(entity, str) and os.path.isfile(entity):
entity_dict = await get_entity(entity_id=entity, synapse_client=synapse_client)
synapse_id = entity_dict.get("id")
else:
synapse_id = id_of(entity)
# Construct the URL
portal_endpoint = client.portalEndpoint
client = Synapse.get_client(synapse_client=synapse_client)
# If it's a file path, resolve it to an entity using the Synapse client
if isinstance(entity, str) and os.path.isfile(entity):
resolved_entity = client.get(entity, downloadFile=False)
synapse_id = id_of(resolved_entity)
else:
synapse_id = id_of(entity)
# Construct the URL
portal_endpoint = client.portalEndpoint
portal_endpoint = client.portalEndpoint

Copilot uses AI. Check for mistakes.
Comment on lines +1710 to +1723
except (SynapseHTTPError, SynapseAuthenticationError) as err:
# Extract status code
status = None
if hasattr(err, "__context__") and hasattr(err.__context__, "response"):
status = err.__context__.response.status_code
elif hasattr(err, "response"):
status = err.response.status_code

if status in (400, 404):
return False
# Valid ID but user lacks permission or is not logged in
elif status == 403:
return True
raise
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

The error handling logic attempts to extract the status code from either err.response.status_code or err.__context__.response.status_code, but the order of checks may not properly handle all cases. In the original implementation (line 1556 of client.py), it uses or to combine both checks: err.__context__.response.status_code or err.response.status_code. The current implementation checks for the presence of attributes first, which is safer, but when status is None after both checks, the code will still attempt to check if status in (400, 404) which could lead to unexpected behavior. Consider adding an explicit check to ensure status is not None before the conditional checks, or re-raise the exception if status cannot be determined.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +5
import pytest

Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

Import of 'pytest' is not used.

Suggested change
import pytest

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +16
from typing import TYPE_CHECKING, List, Optional, Union

from synapseclient.core.async_utils import wrap_async_to_sync

if TYPE_CHECKING:
from synapseclient import Synapse


def find_entity_id(
name: str,
parent: Optional[Union[str, object]] = None,
*,
synapse_client: Optional["Synapse"] = None,
) -> Optional[str]:
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

Import of 'Synapse' is not used.

Suggested change
from typing import TYPE_CHECKING, List, Optional, Union
from synapseclient.core.async_utils import wrap_async_to_sync
if TYPE_CHECKING:
from synapseclient import Synapse
def find_entity_id(
name: str,
parent: Optional[Union[str, object]] = None,
*,
synapse_client: Optional["Synapse"] = None,
) -> Optional[str]:
from typing import List, Optional, Union
from synapseclient.core.async_utils import wrap_async_to_sync
def find_entity_id(
name: str,
parent: Optional[Union[str, object]] = None,
*,
synapse_client: Optional["Synapse"] = None,
) -> Optional[str]:
name: str,
parent: Optional[Union[str, object]] = None,
*,
synapse_client: Optional["Synapse"] = None,
) -> Optional[str]:

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +10
if TYPE_CHECKING:
from synapseclient import Synapse
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

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

Import of 'Synapse' is not used.

Suggested change
if TYPE_CHECKING:
from synapseclient import Synapse

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants