-
Notifications
You must be signed in to change notification settings - Fork 73
[SYNPY-1671] Adding store & delete factory method #1300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SYNPY-1671] Adding store & delete factory method #1300
Conversation
There was a problem hiding this 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 methods store, store_async, delete, and delete_async to simplify common operations on Synapse entities. The implementation follows the pattern established by the existing get/get_async methods and provides a unified interface for storing and deleting various entity types with type-specific configuration options.
Key Changes:
- New
store/store_asyncfunctions supporting 15+ entity types with 5 specialized option classes (StoreFileOptions, StoreContainerOptions, StoreTableOptions, StoreJSONSchemaOptions, StoreGridOptions) - New
delete/delete_asyncfunctions with version-specific deletion support and clear precedence rules for version parameters - Comprehensive integration test suites covering both synchronous and asynchronous variants
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| synapseclient/operations/store_operations.py | Implements store/store_async factory methods with entity-specific handlers and option classes |
| synapseclient/operations/delete_operations.py | Implements delete/delete_async factory methods with version handling and validation |
| synapseclient/operations/init.py | Exports new functions and option classes at package level |
| tests/integration/synapseclient/operations/synchronous/test_factory_operations_store.py | Integration tests for synchronous store operations covering all supported entity types |
| tests/integration/synapseclient/operations/synchronous/test_delete_operations.py | Integration tests for synchronous delete operations including version-specific deletion |
| tests/integration/synapseclient/operations/async/test_factory_operations_store_async.py | Async variants of store operation integration tests |
| tests/integration/synapseclient/operations/async/test_delete_operations_async.py | Async variants of delete operation integration tests |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| # WHEN I delete the grid using delete | ||
| delete(stored_grid, synapse_client=self.syn) | ||
| # Grid deletion is fire-and-forget, no need to verify |
Copilot
AI
Jan 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test description says "Grid deletion is fire-and-forget, no need to verify" but this is misleading - the delete operation is still being called and could potentially fail silently. Either verify the deletion succeeded or document why verification is not necessary for Grid entities specifically.
| # Grid deletion is fire-and-forget, no need to verify | |
| # For Grid entities, this test only checks that delete() can be called without raising. |
| f"Deleting a specific version requires version_only=True. " | ||
| f"Use delete('{entity}', version_only=True) to delete version {final_version}." |
Copilot
AI
Jan 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message includes a hardcoded suggestion with 'delete()' but does not specify the async variant. When this error is raised from delete_async(), the suggestion should say delete_async() instead. Consider making the function name dynamic or providing context-appropriate suggestions.
| f"Deleting a specific version requires version_only=True. " | |
| f"Use delete('{entity}', version_only=True) to delete version {final_version}." | |
| "Deleting a specific version requires version_only=True. " | |
| f"Pass version_only=True when calling this function to delete version {final_version}." |
| # Emit warning only when there's an actual version conflict (both are set and different) | ||
| if ( | ||
| version_only | ||
| and version is not None | ||
| and entity_version is not None | ||
| and version != entity_version |
Copilot
AI
Jan 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type hint for version parameter allows Union[int, str], but the comparison at line 493 version != entity_version doesn't account for type coercion (e.g., version=2 as string "2" vs entity_version=2 as int). This could cause false positive warnings. Consider normalizing both to the same type before comparison, or document that version should always be an int.
| # Emit warning only when there's an actual version conflict (both are set and different) | |
| if ( | |
| version_only | |
| and version is not None | |
| and entity_version is not None | |
| and version != entity_version | |
| # Normalize versions for comparison to avoid false conflicts between str/int | |
| normalized_version = ( | |
| int(version) if isinstance(version, str) and version.isdigit() else version | |
| ) | |
| normalized_entity_version = ( | |
| int(entity_version) | |
| if isinstance(entity_version, str) and entity_version.isdigit() | |
| else entity_version | |
| ) | |
| # Emit warning only when there's an actual version conflict (both are set and different) | |
| if ( | |
| version_only | |
| and normalized_version is not None | |
| and normalized_entity_version is not None | |
| and normalized_version != normalized_entity_version |
| # Set the entity's version_number to the final version so delete_async uses it | ||
| entity.version_number = final_version_for_entity |
Copilot
AI
Jan 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The version parameter type is Union[int, str] but this is assigned directly to entity.version_number at line 516 without type conversion. If final_version_for_entity is a string, this could cause issues if version_number expects an int. Consider converting to int or clarifying the expected type.
| # Set the entity's version_number to the final version so delete_async uses it | |
| entity.version_number = final_version_for_entity | |
| # Normalize final_version_for_entity to an int before assigning | |
| if isinstance(final_version_for_entity, str): | |
| try: | |
| final_version_for_entity_int = int(final_version_for_entity) | |
| except ValueError as exc: | |
| raise ValueError( | |
| f"Invalid version value '{final_version_for_entity}'; an integer is required." | |
| ) from exc | |
| else: | |
| final_version_for_entity_int = final_version_for_entity | |
| # Set the entity's version_number to the final version so delete_async uses it | |
| entity.version_number = final_version_for_entity_int |
tests/integration/synapseclient/operations/async/test_factory_operations_store_async.py
Show resolved
Hide resolved
tests/integration/synapseclient/operations/synchronous/test_factory_operations_store.py
Show resolved
Hide resolved
| raise ValueError( | ||
| f"Invalid Synapse ID: {entity}. " | ||
| "Expected a valid Synapse ID string (e.g., 'syn123456' or 'syn123456.4')." | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I've seen this logic elsewhere. It might make sense to have a SynapseID object that does this check. Then this function could take in a str or SynapseID type.
class SynapseID(id: str):
if not is_synapse_id_str(id):
raise ValueError(
f"Invalid Synapse ID: {entity}. "
"Expected a valid Synapse ID string (e.g., 'syn123456' or 'syn123456.4')."
)
self.id, self.version = get_synid_and_version(id)
Then it could do:
if isinstance(entity, str):
entity = SynapseID(entity)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it needs to be a little easier to verify the Synapse ID format, but putting it into a class doesn't seem like the right thing. Rather it probably makes sense to be a utility function.
|
LGTM! |
… find_entity_id, is_synapse_id, onweb, md5_query (#1301) * Creating functions for additional Synapse class methods, find_entity_id, is_synapse_id, onweb, md5_query
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High test
synapse.org
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 18 days ago
In general, to fix incomplete URL substring sanitization, the URL must be parsed and its components (especially hostname) checked explicitly, instead of using substring checks on the raw URL string. For host checks, use a proper URL parser and compare the hostname (or carefully checked suffix) to an allowlist of expected domains.
For this specific test file, we should stop asserting "synapse.org" in url.lower() and instead parse the URL and assert that its hostname matches the expected Synapse web domain. We should do this wherever that substring check appears (lines 203, 216, 234). Python’s standard library urllib.parse is already appropriate and needs no external dependency. Concretely:
- Add
from urllib.parse import urlparseat the top oftests/integration/synapseclient/operations/async/test_utility_operations_async.py. - In
test_onweb_async_project_by_id, replaceassert "synapse.org" in url.lower()with parsing the URL (parsed = urlparse(url)) and asserting onparsed.hostname, e.g.assert parsed.hostname and parsed.hostname.lower().endswith("synapse.org"). This allows subdomains likewww.synapse.orgwhile rejecting hosts that merely containsynapse.orgsomewhere else. - Apply the same pattern in
test_onweb_async_project_by_objectandtest_onweb_async_with_subpage.
This preserves the existing intent (“URL is a Synapse URL”) while making the checks precise and avoiding unsafe substring usage.
-
Copy modified line R6 -
Copy modified lines R204-R206 -
Copy modified lines R219-R221 -
Copy modified lines R239-R241
| @@ -3,6 +3,7 @@ | ||
| from typing import Callable | ||
|
|
||
| import pytest | ||
| from urllib.parse import urlparse | ||
|
|
||
| from synapseclient import Synapse | ||
| from synapseclient.core import utils | ||
| @@ -200,7 +201,9 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname.lower().endswith("synapse.org") | ||
| assert project_id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -213,7 +216,9 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname.lower().endswith("synapse.org") | ||
| assert project_model.id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -231,7 +236,9 @@ | ||
| # THEN I expect a valid Synapse URL with wiki reference | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname.lower().endswith("synapse.org") | ||
| assert project_id in url | ||
| assert subpage_id in url | ||
| assert "Wiki:" in url |
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High test
synapse.org
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 18 days ago
In general, the way to fix this class of issue is to stop treating the URL as an opaque string for domain validation and instead parse it with a URL parser, then inspect the hostname (and possibly scheme and path) explicitly. For Synapse URLs, we likely expect hostnames such as www.synapse.org or other subdomains of synapse.org; using urllib.parse.urlparse to obtain parsed.hostname and asserting that it either equals synapse.org or ends with .synapse.org is both precise and robust against substring tricks.
For this specific test file, we should:
- Import
urlparsefromurllib.parse. - Replace the three assertions
assert "synapse.org" in url.lower()with hostname-based assertions. - Because we do not know all valid Synapse hosts from this snippet, the safest non-breaking change is to assert that the parsed hostname ends with
synapse.org(allowingwww.synapse.org,foo.synapse.org, etc.), and that parsing succeeded.
Concretely:
- At the top of
tests/integration/synapseclient/operations/async/test_utility_operations_async.py, addfrom urllib.parse import urlparsealongside existing imports. - For each of the three tests that currently contain
assert "synapse.org" in url.lower()(lines 203, 216, 234), first parse the URL:parsed = urlparse(url), then:- Assert
parsed.hostname is not None. - Assert
parsed.hostname.lower().endswith("synapse.org").
- Assert
This changes the tests from substring checks to host-based checks while preserving their intent of verifying that onweb_async returns a proper Synapse URL.
-
Copy modified line R6 -
Copy modified lines R204-R206 -
Copy modified lines R219-R221 -
Copy modified lines R239-R241
| @@ -3,6 +3,7 @@ | ||
| from typing import Callable | ||
|
|
||
| import pytest | ||
| from urllib.parse import urlparse | ||
|
|
||
| from synapseclient import Synapse | ||
| from synapseclient.core import utils | ||
| @@ -200,7 +201,9 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname.lower().endswith("synapse.org") | ||
| assert project_id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -213,7 +216,9 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname.lower().endswith("synapse.org") | ||
| assert project_model.id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -231,7 +236,9 @@ | ||
| # THEN I expect a valid Synapse URL with wiki reference | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname.lower().endswith("synapse.org") | ||
| assert project_id in url | ||
| assert subpage_id in url | ||
| assert "Wiki:" in url |
| # THEN I expect a valid Synapse URL with wiki reference | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High test
synapse.org
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 18 days ago
In general, to avoid incomplete URL sanitization, you should parse the URL with a standard library (such as Python’s urllib.parse.urlparse) and validate the hostname component rather than searching for a domain substring in the raw URL string. This ensures that domain checks apply to the actual host part of the URL and are not accidentally satisfied by occurrences in the path, query, or other components.
For this test file, the best fix is to replace the substring assertion assert "synapse.org" in url.lower() with a hostname-based check using urllib.parse.urlparse. Since we cannot assume or modify other files, we will update only this test module. Specifically:
- Add an import for
urlparsefromurllib.parseat the top oftests/integration/synapseclient/operations/async/test_utility_operations_async.py. - In the three tests that currently assert
"synapse.org" in url.lower()(lines 203, 216, and 234), parse the URL and assert that the parsed hostname equals"synapse.org"or ends with".synapse.org". This is stricter and aligns with the recommended pattern. - Keep the rest of the assertions unchanged to preserve existing functionality.
Concretely:
- At the top of the file, add
from urllib.parse import urlparse. - In each test (
test_onweb_async_project_by_id,test_onweb_async_project_by_object, andtest_onweb_async_with_subpage), insert something like:and remove the old substring assertion.parsed = urlparse(url) assert parsed.hostname is not None assert parsed.hostname == "synapse.org" or parsed.hostname.endswith(".synapse.org")
No additional helper methods are required; we only need the standard-library import and minor assertion changes.
-
Copy modified line R6 -
Copy modified lines R204-R208 -
Copy modified lines R221-R225 -
Copy modified lines R243-R247
| @@ -3,6 +3,7 @@ | ||
| from typing import Callable | ||
|
|
||
| import pytest | ||
| from urllib.parse import urlparse | ||
|
|
||
| from synapseclient import Synapse | ||
| from synapseclient.core import utils | ||
| @@ -200,7 +201,11 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname == "synapse.org" or parsed.hostname.endswith( | ||
| ".synapse.org" | ||
| ) | ||
| assert project_id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -213,7 +218,11 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname == "synapse.org" or parsed.hostname.endswith( | ||
| ".synapse.org" | ||
| ) | ||
| assert project_model.id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -231,7 +240,11 @@ | ||
| # THEN I expect a valid Synapse URL with wiki reference | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname == "synapse.org" or parsed.hostname.endswith( | ||
| ".synapse.org" | ||
| ) | ||
| assert project_id in url | ||
| assert subpage_id in url | ||
| assert "Wiki:" in url |
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High test
synapse.org
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 18 days ago
In general, the issue is that using a substring check like "synapse.org" in url.lower() does not actually verify that the URL’s host is synapse.org (or a valid subdomain), only that this text appears somewhere in the string. The robust fix is to parse the URL with urllib.parse.urlparse, then assert on the hostname component (for example, that it equals or ends with synapse.org). This matches the recommendation in the background: always parse the URL and check the host value instead of using string containment.
For this specific test file, we should update the tests that currently assert "synapse.org" in url.lower() (lines 190 and 203, and potentially 219) to parse the URL and check its hostname. We can do this without altering the behavior of onweb; we are only verifying its output more strictly. Concretely:
- Add an import for
urlparsefromurllib.parseat the top oftests/integration/synapseclient/operations/synchronous/test_utility_operations.py. - In
test_onweb_project_by_id, replaceassert "synapse.org" in url.lower()with code that parsesurland asserts thatparsed.hostnameis either exactlysynapse.orgor ends with.synapse.org. Using an endswith check supports any subdomains thatonwebmight legitimately generate (e.g.,www.synapse.org). - Do the same replacement in
test_onweb_project_by_objectandtest_onweb_with_subpagefor their"synapse.org" in url.lower()checks, to keep tests consistent and to resolve any similar CodeQL warnings.
No new methods are needed; only the new import and slight in-test logic changes are required.
-
Copy modified line R6 -
Copy modified lines R191-R193 -
Copy modified lines R206-R208 -
Copy modified lines R224-R226
| @@ -3,6 +3,7 @@ | ||
| from typing import Callable | ||
|
|
||
| import pytest | ||
| from urllib.parse import urlparse | ||
|
|
||
| from synapseclient import Synapse | ||
| from synapseclient.core import utils | ||
| @@ -187,7 +188,9 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname == "synapse.org" or parsed.hostname.endswith(".synapse.org") | ||
| assert project_id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -200,7 +203,9 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname == "synapse.org" or parsed.hostname.endswith(".synapse.org") | ||
| assert project_model.id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -216,7 +221,9 @@ | ||
| # THEN I expect a valid Synapse URL with wiki reference | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname == "synapse.org" or parsed.hostname.endswith(".synapse.org") | ||
| assert project_id in url | ||
| assert subpage_id in url | ||
| assert "Wiki:" in url |
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High test
synapse.org
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 18 days ago
In general, to avoid incomplete URL substring sanitization, parse the URL and validate the hostname (and optionally scheme), instead of checking whether a domain string appears anywhere in the URL. In Python, urllib.parse.urlparse is the standard way to do this. You then compare parsed.hostname or enforce an allow list of hostnames or suffixes.
For this specific test, we should replace the assertion assert "synapse.org" in url.lower() with logic that parses url, extracts the hostname, normalizes it to lowercase, and asserts that the hostname is either exactly synapse.org or ends with .synapse.org. That matches the intent (“a valid Synapse URL”) without relying on substring checks. To implement this, we need to import urllib.parse.urlparse at the top of tests/integration/synapseclient/operations/synchronous/test_utility_operations.py and update the assertions in the three test_onweb_* methods (lines 190, 203, and 219) to use urlparse(url).hostname. No other behavior of the tests should change: they should still check that the returned URL is non-None, a string, and contains the appropriate IDs and labels.
Concretely:
- Add
from urllib.parse import urlparsealongside the existing imports at the top of the file. - In
test_onweb_project_by_id, replaceassert "synapse.org" in url.lower()with a parsed-hostname check. - In
test_onweb_project_by_object, replaceassert "synapse.org" in url.lower()similarly. - In
test_onweb_with_subpage, replaceassert "synapse.org" in url.lower()similarly.
This preserves all existing semantics while enforcing a more precise and secure notion of “Synapse URL”.
-
Copy modified line R6 -
Copy modified lines R191-R193 -
Copy modified lines R206-R208 -
Copy modified lines R224-R226
| @@ -3,6 +3,7 @@ | ||
| from typing import Callable | ||
|
|
||
| import pytest | ||
| from urllib.parse import urlparse | ||
|
|
||
| from synapseclient import Synapse | ||
| from synapseclient.core import utils | ||
| @@ -187,7 +188,9 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname.lower() == "synapse.org" or parsed.hostname.lower().endswith(".synapse.org") | ||
| assert project_id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -200,7 +203,9 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname.lower() == "synapse.org" or parsed.hostname.lower().endswith(".synapse.org") | ||
| assert project_model.id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -216,7 +221,9 @@ | ||
| # THEN I expect a valid Synapse URL with wiki reference | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| assert parsed.hostname is not None | ||
| assert parsed.hostname.lower() == "synapse.org" or parsed.hostname.lower().endswith(".synapse.org") | ||
| assert project_id in url | ||
| assert subpage_id in url | ||
| assert "Wiki:" in url |
| # THEN I expect a valid Synapse URL with wiki reference | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High test
synapse.org
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 18 days ago
In general, the fix is to stop treating the URL as an arbitrary string and instead parse it, then check its hostname (and optionally scheme) explicitly. Rather than verifying that "synapse.org" appears anywhere in the URL, we should use urllib.parse.urlparse to extract netloc or hostname and assert that it matches or safely ends with the expected domain (for example, equals "synapse.org" or ends with ".synapse.org"). This aligns with the recommendation in the background material.
For this specific test file, we only need to adjust the assertions in the test_onweb_* tests so they verify the parsed hostname instead of (or in addition to) using substring membership. The minimal, behavior‑preserving change is:
- Import
urlparsefromurllib.parseat the top of the file. - In
test_onweb_project_by_id,test_onweb_project_by_object, andtest_onweb_with_subpage, replace the assertionassert "synapse.org" in url.lower()with code that:- parses the URL (
parsed = urlparse(url)), - obtains the hostname (
host = parsed.hostname), - and asserts
host == "synapse.org"orhost.endswith(".synapse.org")as appropriate.
- parses the URL (
Given Synapse typically uses a single domain host like www.synapse.org or synapse.org, a robust check is to assert that host is exactly "synapse.org" or ends with ".synapse.org". This prevents "evil-synapse.org" from passing. To avoid changing tested functionality too much, we won’t enforce an exact value for subdomains, only that they are under synapse.org. We can implement a small helper expression inline in each test:
parsed = urlparse(url)
host = parsed.hostname
assert host is not None
assert host == "synapse.org" or host.endswith(".synapse.org")This keeps the rest of the tests unchanged and introduces only one new standard-library import.
-
Copy modified line R6 -
Copy modified lines R191-R194 -
Copy modified lines R207-R210 -
Copy modified lines R226-R229
| @@ -3,6 +3,7 @@ | ||
| from typing import Callable | ||
|
|
||
| import pytest | ||
| from urllib.parse import urlparse | ||
|
|
||
| from synapseclient import Synapse | ||
| from synapseclient.core import utils | ||
| @@ -187,7 +188,10 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| host = parsed.hostname | ||
| assert host is not None | ||
| assert host == "synapse.org" or host.endswith(".synapse.org") | ||
| assert project_id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -200,7 +204,10 @@ | ||
| # THEN I expect a valid Synapse URL to be returned | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| host = parsed.hostname | ||
| assert host is not None | ||
| assert host == "synapse.org" or host.endswith(".synapse.org") | ||
| assert project_model.id in url | ||
| assert "Synapse:" in url | ||
|
|
||
| @@ -216,7 +223,10 @@ | ||
| # THEN I expect a valid Synapse URL with wiki reference | ||
| assert url is not None | ||
| assert isinstance(url, str) | ||
| assert "synapse.org" in url.lower() | ||
| parsed = urlparse(url) | ||
| host = parsed.hostname | ||
| assert host is not None | ||
| assert host == "synapse.org" or host.endswith(".synapse.org") | ||
| assert project_id in url | ||
| assert subpage_id in url | ||
| assert "Wiki:" in url |
Problem:
synapseclientlibrary requires users to instantiate entity classes and call methods on those instances for common operations like storing and deleting, which creates unnecessary friction.get/get_asyncimplementation, additional methods need to be exposed as standalone functions for a more streamlined developer experience.storeanddelete.Solution:
get/get_async:delete/delete_async: Unified interface for deleting any Synapse entity type (File, Folder, Project, Table, Dataset, Team, etc.) with support for:"syn123456"or"syn123456.4")versionparameter > entity'sversion_numberattribute > ID string version)version_only=Truewhen deleting specific versionsstore/store_async: Unified interface for storing any Synapse entity type with type-specific option classes:StoreFileOptions: Controls forsynapse_store,content_type,merge_existing_annotations,associate_activity_to_new_versionStoreContainerOptions: Controls forfailure_strategy(LOG_EXCEPTION vs RAISE_EXCEPTION)StoreTableOptions: Controls fordry_runandjob_timeoutStoreJSONSchemaOptions: Required options for JSONSchema entities includingschema_body,version,dry_runStoreGridOptions: Controls forattach_to_previous_sessionandtimeoutsynapseclient/operations/__init__.pyto expose all new functions and option classes at the package level.Testing:
test_delete_operations_async.py/test_delete_operations.py: Tests for file deletion by ID string and object, version-specific deletion with various precedence scenarios, error handling for invalid IDs and missing version numbers, warning logging for unsupported version deletion on entity types like Project/Foldertest_factory_operations_store_async.py/test_factory_operations_store.py: Tests for storing all supported entity types (Project, Folder, File, Table, Dataset, EntityView, Team, Evaluation, CurationTask, JSONSchema, Grid, etc.), option class functionality, update workflows, and error handling for unsupported types